399 lines · 14.0 KB · Shell
RawDownload
#!/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" # 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="$ROOT/CodexBar.app"rm -rf "$APP"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=trueLOWER_CONF=$(printf "%s" "$CONF" | tr '[:upper:]' '[:lower:]')if [[ "$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_GROUP_ID="group.com.steipete.codexbar"if [[ "$BUNDLE_ID" == *".debug"* ]]; then  APP_GROUP_ID="group.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>© 2025 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></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[@]}"} 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"fiif [[ -n "$(resolve_binary_path "CodexBarWidget" "${ARCH_LIST[0]}")" ]]; then  WIDGET_APP="$APP/Contents/PlugIns/CodexBarWidget.appex"  mkdir -p "$WIDGET_APP/Contents/MacOS" "$WIDGET_APP/Contents/Resources"  cat > "$WIDGET_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>CodexBarWidget</string>    <key>CFBundleDisplayName</key><string>CodexBar</string>    <key>CFBundleIdentifier</key><string>${WIDGET_BUNDLE_ID}</string>    <key>CFBundleExecutable</key><string>CodexBarWidget</string>    <key>CFBundlePackageType</key><string>XPC!</string>    <key>CFBundleShortVersionString</key><string>${MARKETING_VERSION}</string>    <key>CFBundleVersion</key><string>${BUILD_NUMBER}</string>    <key>LSMinimumSystemVersion</key><string>14.0</string>    <key>NSExtension</key>    <dict>        <key>NSExtensionPointIdentifier</key><string>com.apple.widgetkit-extension</string>        <key>NSExtensionPrincipalClass</key><string>CodexBarWidget.CodexBarWidgetBundle</string>    </dict></dict></plist>PLIST  install_binary "CodexBarWidget" "$WIDGET_APP/Contents/MacOS/CodexBarWidget"fi# 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" echo "Created $APP"