#!/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" < CFBundleNameshannoncoat CFBundleDisplayNameshannoncoat CFBundleIdentifiercom.local.shannoncoat CFBundleVersion$VERSION CFBundleShortVersionString$VERSION CFBundlePackageTypeAPPL CFBundleExecutablelauncher CFBundleIconFileAppIcon LSUIElement NSHighResolutionCapable 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."