diff --git a/BUILD_MAC_KX.md b/BUILD_MAC_KX.md index adee172405..7e3763c683 100644 --- a/BUILD_MAC_KX.md +++ b/BUILD_MAC_KX.md @@ -75,8 +75,10 @@ version `2.4.1-kx1`). with exit 0 and `[202/202] Completed`. - The signature is **ad-hoc / linker-signed**. Strict `codesign -v` and `spctl` complain ("code has no resources but signature indicates they must be present"). - This is **harmless**: a local build is **not quarantined**, so it opens with a - double-click, no Gatekeeper block. + This is **harmless** for launching: a local build is **not quarantined**, so it + opens with a double-click, no Gatekeeper block. It does, however, make macOS + re-ask for folder and keychain permissions on every rebuild — see + *Stop the repeated folder / keychain permission prompts* below to fix that. --- @@ -87,10 +89,41 @@ it, copy under a different name: ```bash ditto build/arm64/OrcaSlicer/OrcaSlicer.app /Applications/OrcaSlicer-KX.app +./scripts/macos_codesign_local.sh # stable local signature — stops repeated permission prompts (see below) open /Applications/OrcaSlicer-KX.app # verify it boots ``` -(`ditto` preserves the bundle + ad-hoc signature better than `cp -R`.) +(`ditto` preserves the bundle + signature better than `cp -R`.) + +--- + +## Stop the repeated folder / keychain permission prompts (recommended) + +Because a local build is only **ad-hoc / linker-signed**, macOS identifies the app +by its content hash (`cdhash`). That hash changes on every rebuild, so macOS treats +each build as a brand-new app and re-asks for **folder access (TCC)** and the +**keychain password** every time — the grants never stick. + +Fix it by signing the app with a **stable, machine-local, self-signed** identity, so +its Designated Requirement becomes certificate-based (identical across rebuilds): + +```bash +./scripts/macos_codesign_local.sh # run AFTER copying the app to /Applications +``` + +On first run the script creates the identity `OrcaSlicer Local Codesign` in your +login keychain (no admin password needed) and re-signs the installed app + local +build output. Re-run it after every build/redeploy — it reuses the same identity. +The Designated Requirement changes from `cdhash H"…"` to +`identifier "com.orcaslicer.OrcaSlicer" and certificate leaf = H"…"`, which is stable. + +On the **next launch only**, allow the folder prompts and click **"Always Allow"** +on the keychain prompt once; macOS then remembers them across future rebuilds. + +**Per-machine — share the script, never the cert.** The certificate and its private +key are generated locally and live only in your login keychain; they are never +written to the repo. Each developer runs the script once on their own Mac to create +their own local identity. Commit and share the *script*, not the certificate. --- diff --git a/scripts/macos_codesign_local.sh b/scripts/macos_codesign_local.sh new file mode 100755 index 0000000000..41f5efc831 --- /dev/null +++ b/scripts/macos_codesign_local.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# +# macos_codesign_local.sh — stop macOS from re-prompting for folder (TCC) and +# Keychain access on every launch/rebuild of a locally-built OrcaSlicer. +# +# WHY THIS IS NEEDED +# On Apple Silicon the linker automatically applies an *ad-hoc* code signature +# to every binary. macOS then identifies the app by the exact hash (cdhash) of +# its contents, so its "Designated Requirement" looks like: +# designated => cdhash H"...." +# macOS binds TCC (folder access) grants and Keychain ACLs to that requirement. +# Because the cdhash changes on every rebuild / redeploy, macOS treats each new +# build as a brand-new unknown app and asks again for folder access and the +# keychain password. Permissions never persist. +# +# WHAT THIS DOES +# Signs the app with a STABLE, self-signed, machine-local code-signing +# certificate. The Designated Requirement then becomes certificate-based: +# designated => identifier "com.orcaslicer.OrcaSlicer" and certificate leaf = H"...." +# which is identical across every rebuild. Grant folder access + click +# "Always Allow" in the keychain ONCE and they persist forever after. +# +# SHARING / PRIVACY +# This script is safe to commit and share (e.g. via Gitea). It is per-machine: +# the certificate and its PRIVATE KEY are generated locally and live only in +# YOUR login keychain — they are never written to the repo. Each developer runs +# this once on their own Mac to create their own local identity. Do NOT export +# or share the certificate/private key; share the script, not the cert. +# +# USAGE +# scripts/macos_codesign_local.sh [APP_BUNDLE ...] +# With no arguments it signs the installed app and the local build output if +# they exist. Run it as the LAST step, AFTER copying the app to /Applications +# (any change to the bundle after signing invalidates the signature). +# +set -euo pipefail + +CERT_CN="OrcaSlicer Local Codesign" +KEYCHAIN="$HOME/Library/Keychains/login.keychain-db" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENTITLEMENTS="$REPO_ROOT/src/dev-utils/platform/osx/entitlements.plist" +BUNDLE_ID="com.orcaslicer.OrcaSlicer" + +# --------------------------------------------------------------------------- +# 1) Ensure a stable local code-signing identity exists (create it if missing). +# --------------------------------------------------------------------------- +if security find-identity -p codesigning 2>/dev/null | grep -qF "$CERT_CN"; then + echo "✓ Local signing identity '$CERT_CN' already present — reusing it." +else + echo "→ Creating local self-signed code-signing identity '$CERT_CN' ..." + TMP="$(mktemp -d)" + trap 'rm -rf "$TMP"' EXIT + + cat > "$TMP/req.cnf" </dev/null 2>&1 + + # Import key + cert separately (avoids PKCS#12 algorithm mismatches between + # OpenSSL 3.x and the macOS `security` importer). -A lets codesign use the key + # without a per-signature keychain prompt. + security import "$TMP/key.pem" -k "$KEYCHAIN" -T /usr/bin/codesign -A >/dev/null + security import "$TMP/cert.pem" -k "$KEYCHAIN" -T /usr/bin/codesign -A >/dev/null + echo "✓ Identity created (self-signed, local only, not exported)." + # Note: the certificate is intentionally left untrusted (CSSMERR_TP_NOT_TRUSTED). + # codesign can still sign with it, and that is all we need for a stable + # Designated Requirement — no admin password / trust settings required. +fi + +# --------------------------------------------------------------------------- +# 2) Collect target bundles. +# --------------------------------------------------------------------------- +TARGETS=("$@") +if [ ${#TARGETS[@]} -eq 0 ]; then + for candidate in \ + "/Applications/OrcaSlicer-KX.app" \ + "/Applications/OrcaSlicer.app" \ + "$REPO_ROOT/build/arm64/OrcaSlicer/OrcaSlicer.app"; do + [ -d "$candidate" ] && TARGETS+=("$candidate") + done +fi + +if [ ${#TARGETS[@]} -eq 0 ]; then + echo "!! No app bundle found to sign. Pass one explicitly:" + echo " $0 /path/to/OrcaSlicer.app" + exit 1 +fi + +# --------------------------------------------------------------------------- +# 3) Sign each target with the stable identity. +# Top-level only (no --deep): nested/third-party dylibs keep their own +# signatures, which is fine because the app is not hardened-runtime. +# --------------------------------------------------------------------------- +ENT_ARG=() +[ -f "$ENTITLEMENTS" ] && ENT_ARG=(--entitlements "$ENTITLEMENTS") + +for app in "${TARGETS[@]}"; do + echo "→ Signing: $app" + codesign --force --sign "$CERT_CN" --identifier "$BUNDLE_ID" "${ENT_ARG[@]}" "$app" + dr="$(codesign -d -r- "$app" 2>&1 | grep -i designated || true)" + echo " $dr" + if echo "$dr" | grep -q "certificate leaf"; then + echo " ✓ Certificate-based requirement — permissions will now persist across rebuilds." + else + echo " !! Unexpected: requirement is not certificate-based." + fi +done + +cat <<'EOF' + +Done. One last time, on the NEXT launch: + • allow the folder-access prompts, and + • click "Always Allow" on any keychain password prompt. +After that macOS will remember them across future rebuilds. Re-run this script +after each build/redeploy (it reuses the same identity). +EOF