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.
140 lines
6.7 KiB
Bash
Executable File
140 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Builds "shannoncoat.app" — a menu bar app (no Dock icon, no window) with a
|
||
# custom icon (icon/AppIcon.icns — see icon/generate-icon.swift) for
|
||
# Finder/Spotlight. It stays running in the menu bar: click the status item
|
||
# to switch profiles, or open Manage Profiles… (Cmd+,) to add/remove one.
|
||
# Launch at Login is a toggle inside the app itself.
|
||
#
|
||
# Builds into ./.build (gitignored) rather than installing straight into an
|
||
# Applications dir — copy/drag the .app from there yourself. It's a fully
|
||
# native, self-contained bundle — no bundled script, so copying it anywhere
|
||
# after the build is safe.
|
||
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}"
|
||
APP="$OUT_DIR/shannoncoat.app"
|
||
|
||
# Read from the repo's VERSION file so the .app and the script it bundles
|
||
# never report different numbers.
|
||
VERSION="$(cat "$VERSION_FILE" 2>/dev/null)"
|
||
[[ -n "$VERSION" ]] || { echo "error: couldn't read $VERSION_FILE" >&2; exit 1; }
|
||
|
||
mkdir -p "$OUT_DIR" "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
||
|
||
[[ -f "$ICON_SRC" ]] || { echo "error: $ICON_SRC missing (see icon/generate-icon.swift)" >&2; exit 1; }
|
||
cp "$ICON_SRC" "$APP/Contents/Resources/AppIcon.icns"
|
||
|
||
cat >"$APP/Contents/Info.plist" <<PLIST
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>CFBundleName</key><string>shannoncoat</string>
|
||
<key>CFBundleDisplayName</key><string>shannoncoat</string>
|
||
<key>CFBundleIdentifier</key><string>com.local.shannoncoat</string>
|
||
<key>CFBundleVersion</key><string>$VERSION</string>
|
||
<key>CFBundleShortVersionString</key><string>$VERSION</string>
|
||
<key>CFBundlePackageType</key><string>APPL</string>
|
||
<key>CFBundleExecutable</key><string>launcher</string>
|
||
<key>CFBundleIconFile</key><string>AppIcon</string>
|
||
<key>LSUIElement</key><true/>
|
||
<key>NSHighResolutionCapable</key><true/>
|
||
</dict>
|
||
</plist>
|
||
PLIST
|
||
|
||
command -v swiftc >/dev/null || { echo "error: swiftc not found (install Xcode Command Line Tools: xcode-select --install)" >&2; exit 1; }
|
||
|
||
# VERSION travels in Resources (not baked into the binary) so the About
|
||
# panel and the update checker read the same file this script stamped into
|
||
# Info.plist above.
|
||
cp "$VERSION_FILE" "$APP/Contents/Resources/VERSION"
|
||
|
||
# COMMIT is the short SHA the About tab appends to the version, so one dev
|
||
# build is distinguishable from another — left empty for a real release
|
||
# build, since the version number alone is unambiguous there. A release
|
||
# build is one of:
|
||
# - SHANNONCOAT_RELEASE_BUILD set by release.yml, which already knows
|
||
# for certain whether it's building a tag — trusted outright rather
|
||
# than re-derived, since `git describe` needs full tag refs that a
|
||
# CI runner's shallow checkout isn't guaranteed to have fetched.
|
||
# - a local/manual build with HEAD sitting exactly on a tag (checked
|
||
# via `git describe`, which is reliable here since a normal
|
||
# non-shallow local clone has its full tag history).
|
||
GIT_SHA=""
|
||
if [[ -n "${SHANNONCOAT_RELEASE_BUILD:-}" ]]; then
|
||
: # release build — leave GIT_SHA empty
|
||
elif git -C "$SCRIPT_DIR" rev-parse --git-dir >/dev/null 2>&1 \
|
||
&& ! git -C "$SCRIPT_DIR" describe --tags --exact-match >/dev/null 2>&1; then
|
||
GIT_SHA="$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || true)"
|
||
fi
|
||
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."
|