Add Developer ID signing and notarization to the macOS build.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-17 16:25:08 +02:00
commit 452dba4a8a
4 changed files with 191 additions and 10 deletions

View file

@ -94,9 +94,41 @@ jobs:
run: | run: |
brew install gtk4 pkg-config brew install gtk4 pkg-config
# Developer ID lives in repo secrets. Without it the bundle is only
# ad-hoc signed and Gatekeeper still blocks downloaded copies.
- name: Import Developer ID certificate
if: ${{ secrets.MACOS_CERTIFICATE != '' }}
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
set -euo pipefail
cert="$RUNNER_TEMP/certificate.p12"
keychain="$RUNNER_TEMP/signing.keychain-db"
pass="$(openssl rand -base64 32)"
echo "$MACOS_CERTIFICATE" | base64 --decode > "$cert"
security create-keychain -p "$pass" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$pass" "$keychain"
security import "$cert" -k "$keychain" \
-P "$MACOS_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s \
-k "$pass" "$keychain"
security list-keychains -d user -s "$keychain" \
$(security list-keychains -d user | tr -d '"')
rm -f "$cert"
- name: Build .app zip - name: Build .app zip
env: env:
COMMIT: ${{ github.sha }} COMMIT: ${{ github.sha }}
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: | run: |
chmod +x scripts/bundle-macos.sh chmod +x scripts/bundle-macos.sh
./scripts/bundle-macos.sh ./scripts/bundle-macos.sh

View file

@ -33,10 +33,11 @@ build, takže soubor nikdy nezestárne.
Kam se to nainstaluje: `/Applications` nebo `~/Applications` (macOS), Kam se to nainstaluje: `/Applications` nebo `~/Applications` (macOS),
`%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux). `%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux).
Buildy nejsou podepsané, proto je při **prvním** spuštění potřeba výše uvedený Buildy zatím nejsou notarizované (chybí Apple Developer ID), proto je při
krok navíc. Aplikace se pak udržuje sama: po startu porovná svůj commit se **prvním** spuštění potřeba výše uvedený krok navíc. Aplikace se pak udržuje
souborem `VERSION` ve větvi `builds` a aktualizaci nabídne v pruhu nahoře. sama: po startu porovná svůj commit se souborem `VERSION` ve větvi `builds`
Ruční kontrola je v **Nastavení → Aktualizace**. a aktualizaci nabídne v pruhu nahoře. Ruční kontrola je
v **Nastavení → Aktualizace**.
#### Bez instalátoru #### Bez instalátoru
@ -95,6 +96,16 @@ make bundle # AppImage / .app zip / Windows složka s DLL
Volitelně CMake: `cmake -S . -B build && cmake --build build`. Volitelně CMake: `cmake -S . -B build && cmake --build build`.
Aby stažená aplikace na macOS šla otevřít bez potvrzení v Nastavení, CI
potřebuje **Apple Developer Program** a GitHub Secrets:
- `MACOS_CERTIFICATE` — Developer ID Application `.p12` v base64
- `MACOS_CERTIFICATE_PASSWORD`
- `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER` — notarizace
(nebo `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID`)
Bez toho zůstane jen ad-hoc podpis a Gatekeeper stažený build zablokuje.
#### Windows (MSYS2 UCRT64, bez Visual Studia) #### Windows (MSYS2 UCRT64, bez Visual Studia)
```cmd ```cmd
@ -154,10 +165,10 @@ the current build when you run it, so the file never goes stale.
Install locations: `/Applications` or `~/Applications` (macOS), Install locations: `/Applications` or `~/Applications` (macOS),
`%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux). `%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux).
The builds are unsigned, so the first launch needs the extra step above. The builds are not notarized yet (no Apple Developer ID), so the first
After that the app stays current: it compares its commit to `VERSION` on launch still needs the extra step above. After that the app stays current:
`builds` and offers an update in a banner. Manual check: it compares its commit to `VERSION` on `builds` and offers an update in a
**Settings → Updates**. banner. Manual check: **Settings → Updates**.
#### Packages without the installer #### Packages without the installer
@ -216,6 +227,16 @@ make bundle # AppImage / .app zip / Windows DLL folder
Optional CMake: `cmake -S . -B build && cmake --build build`. Optional CMake: `cmake -S . -B build && cmake --build build`.
Skipping Gatekeeper on a downloaded macOS build needs an
**Apple Developer Program** membership and these GitHub Secrets:
- `MACOS_CERTIFICATE` — Developer ID Application `.p12`, base64-encoded
- `MACOS_CERTIFICATE_PASSWORD`
- `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER` for notarization
(or `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID`)
Without them the bundle stays ad-hoc signed and Gatekeeper still blocks it.
#### Windows (MSYS2 UCRT64, no Visual Studio) #### Windows (MSYS2 UCRT64, no Visual Studio)
```cmd ```cmd

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The launcher exports DYLD_LIBRARY_PATH for bundled GTK dylibs. -->
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<!-- Homebrew-copied libraries are re-signed, but GTK still loads extra
modules from Resources; keep validation off so those resolve. -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View file

