diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 614c1de3ef02..b1b585c4b7a3 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -24,6 +24,11 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ MACOS_SIGN_P12: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
+ MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
+ MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
+ MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
launcher-build-darwin:
runs-on: macos-latest
steps:
@@ -35,9 +40,19 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: 1.23
- - name: Build launcher for macOS ARM64
- run: |
- make build-launcher-darwin
+ - name: Import signing certificate
+ env:
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
+ MACOS_CI_KEYCHAIN_PWD: ${{ secrets.MACOS_CI_KEYCHAIN_PWD }}
+ run: bash contrib/macos/sign-and-notarize.sh import-cert
+ - name: Build, sign and notarize the DMG
+ env:
+ MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
+ MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
+ MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
+ MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
+ run: make release-launcher-darwin
- name: Upload DMG to Release
uses: softprops/action-gh-release@v3
with:
diff --git a/.gitignore b/.gitignore
index 177c79cbaf9b..91582c006bf3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,3 +94,6 @@ core/http/react-ui/test-results/
# SDD / brainstorm scratch (agent-driven development)
.superpowers/
+
+# Local Apple signing material (never commit)
+.certs/
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 71c9c96e4584..88d6e9ecd9e4 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -9,7 +9,8 @@ source:
enabled: true
name_template: '{{ .ProjectName }}-{{ .Tag }}-source'
builds:
- - main: ./cmd/local-ai
+ - id: local-ai
+ main: ./cmd/local-ai
env:
- CGO_ENABLED=0
ldflags:
@@ -35,3 +36,19 @@ snapshot:
version_template: "{{ .Tag }}-next"
changelog:
use: github-native
+# Sign + notarize the macOS server binary via the quill backend (runs on Linux,
+# no macOS runner needed). Disabled automatically when MACOS_SIGN_P12 is unset
+# (forks / PRs), so those builds stay unsigned and green.
+notarize:
+ macos:
+ - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
+ ids:
+ - local-ai
+ sign:
+ certificate: "{{.Env.MACOS_SIGN_P12}}"
+ password: "{{.Env.MACOS_SIGN_PASSWORD}}"
+ notarize:
+ issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
+ key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
+ key: "{{.Env.MACOS_NOTARY_KEY}}"
+ wait: true
diff --git a/Makefile b/Makefile
index be0711b47baf..7ec8293bb24d 100644
--- a/Makefile
+++ b/Makefile
@@ -1449,13 +1449,32 @@ docs: docs/static/gallery.html
########################################################
## fyne cross-platform build
-build-launcher-darwin: build-launcher
- go run github.com/tiagomelo/macos-dmg-creator/cmd/createdmg@latest \
- --appName "LocalAI" \
- --appBinaryPath "$(LAUNCHER_BINARY_NAME)" \
- --bundleIdentifier "com.localai.launcher" \
- --iconPath "core/http/static/logo.png" \
- --outputDir "dist/"
+# Build LocalAI.app from the launcher via fyne (metadata read from cmd/launcher/FyneApp.toml).
+# Signing happens via contrib/macos/sign-and-notarize.sh, which is a no-op when the signing
+# secrets are unset, so unsigned local/fork builds keep working.
+build-launcher-darwin:
+ rm -rf dist/LocalAI.app cmd/launcher/LocalAI.app
+ mkdir -p dist
+ cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os darwin -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME)
+ 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.
+dmg-launcher-darwin: build-launcher-darwin
+ rm -rf dist/dmg dist/LocalAI.dmg
+ mkdir -p dist/dmg
+ cp -R dist/LocalAI.app dist/dmg/LocalAI.app
+ ln -s /Applications dist/dmg/Applications
+ hdiutil create -volname "LocalAI" -srcfolder dist/dmg -ov -format UDZO dist/LocalAI.dmg
+ bash contrib/macos/sign-and-notarize.sh sign dist/LocalAI.dmg
+
+# Submit the DMG to Apple notarization and staple the ticket (no-op without notary secrets).
+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.
+release-launcher-darwin: notarize-launcher-darwin
+ @echo "dist/LocalAI.dmg is ready"
build-launcher-linux:
- cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os linux -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME)-linux && mv launcher.tar.xz ../../$(LAUNCHER_BINARY_NAME)-linux.tar.xz
+ cd cmd/launcher && go run fyne.io/tools/cmd/fyne@latest package -os linux -icon ../../core/http/static/logo.png --executable $(LAUNCHER_BINARY_NAME)-linux && mv LocalAI.tar.xz ../../$(LAUNCHER_BINARY_NAME)-linux.tar.xz
diff --git a/cmd/launcher/FyneApp.toml b/cmd/launcher/FyneApp.toml
new file mode 100644
index 000000000000..cb0fc38b945b
--- /dev/null
+++ b/cmd/launcher/FyneApp.toml
@@ -0,0 +1,8 @@
+Website = "https://localai.io"
+
+[Details]
+Icon = "../../core/http/static/logo.png"
+Name = "LocalAI"
+ID = "com.localai.launcher"
+Version = "0.0.0"
+Build = 1
diff --git a/contrib/macos/Launcher.entitlements b/contrib/macos/Launcher.entitlements
new file mode 100644
index 000000000000..a46f95113f4c
--- /dev/null
+++ b/contrib/macos/Launcher.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
diff --git a/contrib/macos/sign-and-notarize.sh b/contrib/macos/sign-and-notarize.sh
new file mode 100755
index 000000000000..73497c769474
--- /dev/null
+++ b/contrib/macos/sign-and-notarize.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+# Code-sign and notarize macOS artifacts for LocalAI.
+# Every sub-command is a no-op (exit 0) when its required secret is unset,
+# so unsigned builds (forks, local dev, PRs) keep working.
+set -euo pipefail
+
+ENTITLEMENTS="contrib/macos/Launcher.entitlements"
+KEYCHAIN="localai-ci.keychain-db"
+
+cmd_import_cert() {
+ if [ -z "${MACOS_CERTIFICATE:-}" ]; then
+ echo "[sign] MACOS_CERTIFICATE unset: skipping cert import (unsigned build)"
+ return 0
+ fi
+ local certfile keychain_pwd default_keychain
+ certfile="$(mktemp).p12"
+ keychain_pwd="${MACOS_CI_KEYCHAIN_PWD:?MACOS_CI_KEYCHAIN_PWD required when signing}"
+ echo "$MACOS_CERTIFICATE" | base64 --decode > "$certfile"
+ security create-keychain -p "$keychain_pwd" "$KEYCHAIN"
+ security set-keychain-settings -lut 21600 "$KEYCHAIN"
+ security unlock-keychain -p "$keychain_pwd" "$KEYCHAIN"
+ security import "$certfile" -k "$KEYCHAIN" -P "${MACOS_CERTIFICATE_PWD:?}" \
+ -T /usr/bin/codesign -T /usr/bin/security
+ security set-key-partition-list -S apple-tool:,apple:,codesign: \
+ -s -k "$keychain_pwd" "$KEYCHAIN" >/dev/null
+ default_keychain="$(security default-keychain | tr -d ' "')"
+ security list-keychains -d user -s "$KEYCHAIN" "$default_keychain"
+ rm -f "$certfile"
+ echo "[sign] certificate imported into $KEYCHAIN"
+}
+
+cmd_sign() {
+ local target="$1"
+ if [ -z "${MACOS_SIGN_IDENTITY:-}" ]; then
+ echo "[sign] MACOS_SIGN_IDENTITY unset: skipping codesign of $target"
+ return 0
+ fi
+ case "$target" in
+ *.app)
+ # Hardened runtime + entitlements are required for notarizing the app bundle.
+ codesign --deep --force --options runtime --timestamp \
+ --entitlements "$ENTITLEMENTS" \
+ --sign "$MACOS_SIGN_IDENTITY" "$target"
+ ;;
+ *)
+ # A disk image carries no entitlements/runtime; just sign the container.
+ codesign --force --timestamp --sign "$MACOS_SIGN_IDENTITY" "$target"
+ ;;
+ esac
+ codesign --verify --strict --verbose=2 "$target"
+ echo "[sign] signed $target"
+}
+
+cmd_notarize() {
+ local dmg="$1"
+ if [ -z "${MACOS_NOTARY_KEY:-}" ]; then
+ echo "[notarize] MACOS_NOTARY_KEY unset: skipping notarization of $dmg"
+ return 0
+ fi
+ local keyfile
+ keyfile="$(mktemp).p8"
+ echo "$MACOS_NOTARY_KEY" | base64 --decode > "$keyfile"
+ xcrun notarytool submit "$dmg" \
+ --key "$keyfile" \
+ --key-id "${MACOS_NOTARY_KEY_ID:?}" \
+ --issuer "${MACOS_NOTARY_ISSUER_ID:?}" \
+ --wait
+ rm -f "$keyfile"
+ xcrun stapler staple "$dmg"
+ xcrun stapler validate "$dmg"
+ echo "[notarize] notarized and stapled $dmg"
+}
+
+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 ;;
+ esac
+}
+
+main "$@"
diff --git a/docs/content/installation/macos.md b/docs/content/installation/macos.md
index fc254cedb581..dfda42df2a76 100644
--- a/docs/content/installation/macos.md
+++ b/docs/content/installation/macos.md
@@ -22,13 +22,16 @@ Download the latest DMG from GitHub releases:
3. Drag the LocalAI application to your Applications folder
4. Launch LocalAI from your Applications folder
-## Known Issues
+## Verification
-> **Note**: The DMGs are not signed by Apple and may show as quarantined.
->
-> **Workaround**: See [this issue](https://github.com/mudler/LocalAI/issues/6268) for details on how to bypass the quarantine.
->
-> **Fix tracking**: The signing issue is being tracked in [this issue](https://github.com/mudler/LocalAI/issues/6244).
+The `LocalAI.dmg` (and the app inside it) and the `local-ai` server binary are
+signed with an Apple Developer ID and notarized by Apple, so they launch with no
+quarantine prompt or workaround. To inspect the signature yourself:
+
+```bash
+spctl --assess --type open --context context:primary-signature -v /Applications/LocalAI.app
+codesign --verify --deep --strict --verbose=2 /Applications/LocalAI.app
+```
## Next Steps