diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e72b1c..20e18ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,9 +50,38 @@ jobs: with: python-version: "3.12" cache: pip + # Import the Developer ID Application cert into a temporary keychain so + # build_macos.sh can codesign. No-op (unsigned build) when the secret is + # absent, so forks / unconfigured repos still produce a working .dmg. + - name: Import Developer ID certificate + env: + CERT_B64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} + CERT_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + if [ -z "${CERT_B64}" ]; then + echo "MACOS_CERTIFICATE_P12_BASE64 not set — building unsigned." + exit 0 + fi + KEYCHAIN="$RUNNER_TEMP/signing.keychain-db" + KEYCHAIN_PW="$(uuidgen)" + printf '%s' "${CERT_B64}" | base64 --decode > "$RUNNER_TEMP/cert.p12" + security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN" + security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "${CERT_PASSWORD}" \ + -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PW" "$KEYCHAIN" >/dev/null + security list-keychains -d user -s "$KEYCHAIN" \ + $(security list-keychains -d user | sed 's/["[:space:]]//g') + rm -f "$RUNNER_TEMP/cert.p12" - name: Build .app + .dmg env: MAKE_DMG: "1" + MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} + MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} + MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }} run: bash packaging/build_macos.sh - uses: actions/upload-artifact@v4 with: diff --git a/docs/building.md b/docs/building.md index c958f5f..de52fd1 100644 --- a/docs/building.md +++ b/docs/building.md @@ -16,9 +16,44 @@ bash packaging/build_macos.sh # dist/AutoPTZ.app MAKE_DMG=1 bash packaging/build_macos.sh # + dist/AutoPTZ--macos-arm64.dmg ``` -Produces an unsigned, correctly-named bundle (`CFBundleName=AutoPTZ`). Signing + -notarization need your Apple Developer ID — the exact `codesign` / `notarytool` / -`stapler` commands are printed at the end of the script. +Produces a correctly-named bundle (`CFBundleName=AutoPTZ`). By default it is +**unsigned** and the script prints the manual `codesign` / `notarytool` / `stapler` +commands at the end. + +### Signing + notarization (opt-in) + +Set `MACOS_SIGN_IDENTITY` to your Developer ID and the script signs the `.app` (and, +with `MAKE_DMG=1`, the `.dmg`). Add notary credentials and it also notarizes + +staples: + +```bash +export MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" +# notarize too (either a stored profile, or Apple-ID creds): +export MACOS_NOTARY_KEYCHAIN_PROFILE="AUTOPTZ_NOTARY" # from `notarytool store-credentials` +# …or… +export MACOS_NOTARY_APPLE_ID="you@example.com" +export MACOS_NOTARY_TEAM_ID="TEAMID" +export MACOS_NOTARY_PASSWORD="app-specific-password" +MAKE_DMG=1 bash packaging/build_macos.sh # signed + notarized + stapled .dmg +``` + +`security find-identity -v -p codesigning` lists your identity + Team ID. Entitlements +come from `packaging/entitlements.plist` (hardened runtime, required for notarization). + +### Signed releases in CI + +The [release workflow](../.github/workflows/release.yml) signs + notarizes the published +`.dmg` automatically once these repository secrets are set (Settings → Secrets and +variables → Actions); without them it still builds an unsigned `.dmg`: + +| Secret | What it is | +| --- | --- | +| `MACOS_CERTIFICATE_P12_BASE64` | Your "Developer ID Application" cert exported as `.p12`, base64-encoded (`base64 -i cert.p12 \| pbcopy`). | +| `MACOS_CERTIFICATE_PASSWORD` | The password you set when exporting the `.p12`. | +| `MACOS_SIGN_IDENTITY` | `Developer ID Application: Your Name (TEAMID)`. | +| `MACOS_NOTARY_APPLE_ID` | Apple ID email for notarization. | +| `MACOS_NOTARY_TEAM_ID` | Your 10-character Team ID. | +| `MACOS_NOTARY_PASSWORD` | An app-specific password for that Apple ID. | ## Windows → `.exe` + installer diff --git a/docs/installation.md b/docs/installation.md index 096964b..7556775 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,9 +6,9 @@ Download the latest build for your OS from the [Releases page](https://github.com/AutoPTZ/autoptz/releases): - **macOS** — `AutoPTZ--macos-arm64.dmg`. Open it and drag **AutoPTZ** - to Applications. The builds are currently **unsigned**, so the first launch - needs: right-click the app → **Open** → **Open** (or System Settings → Privacy - & Security → *Open Anyway*). + to Applications. Signed + notarized releases open normally. If you build it + yourself unsigned, the first launch needs: right-click the app → **Open** → + **Open** (or System Settings → Privacy & Security → *Open Anyway*). - **Windows** — `AutoPTZ--windows-x64-setup.exe`. Run it; it installs Start-menu/desktop shortcuts and an uninstaller. SmartScreen may warn on the unsigned installer — **More info → Run anyway**. diff --git a/packaging/build_macos.sh b/packaging/build_macos.sh index 33eabf9..a0dc406 100755 --- a/packaging/build_macos.sh +++ b/packaging/build_macos.sh @@ -26,6 +26,38 @@ echo "==> AutoPTZ macOS build" echo " repo: ${ROOT}" echo " venv: ${VENV}" +# ── signing config (opt-in) ────────────────────────────────────────────────── +# Sign + notarize only when MACOS_SIGN_IDENTITY is set (CI sets it from repo +# secrets; locally, export it to sign with your Developer ID, e.g. +# export MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" +# Unset → unsigned build, and the manual sign/notarize commands are printed at +# the end instead. +SIGN_IDENTITY="${MACOS_SIGN_IDENTITY:-}" + +# Notarize + staple a signed artifact (.app or .dmg) with whichever credentials +# are available; a no-op (with a note) when none are set. +notarize_and_staple() { + local target="$1" + if [[ -n "${MACOS_NOTARY_KEYCHAIN_PROFILE:-}" ]]; then + echo "==> Notarizing ${target} via keychain profile ${MACOS_NOTARY_KEYCHAIN_PROFILE}" + xcrun notarytool submit "${target}" \ + --keychain-profile "${MACOS_NOTARY_KEYCHAIN_PROFILE}" --wait + elif [[ -n "${MACOS_NOTARY_APPLE_ID:-}" && -n "${MACOS_NOTARY_TEAM_ID:-}" \ + && -n "${MACOS_NOTARY_PASSWORD:-}" ]]; then + echo "==> Notarizing ${target} via Apple ID ${MACOS_NOTARY_APPLE_ID}" + xcrun notarytool submit "${target}" \ + --apple-id "${MACOS_NOTARY_APPLE_ID}" \ + --team-id "${MACOS_NOTARY_TEAM_ID}" \ + --password "${MACOS_NOTARY_PASSWORD}" --wait + else + echo "==> Signed but NOT notarized (no notary credentials): ${target}" + echo " Set MACOS_NOTARY_APPLE_ID/TEAM_ID/PASSWORD or MACOS_NOTARY_KEYCHAIN_PROFILE." + return 0 + fi + echo "==> Stapling ${target}" + xcrun stapler staple "${target}" +} + # ── 1. venv ───────────────────────────────────────────────────────────────── if [[ ! -x "${PY}" ]]; then echo "==> Creating venv at ${VENV}" @@ -69,6 +101,18 @@ echo "==> Verifying CFBundleName (the app-name fix):" /usr/libexec/PlistBuddy -c 'Print :CFBundleName' "${APP}/Contents/Info.plist" plutil -lint "${APP}/Contents/Info.plist" +# ── 5b. sign the .app (opt-in) ─────────────────────────────────────────────── +# Deep-sign with the hardened runtime + entitlements so the bundle is notarizable. +# Must happen BEFORE the .dmg is built so the dmg ships the signed app. +if [[ -n "${SIGN_IDENTITY}" ]]; then + echo "==> Codesigning ${APP} with: ${SIGN_IDENTITY}" + codesign --deep --force --options runtime --timestamp \ + --entitlements packaging/entitlements.plist \ + --sign "${SIGN_IDENTITY}" "${APP}" + codesign --verify --deep --strict --verbose=2 "${APP}" + echo "==> ${APP} signed" +fi + # ── 6. (optional) DMG ───────────────────────────────────────────────────────── # MAKE_DMG=1 produces a compressed, versioned dmg with an /Applications symlink # (drag-to-install). Dependency-free (uses macOS hdiutil). The release workflow @@ -85,6 +129,19 @@ if [[ "${MAKE_DMG:-0}" == "1" ]]; then -ov -format UDZO "${DMG}" rm -rf "${STAGE}" echo "==> Built ${DMG}" + + # Sign + notarize + staple the distributed .dmg (opt-in). + if [[ -n "${SIGN_IDENTITY}" ]]; then + echo "==> Codesigning ${DMG}" + codesign --force --timestamp --sign "${SIGN_IDENTITY}" "${DMG}" + notarize_and_staple "${DMG}" + fi +fi + +if [[ -n "${SIGN_IDENTITY}" ]]; then + echo + echo "==> Signed build complete (notarized if notary credentials were provided)." + exit 0 fi cat <<'EOF' diff --git a/packaging/entitlements.plist b/packaging/entitlements.plist index 1100237..dd5c88a 100644 --- a/packaging/entitlements.plist +++ b/packaging/entitlements.plist @@ -4,7 +4,7 @@ packaging/entitlements.plist : macOS hardened-runtime entitlements. Applied at signing time via codesign (see packaging/build_macos.sh and - docs/v2-rework/11-packaging-and-distribution.md for the exact commands). + docs/building.md for the exact commands and the CI signing secrets). Hardened runtime is REQUIRED for Apple notarization. The JIT and unsigned-executable-memory entitlements below are what Python plus PySide6 / Qt