diff --git a/Makefile b/Makefile index 3e640a3b7..32046d6a3 100644 --- a/Makefile +++ b/Makefile @@ -1482,8 +1482,13 @@ build-launcher-darwin: mv cmd/launcher/LocalAI.app dist/LocalAI.app bash contrib/macos/sign-and-notarize.sh sign dist/LocalAI.app -# Wrap the (signed) app into a drag-to-Applications DMG via hdiutil, then sign the DMG. +# Notarize + staple the .app itself, then wrap it into a drag-to-Applications +# DMG via hdiutil and sign the DMG. The app is stapled BEFORE packaging so the +# bundle carries its own ticket and verifies offline (a dmg-only staple leaves +# the app relying on an online Gatekeeper check, which fails offline / once the +# app is copied out of the dmg). No-op without notary secrets. dmg-launcher-darwin: build-launcher-darwin + bash contrib/macos/sign-and-notarize.sh notarize-app dist/LocalAI.app rm -rf dist/dmg dist/LocalAI.dmg mkdir -p dist/dmg cp -R dist/LocalAI.app dist/dmg/LocalAI.app @@ -1495,7 +1500,7 @@ dmg-launcher-darwin: build-launcher-darwin notarize-launcher-darwin: dmg-launcher-darwin bash contrib/macos/sign-and-notarize.sh notarize dist/LocalAI.dmg -# Single entrypoint for CI: build -> sign app -> dmg -> sign dmg -> notarize -> staple. +# Single entrypoint for CI: build -> sign app -> notarize+staple app -> dmg -> sign dmg -> notarize+staple dmg. release-launcher-darwin: notarize-launcher-darwin @echo "dist/LocalAI.dmg is ready" diff --git a/contrib/macos/sign-and-notarize.sh b/contrib/macos/sign-and-notarize.sh index 73497c769..e7cd3b7be 100755 --- a/contrib/macos/sign-and-notarize.sh +++ b/contrib/macos/sign-and-notarize.sh @@ -71,13 +71,42 @@ cmd_notarize() { echo "[notarize] notarized and stapled $dmg" } +# Notarize and staple the .app bundle itself. Stapling the dmg alone is not +# enough: an app with no embedded ticket has no local proof of notarization, so +# Gatekeeper falls back to an online check — and the app then fails to launch on +# a machine that is offline / behind a firewall, or once it has been copied out +# of the dmg. Stapling the bundle makes it verify offline. notarytool needs an +# archive for a bundle, so we zip it first. +cmd_notarize_app() { + local app="$1" + if [ -z "${MACOS_NOTARY_KEY:-}" ]; then + echo "[notarize] MACOS_NOTARY_KEY unset: skipping notarization of $app" + return 0 + fi + local keyfile zip + keyfile="$(mktemp).p8" + zip="$(mktemp).zip" + echo "$MACOS_NOTARY_KEY" | base64 --decode > "$keyfile" + ditto -c -k --keepParent "$app" "$zip" + xcrun notarytool submit "$zip" \ + --key "$keyfile" \ + --key-id "${MACOS_NOTARY_KEY_ID:?}" \ + --issuer "${MACOS_NOTARY_ISSUER_ID:?}" \ + --wait + rm -f "$keyfile" "$zip" + xcrun stapler staple "$app" + xcrun stapler validate "$app" + echo "[notarize] notarized and stapled $app" +} + main() { local sub="${1:-}"; shift || true case "$sub" in - import-cert) cmd_import_cert ;; - sign) cmd_sign "$@" ;; - notarize) cmd_notarize "$@" ;; - *) echo "usage: $0 {import-cert|sign |notarize }" >&2; exit 2 ;; + import-cert) cmd_import_cert ;; + sign) cmd_sign "$@" ;; + notarize) cmd_notarize "$@" ;; + notarize-app) cmd_notarize_app "$@" ;; + *) echo "usage: $0 {import-cert|sign |notarize |notarize-app }" >&2; exit 2 ;; esac }