#!/usr/bin/env bashset -euo pipefailCONF=${1:-release}ALLOW_LLDB=${CODEXBAR_ALLOW_LLDB:-0}SIGNING_MODE=${CODEXBAR_SIGNING:-}ROOT=$(cd "$(dirname "$0")/.." && pwd)cd "$ROOT"LOWER_CONF=$(printf "%s" "$CONF" | tr '[:upper:]' '[:lower:]')case "$LOWER_CONF" in debug|release) ;; *) echo "ERROR: Unsupported build configuration: $CONF (expected debug or release)" >&2 exit 1 ;;esac # Load version infosource "$ROOT/version.env" # Clean build only when explicitly requested (slower).if [[ "${CODEXBAR_FORCE_CLEAN:-0}" == "1" ]]; then if [[ -d "$ROOT/.build" ]]; then if command -v trash >/dev/null 2>&1; then if ! trash "$ROOT/.build"; then echo "WARN: trash .build failed; continuing with swift package clean." >&2 fi else rm -rf "$ROOT/.build" || echo "WARN: rm -rf .build failed; continuing with swift package clean." >&2 fi fi swift package clean >/dev/null 2>&1 || truefi # Build for host architecture by default; allow overriding via ARCHES (e.g., "arm64 x86_64" for universal).ARCH_LIST=( ${ARCHES:-} )if [[ ${#ARCH_LIST[@]} -eq 0 ]]; then HOST_ARCH=$(uname -m) case "$HOST_ARCH" in arm64) ARCH_LIST=(arm64) ;; x86_64) ARCH_LIST=(x86_64) ;; *) ARCH_LIST=("$HOST_ARCH") ;; esacfi patch_keyboard_shortcuts() { local util_path="$ROOT/.build/checkouts/KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift" if [[ ! -f "$util_path" ]]; then return 0 fi if grep -q "keyboardShortcutsSafeBundle" "$util_path"; then return 0 fi chmod +w "$util_path" || true python3 - "$util_path" <<'PY'import sysfrom pathlib import Path path = Path(sys.argv[1])text = path.read_text()if ".keyboardShortcutsSafeBundle" in text: sys.exit(0) text = text.replace( 'NSLocalizedString(self, bundle: .module, comment: self)', 'NSLocalizedString(self, bundle: .keyboardShortcutsSafeBundle, comment: self)',) inject = """private extension Bundle { /// Safe lookup that avoids the fatal trap in the autogenerated `Bundle.module` /// when the resource bundle is not placed at the bundle root. static let keyboardShortcutsSafeBundle: Bundle = { #if os(macOS) if let url = Bundle.main.url(forResource: "KeyboardShortcuts_KeyboardShortcuts", withExtension: "bundle"), let bundle = Bundle(url: url) { return bundle } let rootURL = Bundle.main.bundleURL.appendingPathComponent("KeyboardShortcuts_KeyboardShortcuts.bundle") if let bundle = Bundle(url: rootURL) { return bundle } #endif let devURL = URL(fileURLWithPath: #file) .deletingLastPathComponent() // Utilities.swift .deletingLastPathComponent() // KeyboardShortcuts .deletingLastPathComponent() // Sources .appendingPathComponent("KeyboardShortcuts_KeyboardShortcuts.bundle") if let bundle = Bundle(url: devURL) { return bundle } return Bundle.main }()}""" marker = "}\n\n\nextension Data {"if marker not in text: raise SystemExit("Marker not found in Utilities.swift; patch failed.") text = text.replace(marker, "}\n\n" + inject + "\n\nextension Data {")path.write_text(text)PY} KEYBOARD_SHORTCUTS_UTIL="$ROOT/.build/checkouts/KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift"if [[ ! -f "$KEYBOARD_SHORTCUTS_UTIL" ]]; then swift build -c "$CONF" --arch "${ARCH_LIST[0]}"fipatch_keyboard_shortcuts build_product_path() { local name="$1" local arch="$2" case "$arch" in arm64|x86_64) echo ".build/${arch}-apple-macosx/$CONF/$name" ;; *) echo ".build/$CONF/$name" ;; esac} # Resolve SwiftPM's current output path without relying on a fixed build-system layout.# The output variable keeps the per-arch cache in this shell instead of losing it to# command substitution.swiftpm_bin_path() { local arch="$1" local output_var="$2" local cache_var="SWIFTPM_BIN_PATH_${arch//[^A-Za-z0-9]/_}" if [[ -z "${!cache_var+set}" ]]; then local resolved resolved=$(swift build --show-bin-path -c "$CONF" --arch "$arch" 2>/dev/null || true) printf -v "$cache_var" '%s' "$resolved" fi printf -v "$output_var" '%s' "${!cache_var}"} binary_has_arch() { local binary="$1" local arch="$2" [[ -f "$binary" ]] && lipo -archs "$binary" 2>/dev/null | tr ' ' '\n' | grep -qx "$arch"} # SwiftBuild can reuse one output directory for sequential per-arch builds. Snapshot# each fresh slice before the next build can replace it.PRODUCT_STAGE_ROOT="$ROOT/.build/package-products/$LOWER_CONF"rm -rf "$PRODUCT_STAGE_ROOT" stage_binary_products() { local arch="$1" local bin_dir stage_dir name swiftpm_bin_path "$arch" bin_dir [[ -n "$bin_dir" ]] || return 0 stage_dir="$PRODUCT_STAGE_ROOT/$arch" mkdir -p "$stage_dir" for name in CodexBar CodexBarCLI CodexBarClaudeWatchdog; do if binary_has_arch "$bin_dir/$name" "$arch"; then cp "$bin_dir/$name" "$stage_dir/$name" fi done} for ARCH in "${ARCH_LIST[@]}"; do swift build -c "$CONF" --arch "$ARCH" stage_binary_products "$ARCH"done APP_FINAL="$ROOT/CodexBar.app"APP_STAGE="$ROOT/.build/package/CodexBar.app"rm -rf "$APP_STAGE"APP="$APP_STAGE"mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/Frameworks"mkdir -p "$APP/Contents/Helpers" "$APP/Contents/PlugIns" # Convert new .icon bundle to .icns if present (macOS 14+/IconStudio export)ICON_SOURCE="$ROOT/Icon.icon"ICON_TARGET="$ROOT/Icon.icns"if [[ -f "$ICON_SOURCE" ]]; then iconutil --convert icns --output "$ICON_TARGET" "$ICON_SOURCE"fi BUNDLE_ID="com.steipete.codexbar"FEED_URL="https://raw.githubusercontent.com/steipete/CodexBar/main/appcast.xml"AUTO_CHECKS=trueif [[ "$LOWER_CONF" == "debug" ]]; then BUNDLE_ID="com.steipete.codexbar.debug" FEED_URL="" AUTO_CHECKS=falsefiif [[ "$SIGNING_MODE" == "adhoc" ]]; then FEED_URL="" AUTO_CHECKS=falsefiWIDGET_BUNDLE_ID="${BUNDLE_ID}.widget"APP_TEAM_ID="${APP_TEAM_ID:-Y5PE65HELJ}"APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar"if [[ "$BUNDLE_ID" == *".debug"* ]]; then APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar.debug"fiENTITLEMENTS_DIR="$ROOT/.build/entitlements"APP_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBar.entitlements"WIDGET_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBarWidget.entitlements"mkdir -p "$ENTITLEMENTS_DIR"if [[ "$ALLOW_LLDB" == "1" && "$LOWER_CONF" != "debug" ]]; then echo "ERROR: CODEXBAR_ALLOW_LLDB requires debug configuration" >&2 exit 1ficat > "$APP_ENTITLEMENTS" <<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>com.apple.security.application-groups</key> <array> <string>${APP_GROUP_ID}</string> </array> $(if [[ "$ALLOW_LLDB" == "1" ]]; then echo " <key>com.apple.security.get-task-allow</key><true/>"; fi)</dict></plist>PLISTcat > "$WIDGET_ENTITLEMENTS" <<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>com.apple.security.app-sandbox</key> <true/> <key>com.apple.security.application-groups</key> <array> <string>${APP_GROUP_ID}</string> </array></dict></plist>PLISTBUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") 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>CodexBar</string> <key>CFBundleDisplayName</key><string>CodexBar</string> <key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string> <key>CFBundleExecutable</key><string>CodexBar</string> <key>CFBundlePackageType</key><string>APPL</string> <key>CFBundleShortVersionString</key><string>${MARKETING_VERSION}</string> <key>CFBundleVersion</key><string>${BUILD_NUMBER}</string> <key>LSMinimumSystemVersion</key><string>14.0</string> <key>LSUIElement</key><true/> <key>CFBundleIconFile</key><string>Icon</string> <key>NSHumanReadableCopyright</key><string>© 2026 Peter Steinberger. MIT License.</string> <key>SUFeedURL</key><string>${FEED_URL}</string> <key>SUPublicEDKey</key><string>AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=</string> <key>SUEnableAutomaticChecks</key><${AUTO_CHECKS}/> <key>CodexBuildTimestamp</key><string>${BUILD_TIMESTAMP}</string> <key>CodexGitCommit</key><string>${GIT_COMMIT}</string> <key>CodexBarTeamID</key><string>${APP_TEAM_ID}</string></dict></plist>PLIST # Resolve path to a built binary. Prefer the per-arch snapshot, then SwiftPM's# reported directory, then legacy layouts for older toolchains.resolve_binary_path() { local name="$1" local arch="$2" local bin_dir candidate candidate="$PRODUCT_STAGE_ROOT/$arch/$name" if binary_has_arch "$candidate" "$arch"; then echo "$candidate" return fi swiftpm_bin_path "$arch" bin_dir if [[ -n "$bin_dir" ]] && binary_has_arch "$bin_dir/$name" "$arch"; then echo "$bin_dir/$name" return fi candidate=$(build_product_path "$name" "$arch") if binary_has_arch "$candidate" "$arch"; then echo "$candidate" return fi if [[ "$arch" == "arm64" || "$arch" == "x86_64" ]] && binary_has_arch ".build/$CONF/$name" "$arch"; then echo ".build/$CONF/$name" fi} verify_binary_arches() { local binary="$1"; shift local expected=("$@") local actual actual=$(lipo -archs "$binary") local actual_count expected_count actual_count=$(wc -w <<<"$actual" | tr -d ' ') expected_count=${#expected[@]} if [[ "$actual_count" -ne "$expected_count" ]]; then echo "ERROR: $binary arch mismatch (expected: ${expected[*]}, actual: ${actual})" >&2 exit 1 fi for arch in "${expected[@]}"; do if [[ "$actual" != *"$arch"* ]]; then echo "ERROR: $binary missing arch $arch (have: ${actual})" >&2 exit 1 fi done} install_binary() { local name="$1" local dest="$2" local binaries=() for arch in "${ARCH_LIST[@]}"; do local src bin_dir src=$(resolve_binary_path "$name" "$arch") if [[ -z "$src" || ! -f "$src" ]]; then swiftpm_bin_path "$arch" bin_dir echo "ERROR: Missing ${name} build for ${arch} (looked in: $PRODUCT_STAGE_ROOT/$arch, $bin_dir, $(build_product_path "$name" "$arch"), .build/$CONF)" >&2 exit 1 fi binaries+=("$src") done if [[ ${#ARCH_LIST[@]} -gt 1 ]]; then lipo -create "${binaries[@]}" -output "$dest" else cp "${binaries[0]}" "$dest" fi chmod +x "$dest" verify_binary_arches "$dest" "${ARCH_LIST[@]}"} ensure_widget_extension_project() { local spec="$ROOT/WidgetExtension/project.yml" local project_dir="$ROOT/WidgetExtension/CodexBarWidgetExtension.xcodeproj" if command -v xcodegen >/dev/null 2>&1; then xcodegen generate --spec "$spec" --project "$ROOT/WidgetExtension" --quiet elif [[ ! -f "$project_dir/project.pbxproj" ]]; then echo "ERROR: Missing ${project_dir}; install xcodegen or restore the generated project." >&2 exit 1 fi} build_widget_extension() { local xcode_conf="Release" if [[ "$LOWER_CONF" == "debug" ]]; then xcode_conf="Debug" fi ensure_widget_extension_project local derived_dir="$ROOT/.build/xcode-widget-extension-${LOWER_CONF}" local project_dir="$ROOT/WidgetExtension/CodexBarWidgetExtension.xcodeproj" local build_log="$derived_dir/xcodebuild.log" local timeout_seconds="${CODEXBAR_WIDGET_EXTENSION_TIMEOUT_SECONDS:-900}" local archs="${ARCH_LIST[*]}" mkdir -p "$derived_dir" echo "Building CodexBarWidget Xcode extension (${xcode_conf}, ${archs})." >&2 xcodebuild \ -project "$project_dir" \ -scheme CodexBarWidgetExtension \ -configuration "$xcode_conf" \ -destination "generic/platform=macOS" \ -derivedDataPath "$derived_dir" \ -skipPackageUpdates \ -disableAutomaticPackageResolution \ -skipMacroValidation \ -skipPackagePluginValidation \ CODEXBAR_WIDGET_BUNDLE_ID="$WIDGET_BUNDLE_ID" \ CODEXBAR_TEAM_ID="$APP_TEAM_ID" \ MARKETING_VERSION="$MARKETING_VERSION" \ CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \ CODE_SIGNING_ALLOWED=NO \ ARCHS="$archs" \ ONLY_ACTIVE_ARCH=NO \ build >"$build_log" 2>&1 & local xcodebuild_pid=$! local elapsed=0 while kill -0 "$xcodebuild_pid" 2>/dev/null; do if [[ "$elapsed" -ge "$timeout_seconds" ]]; then kill "$xcodebuild_pid" 2>/dev/null || true wait "$xcodebuild_pid" 2>/dev/null || true tail -80 "$build_log" >&2 || true echo "ERROR: Timed out building CodexBarWidget extension after ${timeout_seconds}s" >&2 exit 1 fi sleep 5 elapsed=$((elapsed + 5)) if (( elapsed > 0 && elapsed % 60 == 0 )); then echo "Still building CodexBarWidget extension (${elapsed}s)..." >&2 fi done if ! wait "$xcodebuild_pid"; then tail -120 "$build_log" >&2 || true echo "ERROR: Failed to build CodexBarWidget extension" >&2 exit 1 fi local appex="$derived_dir/Build/Products/${xcode_conf}/CodexBarWidget.appex" if [[ ! -f "$appex/Contents/MacOS/CodexBarWidget" ]]; then echo "ERROR: Missing Xcode-built CodexBarWidget.appex at ${appex}" >&2 exit 1 fi echo "$appex"} install_widget_extension() { local src_appex src_appex="$(build_widget_extension)" local widget_app="$APP/Contents/PlugIns/CodexBarWidget.appex" rm -rf "$widget_app" mkdir -p "$APP/Contents/PlugIns" cp -R "$src_appex" "$widget_app" verify_binary_arches "$widget_app/Contents/MacOS/CodexBarWidget" "${ARCH_LIST[@]}"} install_binary "CodexBar" "$APP/Contents/MacOS/CodexBar"# Ship CodexBarCLI alongside the app for easy symlinking.if [[ -n "$(resolve_binary_path "CodexBarCLI" "${ARCH_LIST[0]}")" ]]; then install_binary "CodexBarCLI" "$APP/Contents/Helpers/CodexBarCLI"fi# Watchdog helper: ensures `claude` probes die when CodexBar crashes/gets killed.if [[ -n "$(resolve_binary_path "CodexBarClaudeWatchdog" "${ARCH_LIST[0]}")" ]]; then install_binary "CodexBarClaudeWatchdog" "$APP/Contents/Helpers/CodexBarClaudeWatchdog"fiinstall_widget_extension# Embed Sparkle.frameworkif [[ -d ".build/$CONF/Sparkle.framework" ]]; then cp -R ".build/$CONF/Sparkle.framework" "$APP/Contents/Frameworks/" chmod -R a+rX "$APP/Contents/Frameworks/Sparkle.framework" install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP/Contents/MacOS/CodexBar" # Re-sign Sparkle and all nested components with Developer ID + timestamp SPARKLE="$APP/Contents/Frameworks/Sparkle.framework"if [[ "$SIGNING_MODE" == "adhoc" ]]; then CODESIGN_ID="-" CODESIGN_ARGS=(--force --sign "$CODESIGN_ID")elif [[ "$ALLOW_LLDB" == "1" ]]; then CODESIGN_ID="-" CODESIGN_ARGS=(--force --sign "$CODESIGN_ID")else CODESIGN_ID="${APP_IDENTITY:-Developer ID Application: Peter Steinberger (Y5PE65HELJ)}" CODESIGN_ARGS=(--force --timestamp --options runtime --sign "$CODESIGN_ID")fifunction resign() { codesign "${CODESIGN_ARGS[@]}" "$1"; } # Sign innermost binaries first, then the framework root to seal resources resign "$SPARKLE" resign "$SPARKLE/Versions/B/Sparkle" resign "$SPARKLE/Versions/B/Autoupdate" resign "$SPARKLE/Versions/B/Updater.app" resign "$SPARKLE/Versions/B/Updater.app/Contents/MacOS/Updater" resign "$SPARKLE/Versions/B/XPCServices/Downloader.xpc" resign "$SPARKLE/Versions/B/XPCServices/Downloader.xpc/Contents/MacOS/Downloader" resign "$SPARKLE/Versions/B/XPCServices/Installer.xpc" resign "$SPARKLE/Versions/B/XPCServices/Installer.xpc/Contents/MacOS/Installer" resign "$SPARKLE/Versions/B" resign "$SPARKLE"fi if [[ -f "$ICON_TARGET" ]]; then cp "$ICON_TARGET" "$APP/Contents/Resources/Icon.icns"fi # Bundle app resources (provider icons, etc.).APP_RESOURCES_DIR="$ROOT/Sources/CodexBar/Resources"if [[ -d "$APP_RESOURCES_DIR" ]]; then cp -R "$APP_RESOURCES_DIR/." "$APP/Contents/Resources/"fiif [[ ! -f "$APP/Contents/Resources/Icon-classic.icns" ]]; then echo "ERROR: Missing Icon-classic.icns in app bundle resources." >&2 exit 1fi # SwiftPM resource bundles (e.g. KeyboardShortcuts) are emitted next to the built binary.swiftpm_bin_path "${ARCH_LIST[0]}" PREFERRED_BUILD_DIRif [[ -z "$PREFERRED_BUILD_DIR" ]]; then CODEXBAR_BINARY="$(resolve_binary_path "CodexBar" "${ARCH_LIST[0]}")" PREFERRED_BUILD_DIR="$(dirname "${CODEXBAR_BINARY:-$(build_product_path "CodexBar" "${ARCH_LIST[0]}")}")"fishopt -s nullglobSWIFTPM_BUNDLES=("${PREFERRED_BUILD_DIR}/"*.bundle)shopt -u nullglobif [[ ${#SWIFTPM_BUNDLES[@]} -gt 0 ]]; then for bundle in "${SWIFTPM_BUNDLES[@]}"; do bundle_name="$(basename "$bundle")" cp -R "$bundle" "$APP/Contents/Resources/" donefiif [[ ! -d "$APP/Contents/Resources/KeyboardShortcuts_KeyboardShortcuts.bundle" ]]; then echo "ERROR: Missing KeyboardShortcuts SwiftPM resource bundle (Settings → Keyboard shortcut will crash)." >&2 echo "Expected: ${PREFERRED_BUILD_DIR}/KeyboardShortcuts_KeyboardShortcuts.bundle" >&2 exit 1fi # Ensure contents are writable before stripping attributes and signing.chmod -R u+w "$APP" Lines 1–500 of 531. Load more
#!/usr/bin/env bash set -euo pipefail CONF=${1:-release} ALLOW_LLDB=${CODEXBAR_ALLOW_LLDB:-0} SIGNING_MODE=${CODEXBAR_SIGNING:-} ROOT=$(cd "$(dirname "$0")/.." && pwd) cd "$ROOT" LOWER_CONF=$(printf "%s" "$CONF" | tr '[:upper:]' '[:lower:]') case "$LOWER_CONF" in debug|release) ;; *) echo "ERROR: Unsupported build configuration: $CONF (expected debug or release)" >&2 exit 1 ;; esac # Load version info source "$ROOT/version.env" # Clean build only when explicitly requested (slower). if [[ "${CODEXBAR_FORCE_CLEAN:-0}" == "1" ]]; then if [[ -d "$ROOT/.build" ]]; then if command -v trash >/dev/null 2>&1; then if ! trash "$ROOT/.build"; then echo "WARN: trash .build failed; continuing with swift package clean." >&2 fi else rm -rf "$ROOT/.build" || echo "WARN: rm -rf .build failed; continuing with swift package clean." >&2 fi fi swift package clean >/dev/null 2>&1 || true fi # Build for host architecture by default; allow overriding via ARCHES (e.g., "arm64 x86_64" for universal). ARCH_LIST=( ${ARCHES:-} ) if [[ ${#ARCH_LIST[@]} -eq 0 ]]; then HOST_ARCH=$(uname -m) case "$HOST_ARCH" in arm64) ARCH_LIST=(arm64) ;; x86_64) ARCH_LIST=(x86_64) ;; *) ARCH_LIST=("$HOST_ARCH") ;; esac fi patch_keyboard_shortcuts() { local util_path="$ROOT/.build/checkouts/KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift" if [[ ! -f "$util_path" ]]; then return 0 fi if grep -q "keyboardShortcutsSafeBundle" "$util_path"; then return 0 fi chmod +w "$util_path" || true python3 - "$util_path" <<'PY' import sys from pathlib import Path path = Path(sys.argv[1]) text = path.read_text() if ".keyboardShortcutsSafeBundle" in text: sys.exit(0) text = text.replace( 'NSLocalizedString(self, bundle: .module, comment: self)', 'NSLocalizedString(self, bundle: .keyboardShortcutsSafeBundle, comment: self)', ) inject = """ private extension Bundle { /// Safe lookup that avoids the fatal trap in the autogenerated `Bundle.module` /// when the resource bundle is not placed at the bundle root. static let keyboardShortcutsSafeBundle: Bundle = { #if os(macOS) if let url = Bundle.main.url(forResource: "KeyboardShortcuts_KeyboardShortcuts", withExtension: "bundle"), let bundle = Bundle(url: url) { return bundle } let rootURL = Bundle.main.bundleURL.appendingPathComponent("KeyboardShortcuts_KeyboardShortcuts.bundle") if let bundle = Bundle(url: rootURL) { return bundle } #endif let devURL = URL(fileURLWithPath: #file) .deletingLastPathComponent() // Utilities.swift .deletingLastPathComponent() // KeyboardShortcuts .deletingLastPathComponent() // Sources .appendingPathComponent("KeyboardShortcuts_KeyboardShortcuts.bundle") if let bundle = Bundle(url: devURL) { return bundle } return Bundle.main }() } """ marker = "}\n\n\nextension Data {" if marker not in text: raise SystemExit("Marker not found in Utilities.swift; patch failed.") text = text.replace(marker, "}\n\n" + inject + "\n\nextension Data {") path.write_text(text) PY } KEYBOARD_SHORTCUTS_UTIL="$ROOT/.build/checkouts/KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift" if [[ ! -f "$KEYBOARD_SHORTCUTS_UTIL" ]]; then swift build -c "$CONF" --arch "${ARCH_LIST[0]}" fi patch_keyboard_shortcuts build_product_path() { local name="$1" local arch="$2" case "$arch" in arm64|x86_64) echo ".build/${arch}-apple-macosx/$CONF/$name" ;; *) echo ".build/$CONF/$name" ;; esac } # Resolve SwiftPM's current output path without relying on a fixed build-system layout. # The output variable keeps the per-arch cache in this shell instead of losing it to # command substitution. swiftpm_bin_path() { local arch="$1" local output_var="$2" local cache_var="SWIFTPM_BIN_PATH_${arch//[^A-Za-z0-9]/_}" if [[ -z "${!cache_var+set}" ]]; then local resolved resolved=$(swift build --show-bin-path -c "$CONF" --arch "$arch" 2>/dev/null || true) printf -v "$cache_var" '%s' "$resolved" fi printf -v "$output_var" '%s' "${!cache_var}" } binary_has_arch() { local binary="$1" local arch="$2" [[ -f "$binary" ]] && lipo -archs "$binary" 2>/dev/null | tr ' ' '\n' | grep -qx "$arch" } # SwiftBuild can reuse one output directory for sequential per-arch builds. Snapshot # each fresh slice before the next build can replace it. PRODUCT_STAGE_ROOT="$ROOT/.build/package-products/$LOWER_CONF" rm -rf "$PRODUCT_STAGE_ROOT" stage_binary_products() { local arch="$1" local bin_dir stage_dir name swiftpm_bin_path "$arch" bin_dir [[ -n "$bin_dir" ]] || return 0 stage_dir="$PRODUCT_STAGE_ROOT/$arch" mkdir -p "$stage_dir" for name in CodexBar CodexBarCLI CodexBarClaudeWatchdog; do if binary_has_arch "$bin_dir/$name" "$arch"; then cp "$bin_dir/$name" "$stage_dir/$name" fi done } for ARCH in "${ARCH_LIST[@]}"; do swift build -c "$CONF" --arch "$ARCH" stage_binary_products "$ARCH" done APP_FINAL="$ROOT/CodexBar.app" APP_STAGE="$ROOT/.build/package/CodexBar.app" rm -rf "$APP_STAGE" APP="$APP_STAGE" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/Frameworks" mkdir -p "$APP/Contents/Helpers" "$APP/Contents/PlugIns" # Convert new .icon bundle to .icns if present (macOS 14+/IconStudio export) ICON_SOURCE="$ROOT/Icon.icon" ICON_TARGET="$ROOT/Icon.icns" if [[ -f "$ICON_SOURCE" ]]; then iconutil --convert icns --output "$ICON_TARGET" "$ICON_SOURCE" fi BUNDLE_ID="com.steipete.codexbar" FEED_URL="https://raw.githubusercontent.com/steipete/CodexBar/main/appcast.xml" AUTO_CHECKS=true if [[ "$LOWER_CONF" == "debug" ]]; then BUNDLE_ID="com.steipete.codexbar.debug" FEED_URL="" AUTO_CHECKS=false fi if [[ "$SIGNING_MODE" == "adhoc" ]]; then FEED_URL="" AUTO_CHECKS=false fi WIDGET_BUNDLE_ID="${BUNDLE_ID}.widget" APP_TEAM_ID="${APP_TEAM_ID:-Y5PE65HELJ}" APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar" if [[ "$BUNDLE_ID" == *".debug"* ]]; then APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar.debug" fi ENTITLEMENTS_DIR="$ROOT/.build/entitlements" APP_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBar.entitlements" WIDGET_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBarWidget.entitlements" mkdir -p "$ENTITLEMENTS_DIR" if [[ "$ALLOW_LLDB" == "1" && "$LOWER_CONF" != "debug" ]]; then echo "ERROR: CODEXBAR_ALLOW_LLDB requires debug configuration" >&2 exit 1 fi cat > "$APP_ENTITLEMENTS" <<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>com.apple.security.application-groups</key> <array> <string>${APP_GROUP_ID}</string> </array> $(if [[ "$ALLOW_LLDB" == "1" ]]; then echo " <key>com.apple.security.get-task-allow</key><true/>"; fi) </dict> </plist> PLIST cat > "$WIDGET_ENTITLEMENTS" <<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>com.apple.security.app-sandbox</key> <true/> <key>com.apple.security.application-groups</key> <array> <string>${APP_GROUP_ID}</string> </array> </dict> </plist> PLIST BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") 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>CodexBar</string> <key>CFBundleDisplayName</key><string>CodexBar</string> <key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string> <key>CFBundleExecutable</key><string>CodexBar</string> <key>CFBundlePackageType</key><string>APPL</string> <key>CFBundleShortVersionString</key><string>${MARKETING_VERSION}</string> <key>CFBundleVersion</key><string>${BUILD_NUMBER}</string> <key>LSMinimumSystemVersion</key><string>14.0</string> <key>LSUIElement</key><true/> <key>CFBundleIconFile</key><string>Icon</string> <key>NSHumanReadableCopyright</key><string>© 2026 Peter Steinberger. MIT License.</string> <key>SUFeedURL</key><string>${FEED_URL}</string> <key>SUPublicEDKey</key><string>AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=</string> <key>SUEnableAutomaticChecks</key><${AUTO_CHECKS}/> <key>CodexBuildTimestamp</key><string>${BUILD_TIMESTAMP}</string> <key>CodexGitCommit</key><string>${GIT_COMMIT}</string> <key>CodexBarTeamID</key><string>${APP_TEAM_ID}</string> </dict> </plist> PLIST # Resolve path to a built binary. Prefer the per-arch snapshot, then SwiftPM's # reported directory, then legacy layouts for older toolchains. resolve_binary_path() { local name="$1" local arch="$2" local bin_dir candidate candidate="$PRODUCT_STAGE_ROOT/$arch/$name" if binary_has_arch "$candidate" "$arch"; then echo "$candidate" return fi swiftpm_bin_path "$arch" bin_dir if [[ -n "$bin_dir" ]] && binary_has_arch "$bin_dir/$name" "$arch"; then echo "$bin_dir/$name" return fi candidate=$(build_product_path "$name" "$arch") if binary_has_arch "$candidate" "$arch"; then echo "$candidate" return fi if [[ "$arch" == "arm64" || "$arch" == "x86_64" ]] && binary_has_arch ".build/$CONF/$name" "$arch"; then echo ".build/$CONF/$name" fi } verify_binary_arches() { local binary="$1"; shift local expected=("$@") local actual actual=$(lipo -archs "$binary") local actual_count expected_count actual_count=$(wc -w <<<"$actual" | tr -d ' ') expected_count=${#expected[@]} if [[ "$actual_count" -ne "$expected_count" ]]; then echo "ERROR: $binary arch mismatch (expected: ${expected[*]}, actual: ${actual})" >&2 exit 1 fi for arch in "${expected[@]}"; do if [[ "$actual" != *"$arch"* ]]; then echo "ERROR: $binary missing arch $arch (have: ${actual})" >&2 exit 1 fi done } install_binary() { local name="$1" local dest="$2" local binaries=() for arch in "${ARCH_LIST[@]}"; do local src bin_dir src=$(resolve_binary_path "$name" "$arch") if [[ -z "$src" || ! -f "$src" ]]; then swiftpm_bin_path "$arch" bin_dir echo "ERROR: Missing ${name} build for ${arch} (looked in: $PRODUCT_STAGE_ROOT/$arch, $bin_dir, $(build_product_path "$name" "$arch"), .build/$CONF)" >&2 exit 1 fi binaries+=("$src") done if [[ ${#ARCH_LIST[@]} -gt 1 ]]; then lipo -create "${binaries[@]}" -output "$dest" else cp "${binaries[0]}" "$dest" fi chmod +x "$dest" verify_binary_arches "$dest" "${ARCH_LIST[@]}" } ensure_widget_extension_project() { local spec="$ROOT/WidgetExtension/project.yml" local project_dir="$ROOT/WidgetExtension/CodexBarWidgetExtension.xcodeproj" if command -v xcodegen >/dev/null 2>&1; then xcodegen generate --spec "$spec" --project "$ROOT/WidgetExtension" --quiet elif [[ ! -f "$project_dir/project.pbxproj" ]]; then echo "ERROR: Missing ${project_dir}; install xcodegen or restore the generated project." >&2 exit 1 fi } build_widget_extension() { local xcode_conf="Release" if [[ "$LOWER_CONF" == "debug" ]]; then xcode_conf="Debug" fi ensure_widget_extension_project local derived_dir="$ROOT/.build/xcode-widget-extension-${LOWER_CONF}" local project_dir="$ROOT/WidgetExtension/CodexBarWidgetExtension.xcodeproj" local build_log="$derived_dir/xcodebuild.log" local timeout_seconds="${CODEXBAR_WIDGET_EXTENSION_TIMEOUT_SECONDS:-900}" local archs="${ARCH_LIST[*]}" mkdir -p "$derived_dir" echo "Building CodexBarWidget Xcode extension (${xcode_conf}, ${archs})." >&2 xcodebuild \ -project "$project_dir" \ -scheme CodexBarWidgetExtension \ -configuration "$xcode_conf" \ -destination "generic/platform=macOS" \ -derivedDataPath "$derived_dir" \ -skipPackageUpdates \ -disableAutomaticPackageResolution \ -skipMacroValidation \ -skipPackagePluginValidation \ CODEXBAR_WIDGET_BUNDLE_ID="$WIDGET_BUNDLE_ID" \ CODEXBAR_TEAM_ID="$APP_TEAM_ID" \ MARKETING_VERSION="$MARKETING_VERSION" \ CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \ CODE_SIGNING_ALLOWED=NO \ ARCHS="$archs" \ ONLY_ACTIVE_ARCH=NO \ build >"$build_log" 2>&1 & local xcodebuild_pid=$! local elapsed=0 while kill -0 "$xcodebuild_pid" 2>/dev/null; do if [[ "$elapsed" -ge "$timeout_seconds" ]]; then kill "$xcodebuild_pid" 2>/dev/null || true wait "$xcodebuild_pid" 2>/dev/null || true tail -80 "$build_log" >&2 || true echo "ERROR: Timed out building CodexBarWidget extension after ${timeout_seconds}s" >&2 exit 1 fi sleep 5 elapsed=$((elapsed + 5)) if (( elapsed > 0 && elapsed % 60 == 0 )); then echo "Still building CodexBarWidget extension (${elapsed}s)..." >&2 fi done if ! wait "$xcodebuild_pid"; then tail -120 "$build_log" >&2 || true echo "ERROR: Failed to build CodexBarWidget extension" >&2 exit 1 fi local appex="$derived_dir/Build/Products/${xcode_conf}/CodexBarWidget.appex" if [[ ! -f "$appex/Contents/MacOS/CodexBarWidget" ]]; then echo "ERROR: Missing Xcode-built CodexBarWidget.appex at ${appex}" >&2 exit 1 fi echo "$appex" } install_widget_extension() { local src_appex src_appex="$(build_widget_extension)" local widget_app="$APP/Contents/PlugIns/CodexBarWidget.appex" rm -rf "$widget_app" mkdir -p "$APP/Contents/PlugIns" cp -R "$src_appex" "$widget_app" verify_binary_arches "$widget_app/Contents/MacOS/CodexBarWidget" "${ARCH_LIST[@]}" } install_binary "CodexBar" "$APP/Contents/MacOS/CodexBar" # Ship CodexBarCLI alongside the app for easy symlinking. if [[ -n "$(resolve_binary_path "CodexBarCLI" "${ARCH_LIST[0]}")" ]]; then install_binary "CodexBarCLI" "$APP/Contents/Helpers/CodexBarCLI" fi # Watchdog helper: ensures `claude` probes die when CodexBar crashes/gets killed. if [[ -n "$(resolve_binary_path "CodexBarClaudeWatchdog" "${ARCH_LIST[0]}")" ]]; then install_binary "CodexBarClaudeWatchdog" "$APP/Contents/Helpers/CodexBarClaudeWatchdog" fi install_widget_extension # Embed Sparkle.framework if [[ -d ".build/$CONF/Sparkle.framework" ]]; then cp -R ".build/$CONF/Sparkle.framework" "$APP/Contents/Frameworks/" chmod -R a+rX "$APP/Contents/Frameworks/Sparkle.framework" install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP/Contents/MacOS/CodexBar" # Re-sign Sparkle and all nested components with Developer ID + timestamp SPARKLE="$APP/Contents/Frameworks/Sparkle.framework" if [[ "$SIGNING_MODE" == "adhoc" ]]; then CODESIGN_ID="-" CODESIGN_ARGS=(--force --sign "$CODESIGN_ID") elif [[ "$ALLOW_LLDB" == "1" ]]; then CODESIGN_ID="-" CODESIGN_ARGS=(--force --sign "$CODESIGN_ID") else CODESIGN_ID="${APP_IDENTITY:-Developer ID Application: Peter Steinberger (Y5PE65HELJ)}" CODESIGN_ARGS=(--force --timestamp --options runtime --sign "$CODESIGN_ID") fi function resign() { codesign "${CODESIGN_ARGS[@]}" "$1"; } # Sign innermost binaries first, then the framework root to seal resources resign "$SPARKLE" resign "$SPARKLE/Versions/B/Sparkle" resign "$SPARKLE/Versions/B/Autoupdate" resign "$SPARKLE/Versions/B/Updater.app" resign "$SPARKLE/Versions/B/Updater.app/Contents/MacOS/Updater" resign "$SPARKLE/Versions/B/XPCServices/Downloader.xpc" resign "$SPARKLE/Versions/B/XPCServices/Downloader.xpc/Contents/MacOS/Downloader" resign "$SPARKLE/Versions/B/XPCServices/Installer.xpc" resign "$SPARKLE/Versions/B/XPCServices/Installer.xpc/Contents/MacOS/Installer" resign "$SPARKLE/Versions/B" resign "$SPARKLE" fi if [[ -f "$ICON_TARGET" ]]; then cp "$ICON_TARGET" "$APP/Contents/Resources/Icon.icns" fi # Bundle app resources (provider icons, etc.). APP_RESOURCES_DIR="$ROOT/Sources/CodexBar/Resources" if [[ -d "$APP_RESOURCES_DIR" ]]; then cp -R "$APP_RESOURCES_DIR/." "$APP/Contents/Resources/" fi if [[ ! -f "$APP/Contents/Resources/Icon-classic.icns" ]]; then echo "ERROR: Missing Icon-classic.icns in app bundle resources." >&2 exit 1 fi # SwiftPM resource bundles (e.g. KeyboardShortcuts) are emitted next to the built binary. swiftpm_bin_path "${ARCH_LIST[0]}" PREFERRED_BUILD_DIR if [[ -z "$PREFERRED_BUILD_DIR" ]]; then CODEXBAR_BINARY="$(resolve_binary_path "CodexBar" "${ARCH_LIST[0]}")" PREFERRED_BUILD_DIR="$(dirname "${CODEXBAR_BINARY:-$(build_product_path "CodexBar" "${ARCH_LIST[0]}")}")" fi shopt -s nullglob SWIFTPM_BUNDLES=("${PREFERRED_BUILD_DIR}/"*.bundle) shopt -u nullglob if [[ ${#SWIFTPM_BUNDLES[@]} -gt 0 ]]; then for bundle in "${SWIFTPM_BUNDLES[@]}"; do bundle_name="$(basename "$bundle")" cp -R "$bundle" "$APP/Contents/Resources/" done fi if [[ ! -d "$APP/Contents/Resources/KeyboardShortcuts_KeyboardShortcuts.bundle" ]]; then echo "ERROR: Missing KeyboardShortcuts SwiftPM resource bundle (Settings → Keyboard shortcut will crash)." >&2 echo "Expected: ${PREFERRED_BUILD_DIR}/KeyboardShortcuts_KeyboardShortcuts.bundle" >&2 exit 1 fi # Ensure contents are writable before stripping attributes and signing. chmod -R u+w "$APP"