#!/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:]') # 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 for ARCH in "${ARCH_LIST[@]}"; do swift build -c "$CONF" --arch "$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 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 path to built binary; some SwiftPM versions use .build/$CONF/ when building for host only.resolve_binary_path() { local name="$1" local arch="$2" local candidate candidate=$(build_product_path "$name" "$arch") if [[ -f "$candidate" ]]; then echo "$candidate" return fi if [[ "$arch" == "arm64" || "$arch" == "x86_64" ]] && [[ -f ".build/$CONF/$name" ]]; 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 src=$(resolve_binary_path "$name" "$arch") if [[ -z "$src" || ! -f "$src" ]]; then echo "ERROR: Missing ${name} build for ${arch} at $(build_product_path "$name" "$arch")" >&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.CODEXBAR_BINARY="$(resolve_binary_path "CodexBar" "${ARCH_LIST[0]}")"PREFERRED_BUILD_DIR="$(dirname "${CODEXBAR_BINARY:-$(build_product_path "CodexBar" "${ARCH_LIST[0]}")}")"shopt -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" # Strip extended attributes to prevent AppleDouble (._*) files that break code sealingxattr -cr "$APP"find "$APP" -name '._*' -delete # Sign helper binaries if presentif [[ -f "${APP}/Contents/Helpers/CodexBarCLI" ]]; then codesign "${CODESIGN_ARGS[@]}" "${APP}/Contents/Helpers/CodexBarCLI"fiif [[ -f "${APP}/Contents/Helpers/CodexBarClaudeWatchdog" ]]; then codesign "${CODESIGN_ARGS[@]}" "${APP}/Contents/Helpers/CodexBarClaudeWatchdog"fi # Sign widget extension if presentif [[ -d "${APP}/Contents/PlugIns/CodexBarWidget.appex" ]]; then codesign "${CODESIGN_ARGS[@]}" \ --entitlements "$WIDGET_ENTITLEMENTS" \ "$APP/Contents/PlugIns/CodexBarWidget.appex/Contents/MacOS/CodexBarWidget" codesign "${CODESIGN_ARGS[@]}" \ --entitlements "$WIDGET_ENTITLEMENTS" \ "$APP/Contents/PlugIns/CodexBarWidget.appex"fi # Finally sign the app bundle itselfcodesign "${CODESIGN_ARGS[@]}" \ --entitlements "$APP_ENTITLEMENTS" \ "$APP" rm -rf "$APP_FINAL"mv "$APP" "$APP_FINAL"APP="$APP_FINAL"echo "Created $APP"
#!/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:]')
# 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
for ARCH in "${ARCH_LIST[@]}"; do
swift build -c "$CONF" --arch "$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
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 path to built binary; some SwiftPM versions use .build/$CONF/ when building for host only.
resolve_binary_path() {
local name="$1"
local arch="$2"
local candidate
candidate=$(build_product_path "$name" "$arch")
if [[ -f "$candidate" ]]; then
echo "$candidate"
return
fi
if [[ "$arch" == "arm64" || "$arch" == "x86_64" ]] && [[ -f ".build/$CONF/$name" ]]; 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
src=$(resolve_binary_path "$name" "$arch")
if [[ -z "$src" || ! -f "$src" ]]; then
echo "ERROR: Missing ${name} build for ${arch} at $(build_product_path "$name" "$arch")" >&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.
CODEXBAR_BINARY="$(resolve_binary_path "CodexBar" "${ARCH_LIST[0]}")"
PREFERRED_BUILD_DIR="$(dirname "${CODEXBAR_BINARY:-$(build_product_path "CodexBar" "${ARCH_LIST[0]}")}")"
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"
# Strip extended attributes to prevent AppleDouble (._*) files that break code sealing
xattr -cr "$APP"
find "$APP" -name '._*' -delete
# Sign helper binaries if present
if [[ -f "${APP}/Contents/Helpers/CodexBarCLI" ]]; then
codesign "${CODESIGN_ARGS[@]}" "${APP}/Contents/Helpers/CodexBarCLI"
fi
if [[ -f "${APP}/Contents/Helpers/CodexBarClaudeWatchdog" ]]; then
codesign "${CODESIGN_ARGS[@]}" "${APP}/Contents/Helpers/CodexBarClaudeWatchdog"
fi
# Sign widget extension if present
if [[ -d "${APP}/Contents/PlugIns/CodexBarWidget.appex" ]]; then
codesign "${CODESIGN_ARGS[@]}" \
--entitlements "$WIDGET_ENTITLEMENTS" \
"$APP/Contents/PlugIns/CodexBarWidget.appex/Contents/MacOS/CodexBarWidget"
codesign "${CODESIGN_ARGS[@]}" \
--entitlements "$WIDGET_ENTITLEMENTS" \
"$APP/Contents/PlugIns/CodexBarWidget.appex"
fi
# Finally sign the app bundle itself
codesign "${CODESIGN_ARGS[@]}" \
--entitlements "$APP_ENTITLEMENTS" \
"$APP"
rm -rf "$APP_FINAL"
mv "$APP" "$APP_FINAL"
APP="$APP_FINAL"
echo "Created $APP"