Sign builds and releases with a stable code identity
swiftc linker-signs only the inner binary, leaving the bundle unsigned and its codesign identifier as "launcher" rather than the bundle id. More importantly it leaves the app ad-hoc signed, and macOS pins an ad-hoc app's Accessibility grant to its exact cdhash instead of to a designated requirement. Every build mints a new cdhash, so each rebuild-and-replace silently revoked the permission while the app stayed listed and ticked under Privacy & Security — and every release did the same to everyone who updated. That was the root cause of the window tags never appearing. - build.sh signs the bundle with its real identifier, honours SHANNONCOAT_SIGN_IDENTITY, and fails outright rather than falling back to ad-hoc when an identity was asked for explicitly. - The identity is read from a gitignored .env, so it doesn't have to be retyped every build. Parsed rather than sourced, so a stray command in the file can't execute as a side effect of building, and so an existing environment variable still wins. .env.example carries the full one-time setup. - release.yml imports the certificate into a throwaway keychain, builds, verifies, and deletes the keychain on if: always(). It stays inert until the three secrets exist, and fails the release rather than shipping ad-hoc. - Guards the empty-password case explicitly: macOS cannot import an OpenSSL-produced PKCS#12 with an empty password, and reports it as "MAC verification failed (wrong password?)", which sends you hunting for a wrong password rather than a missing one. Nothing local catches this, since the login keychain imports the PEM pair and needs no password. - Ignores *.p12 and *.pem as a backstop; the certificate belongs outside the working tree entirely. Verified end-to-end: two from-scratch builds produce byte-identical designated requirements where ad-hoc differs every time, and six rebuild-reinstall cycles under a real certificate kept the Accessibility grant with no System Settings interaction. This buys permission persistence, not Gatekeeper approval — a self-signed certificate isn't notarized, so downloads still need System Settings -> Privacy & Security -> Open Anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,29 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCES_DIR="$SCRIPT_DIR/Sources"
|
||||
|
||||
# Local, gitignored build configuration — currently just the code-signing
|
||||
# identity, which otherwise has to be remembered and retyped on every
|
||||
# build. See .env.example.
|
||||
#
|
||||
# Parsed rather than sourced, for two reasons: a stray command in the file
|
||||
# shouldn't execute as a side effect of building, and this lets an existing
|
||||
# environment variable win, so `SHANNONCOAT_SIGN_IDENTITY= ./build.sh`
|
||||
# still forces a one-off ad-hoc build without editing the file.
|
||||
ENV_FILE="$SCRIPT_DIR/.env"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||
key="${key#"${key%%[![:space:]]*}"}" # strip leading whitespace
|
||||
key="${key#export }"
|
||||
[[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue # skips blanks and # comments
|
||||
value="${value%"${value##*[![:space:]]}"}" # strip trailing whitespace
|
||||
value="${value%\"}"; value="${value#\"}"
|
||||
value="${value%\'}"; value="${value#\'}"
|
||||
[[ -n "${!key+set}" ]] && continue # already in the environment; leave it
|
||||
printf -v "$key" '%s' "$value"
|
||||
export "${key?}"
|
||||
done <"$ENV_FILE"
|
||||
fi
|
||||
VERSION_FILE="$SCRIPT_DIR/VERSION"
|
||||
ICON_SRC="$SCRIPT_DIR/icon/AppIcon.icns"
|
||||
OUT_DIR="${1:-$SCRIPT_DIR/.build}"
|
||||
@@ -76,6 +99,41 @@ printf '%s' "$GIT_SHA" >"$APP/Contents/Resources/COMMIT"
|
||||
|
||||
swiftc -O "$SOURCES_DIR"/*.swift -o "$APP/Contents/MacOS/launcher"
|
||||
|
||||
# Sign the bundle, not just the binary swiftc linker-signed for us — and
|
||||
# with the bundle's real identifier rather than the "launcher" executable
|
||||
# name codesign would otherwise infer.
|
||||
#
|
||||
# This matters well beyond tidiness, because the window tags need
|
||||
# Accessibility. macOS ties that grant to a *properly signed* app's
|
||||
# designated requirement, which survives a rebuild, but falls back to
|
||||
# pinning an ad-hoc-signed app to its exact cdhash — and every build
|
||||
# produces a new one. So with the ad-hoc default below, each build +
|
||||
# reinstall silently invalidates the permission, while the app stays
|
||||
# listed and stays ticked under Privacy & Security → Accessibility. It
|
||||
# looks granted and isn't, and no amount of toggling that row fixes it:
|
||||
# the stale entry has to be removed with "−" and the new build added back.
|
||||
#
|
||||
# To stop paying that tax on every build, make a self-signed code-signing
|
||||
# certificate once (Keychain Access → Certificate Assistant → Create a
|
||||
# Certificate…, type "Code Signing", self-signed) and point this at it:
|
||||
#
|
||||
# SHANNONCOAT_SIGN_IDENTITY="My Local Signing Cert" ./build.sh
|
||||
#
|
||||
# then re-grant Accessibility one final time. The identity is stable from
|
||||
# then on, so later builds keep the permission.
|
||||
if [[ -n "${SHANNONCOAT_SIGN_IDENTITY:-}" ]]; then
|
||||
# An identity was asked for explicitly, so falling back to ad-hoc would
|
||||
# quietly reintroduce the very problem it was set to avoid — and a
|
||||
# release that ships ad-hoc revokes the Accessibility permission of
|
||||
# everyone who updates. Fail the build instead.
|
||||
codesign --force --sign "$SHANNONCOAT_SIGN_IDENTITY" \
|
||||
--identifier "com.local.shannoncoat" "$APP" \
|
||||
|| { echo "error: signing with \"$SHANNONCOAT_SIGN_IDENTITY\" failed" >&2; exit 1; }
|
||||
else
|
||||
codesign --force --sign - --identifier "com.local.shannoncoat" "$APP" >/dev/null 2>&1 \
|
||||
|| echo "warning: codesign failed; Accessibility permission may not stick" >&2
|
||||
fi
|
||||
|
||||
touch "$APP"
|
||||
echo "Built: $APP"
|
||||
echo "Copy it to /Applications (or ~/Applications) to install."
|
||||
|
||||
Reference in New Issue
Block a user