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: |
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
env:
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: |
chmod +x 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),
`%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux).
Buildy nejsou podepsané, proto je při **prvním** spuštění potřeba výše uvedený
krok navíc. Aplikace se pak udržuje sama: po startu porovná svůj commit se
souborem `VERSION` ve větvi `builds` a aktualizaci nabídne v pruhu nahoře.
Ruční kontrola je v **Nastavení → Aktualizace**.
Buildy zatím nejsou notarizované (chybí Apple Developer ID), proto je při
**prvním** spuštění potřeba výše uvedený krok navíc. Aplikace se pak udržuje
sama: po startu porovná svůj commit se souborem `VERSION` ve větvi `builds`
a aktualizaci nabídne v pruhu nahoře. Ruční kontrola je
v **Nastavení → Aktualizace**.
#### 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`.
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)
```cmd
@ -154,10 +165,10 @@ the current build when you run it, so the file never goes stale.
Install locations: `/Applications` or `~/Applications` (macOS),
`%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux).
The builds are unsigned, so the first launch needs the extra step above.
After that the app stays current: it compares its commit to `VERSION` on
`builds` and offers an update in a banner. Manual check:
**Settings → Updates**.
The builds are not notarized yet (no Apple Developer ID), so the first
launch still needs the extra step above. After that the app stays current:
it compares its commit to `VERSION` on `builds` and offers an update in a
banner. Manual check: **Settings → Updates**.
#### 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`.
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)
```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
fi
# Ad-hoc sign the .app so Gatekeeper is less likely to kill dyld loads.
codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || true
# Developer ID + notarization is what lets a downloaded app open without
# 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"
if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then
@ -270,6 +382,8 @@ else
echo " smoke test OK (process stayed up)"
fi
notarize_bundle
echo "==> Zipping"
rm -f "$ROOT/dist/$OUT_ZIP"
(