@ -243,8 +243,120 @@ if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then
iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true
fi fi
# Ad-hoc sign the .app so Gatekeeper is less likely to kill dyld loads. # Developer ID + notarization is what lets a downloaded app open without
codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || true # System Settings. Without a certificate the bundle stays ad-hoc signed, which
# is enough for local runs but Gatekeeper still blocks internet downloads.
ENTITLEMENTS="$ROOT/macos/Maturita.entitlements"
pick_sign_identity() {
if [[ -n "${MACOS_SIGN_IDENTITY:-}" ]]; then
printf '%s\n' "$MACOS_SIGN_IDENTITY"
return
fi
local found
found="$(security find-identity -v -p codesigning 2>/dev/null \
| awk -F'"' '/Developer ID Application/ { print $2; exit }')"
if [[ -n "$found" ]]; then
printf '%s\n' "$found"
return
fi
printf '%s\n' "-"
}
sign_one() {
local path="$1"
local use_entitlements="${2:-}"
local args=(--force --sign "$IDENTITY")
if [[ "$IDENTITY" != "-" ]]; then
args+=(--options runtime --timestamp)
fi
if [[ "$use_entitlements" == "entitlements" && -f "$ENTITLEMENTS" ]]; then
args+=(--entitlements "$ENTITLEMENTS")
fi
codesign "${args[@]}" "$path"
}
sign_bundle() {
local f
echo "==> Signing $APP_NAME"
IDENTITY="$(pick_sign_identity)"
if [[ "$IDENTITY" == "-" ]]; then
echo " identity: ad-hoc (downloaded copies will still need Gatekeeper approval)"
else
echo " identity: $IDENTITY"
fi
shopt -s nullglob
for f in "$FW"/*.dylib; do
sign_one "$f"
done
shopt -u nullglob
sign_one "$MACOS/maturita-bin" entitlements
sign_one "$MACOS/maturita" entitlements
sign_one "$APP" entitlements
codesign --verify --strict --verbose=2 "$APP"
}
can_notarize() {
[[ "$IDENTITY" != "-" ]] || return 1
if [[ -n "${APPLE_API_KEY:-}" && -n "${APPLE_API_KEY_ID:-}" &&
-n "${APPLE_API_ISSUER:-}" ]]; then
return 0
fi
if [[ -n "${APPLE_ID:-}" && -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" &&
-n "${APPLE_TEAM_ID:-}" ]]; then
return 0
fi
return 1
}
notarize_bundle() {
local zip notary_args keyfile
if [[ "$IDENTITY" == "-" ]]; then
return 0
fi
if ! can_notarize; then
if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then
echo "error: Developer ID is present but notarization secrets are missing." >&2
echo "Set APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER, or" >&2
echo "APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID." >&2
exit 1
fi
echo " warning: signed, but not notarized — Gatekeeper will still block downloads"
return 0
fi
echo "==> Notarizing $APP_NAME"
zip="$ROOT/dist/.notarize-$OUT_ZIP"
rm -f "$zip"
ditto -c -k --keepParent "$APP" "$zip"
notary_args=(submit "$zip" --wait)
keyfile=""
if [[ -n "${APPLE_API_KEY:-}" ]]; then
keyfile="$(mktemp)"
printf '%s\n' "$APPLE_API_KEY" > "$keyfile"
notary_args+=(--key "$keyfile" --key-id "$APPLE_API_KEY_ID"
--issuer "$APPLE_API_ISSUER")
else
notary_args+=(--apple-id "$APPLE_ID"
--password "$APPLE_APP_SPECIFIC_PASSWORD"
--team-id "$APPLE_TEAM_ID")
fi
xcrun notarytool "${notary_args[@]}"
[[ -n "$keyfile" ]] && rm -f "$keyfile"
rm -f "$zip"
xcrun stapler staple "$APP"
echo " stapled notarization ticket"
}
sign_bundle
echo "==> Smoke-testing launch" echo "==> Smoke-testing launch"
if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then
@ -270,6 +382,8 @@ else
echo " smoke test OK (process stayed up)" echo " smoke test OK (process stayed up)"
fi fi
notarize_bundle
echo "==> Zipping" echo "==> Zipping"
rm -f "$ROOT/dist/$OUT_ZIP" rm -f "$ROOT/dist/$OUT_ZIP"
( (