574 lines · 19.0 KB · Shell
RawDownload
#!/usr/bin/env bashset -euo pipefail resolve_package_signing_mode() {  local requested="${CODEXBAR_SIGNING:-adhoc}"  case "$requested" in    adhoc|identity) ;;    *)      echo "ERROR: Unsupported CODEXBAR_SIGNING: $requested (expected adhoc or identity)" >&2      return 1      ;;  esac  SIGNING_MODE="$requested"} verify_no_quarantine_attribute() {  local bundle="$1"  local quarantined  quarantined="$(xattr -r -p com.apple.quarantine "$bundle" 2>/dev/null || true)"  if [[ -n "$quarantined" ]]; then    echo "ERROR: Packaged app still has com.apple.quarantine: ${bundle}" >&2    return 1  fi} verify_packaged_app_integrity() {  local bundle="$1"  local sparkle="$bundle/Contents/Frameworks/Sparkle.framework"   verify_no_quarantine_attribute "$bundle" || return 1  codesign --verify --deep --strict --verbose=2 "$sparkle" || return 1  codesign --verify --deep --strict --verbose=2 "$bundle" || return 1} CONF=${1:-release}ALLOW_LLDB=${CODEXBAR_ALLOW_LLDB:-0}SIGNING_MODE=resolve_package_signing_modeROOT=$(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"source "$ROOT/Scripts/package_product_paths.sh"source "$ROOT/Scripts/sparkle_signing_paths.sh" # 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 # 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    if ! resolved=$(codexbar_swiftpm_bin_path "$CONF" "$arch"); then      return 1    fi    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_build_products() {  local arch="$1"  local bin_dir stage_dir name product  swiftpm_bin_path "$arch" bin_dir   stage_dir="$PRODUCT_STAGE_ROOT/$arch"  mkdir -p "$stage_dir"  for name in CodexBar CodexBarCLI CodexBarClaudeWatchdog; do    if ! product=$(codexbar_require_product_file "$bin_dir" "$name" "$arch"); then      return 1    fi    if ! binary_has_arch "$product" "$arch"; then      echo "ERROR: ${product} does not contain required architecture: ${arch}" >&2      return 1    fi    cp "$product" "$stage_dir/$name"  done  if [[ -d "$bin_dir/CodexBar.dSYM" ]]; then    cp -R "$bin_dir/CodexBar.dSYM" "$stage_dir/"  fi} for ARCH in "${ARCH_LIST[@]}"; do  swift build -c "$CONF" --arch "$ARCH"  stage_build_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>    <key>UTExportedTypeDeclarations</key>    <array>        <dict>            <key>UTTypeIdentifier</key><string>com.steipete.codexbar.menu-layout-item</string>            <key>UTTypeDescription</key><string>CodexBar menu bar layout token</string>            <key>UTTypeConformsTo</key>            <array>                <string>public.data</string>            </array>            <key>UTTypeTagSpecification</key>            <dict/>        </dict>    </array></dict></plist>PLIST # Resolve a built binary from the fresh per-arch snapshot or SwiftPM's reported directory.resolve_binary_path() {  local name="$1"  local arch="$2"  local bin_dir candidate  swiftpm_bin_path "$arch" bin_dir  if ! candidate=$(codexbar_resolve_staged_or_reported_file \    "$PRODUCT_STAGE_ROOT" "$bin_dir" "$name" "$arch"); then    return 1  fi  if ! binary_has_arch "$candidate" "$arch"; then    echo "ERROR: ${candidate} does not contain required architecture: ${arch}" >&2    return 1  fi  echo "$candidate"} 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    if ! src=$(resolve_binary_path "$name" "$arch"); then      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[@]}"} strip_release_binary() {  local binary="$1"  if [[ "$LOWER_CONF" != "release" ]]; then    return 0  fi  if [[ ! -f "$binary" ]]; then    return 0  fi  xcrun strip -x "$binary"} ensure_widget_extension_project() {  local spec="$ROOT/WidgetExtension/project.yml"  local project_dir="$ROOT/WidgetExtension/CodexBarWidgetExtension.xcodeproj"  if [[ -f "$project_dir/project.pbxproj" ]]; then    return  fi  if ! command -v xcodegen >/dev/null 2>&1; then    echo "ERROR: Missing ${project_dir}; install xcodegen or restore the generated project." >&2    exit 1  fi   # The tracked project is authoritative. Regenerating it during packaging records the checkout  # directory's spelling in a package file reference and leaves release worktrees dirty.  xcodegen generate --spec "$spec" --project "$ROOT/WidgetExtension" --quiet} 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"strip_release_binary "$APP/Contents/MacOS/CodexBar"# Ship CodexBarCLI alongside the app for easy symlinking.install_binary "CodexBarCLI" "$APP/Contents/Helpers/CodexBarCLI"strip_release_binary "$APP/Contents/Helpers/CodexBarCLI"# Watchdog helper: ensures `claude` probes die when CodexBar crashes/gets killed.install_binary "CodexBarClaudeWatchdog" "$APP/Contents/Helpers/CodexBarClaudeWatchdog"strip_release_binary "$APP/Contents/Helpers/CodexBarClaudeWatchdog"install_widget_extensionstrip_release_binary "$APP/Contents/PlugIns/CodexBarWidget.appex/Contents/MacOS/CodexBarWidget" swiftpm_bin_path "${ARCH_LIST[0]}" PREFERRED_BUILD_DIR # Embed Sparkle.frameworkSPARKLE_SOURCE=$(codexbar_require_product_directory "$PREFERRED_BUILD_DIR" Sparkle.framework packaging)cp -R "$SPARKLE_SOURCE" "$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 the selected package identity.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)}"

Lines 1–500 of 574.