From dd625921ff065f458482f2eb311e803d8b9d9c12 Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:38:47 +0200 Subject: [PATCH] fix(macos): staple the notarization ticket to the .app, not just the dmg (#10606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stapling only the dmg leaves the LocalAI.app bundle with no embedded notarization ticket. Gatekeeper then falls back to an online notarization check on first launch, so the app fails to open on a Mac that is offline or behind a firewall, or once it has been copied out of the dmg — while it keeps working on the (online) build host, which masks the problem. Notarize and staple the .app before packaging it into the dmg so the bundle verifies offline. Adds a `notarize-app` subcommand to contrib/macos/sign-and-notarize.sh (zips the bundle for notarytool, then staples + validates) and invokes it from dmg-launcher-darwin. Stays a no-op when notary secrets are unset, so unsigned local/fork builds are unaffected. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: mudler Co-authored-by: Ettore Di Giacinto --- Makefile | 9 ++++++-- contrib/macos/sign-and-notarize.sh | 37 ++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) 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 }