#!/bin/bash # codesign.sh — deepest-first walker for the Jarvis bundle. # # Signs every nested .app under Contents/Helpers/ with its own per-helper # .entitlements first, then signs the main app last. Never uses --deep # (TN2206 / Pitfall #3) — --deep re-signs nested bundles with the parent's # entitlements and strips per-helper grants. # # Expected env from Xcode: # BUILT_PRODUCTS_DIR, WRAPPER_NAME, EXPANDED_CODE_SIGN_IDENTITY, # OTHER_CODE_SIGN_FLAGS, SRCROOT set -euo pipefail APP="${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}" HELPERS_DIR="${APP}/Contents/Helpers" IDENTITY="${EXPANDED_CODE_SIGN_IDENTITY:-}" # Hardened Runtime + ad-hoc + restricted-entitlement-free Debug bundles # still get rejected by AMFI on launch because the kernel's # "non-platform Team IDs must match" rule fires when HR is on. Debug must # sign WITHOUT --options=runtime regardless of what OTHER_CODE_SIGN_FLAGS # inheritance produced (xcodegen Debug overrides don't always reach # this script's env). Explicit per-config branch is the clearest fix. if [ "${CONFIGURATION:-Release}" = "Debug" ]; then FLAGS="--timestamp=none" else FLAGS="${OTHER_CODE_SIGN_FLAGS---options=runtime --timestamp}" fi if [ -z "${IDENTITY:-}" ]; then echo "error: EXPANDED_CODE_SIGN_IDENTITY is empty — is CODE_SIGN_IDENTITY configured?" >&2 exit 1 fi # 1. Sign each nested helper .app DEEPEST-FIRST via `find -depth`. # Each helper carries its OWN .entitlements under Contents/Resources/ # matching the helper's binary name (.app/Contents/Resources/.entitlements). if [ -d "$HELPERS_DIR" ]; then while IFS= read -r HELPER; do HELPER_NAME="$(basename "$HELPER" .app)" ENT="$HELPER/Contents/Resources/$HELPER_NAME.entitlements" if [ ! -f "$ENT" ]; then echo "error: missing entitlements for helper $HELPER (expected $ENT)" >&2 exit 1 fi echo "codesign (helper): $HELPER" # shellcheck disable=SC2086 /usr/bin/codesign --force --sign "$IDENTITY" $FLAGS \ --entitlements "$ENT" \ "$HELPER" done < <(find "$HELPERS_DIR" -depth -name "*.app" -type d) fi # 1b. Sign nested XCTest plugin bundles under Contents/PlugIns/ DEEPEST-FIRST. # XCTest bundles (e.g., JarvisEntitlementProbeTests.xctest) are signed without # entitlements — they ride on the main app's entitlement set via the test host, # and forcing entitlements on a .xctest breaks XCTest runtime loading. Signing # is still mandatory because codesign refuses to sign a container whose nested # bundles are unsigned. # # Empty xctest directory shells appear in the bundle before Xcode has compiled # the test binary (the Run Script phases on the Jarvis target can run before # the dependent test target's compile step). Skip any xctest that lacks an # Info.plist or a MacOS/ — those are pre-compile artifacts and will be # signed by a later build iteration once the binary is produced. PLUGINS_DIR="${APP}/Contents/PlugIns" if [ -d "$PLUGINS_DIR" ]; then while IFS= read -r PLUGIN; do PLUGIN_NAME="$(basename "$PLUGIN" .xctest)" PLUGIN_INFO="$PLUGIN/Contents/Info.plist" PLUGIN_BIN="$PLUGIN/Contents/MacOS/$PLUGIN_NAME" if [ ! -f "$PLUGIN_INFO" ] || [ ! -f "$PLUGIN_BIN" ]; then # Empty xctest directory shells can appear in BUILT_PRODUCTS_DIR before Xcode # has compiled the test binary (build-for-testing materializes the destination # path early so PBXCopyFilesBuildPhase has something to target). codesign on # the parent bundle refuses to sign a container with an unsigned empty # subdirectory. Remove the empty shell so the main-app sign succeeds; Xcode # re-creates + populates it in a later build iteration and the test-target's # own codesign step handles signing. echo "codesign (plugin removed — empty shell, not yet compiled): $PLUGIN" rm -rf "$PLUGIN" continue fi echo "codesign (plugin): $PLUGIN" # shellcheck disable=SC2086 /usr/bin/codesign --force --sign "$IDENTITY" $FLAGS \ "$PLUGIN" done < <(find "$PLUGINS_DIR" -depth -name "*.xctest" -type d) fi # 1c. Sign Xcode 26's "debug dylib" (Swift incremental-link artifact) under # Contents/MacOS/*.debug.dylib — same identity as main, no entitlements. # Known rough edge: on Debug config with ad-hoc signing, dyld can still # reject the dylib for xctest bundle loading ("different Team IDs") due to # an Xcode 26 quirk where the incremental-link artifact carries metadata # that diverges from the outer bundle's re-sign. SPM package tests are # unaffected; JarvisAppTests integration coverage is tracked in HUMAN-UAT.md. for DYLIB in "${APP}/Contents/MacOS/"*.debug.dylib; do [ -f "$DYLIB" ] || continue echo "codesign (debug dylib): $DYLIB" # shellcheck disable=SC2086 /usr/bin/codesign --force --sign "$IDENTITY" $FLAGS "$DYLIB" done # 2. Sign the main app LAST with its own config-specific entitlements. # Debug and Release have different entitlement sets (split 2026-04-23) — # Debug omits `speech-recognition-assets` because AMFI rejects ad-hoc signed # bundles carrying managed entitlements (RunningBoard error 5 on xctest launch). # Falls back to the Xcode-provided CODE_SIGN_ENTITLEMENTS path (which resolves # per-config via project.yml) before hard-coded paths. if [ -n "${CODE_SIGN_ENTITLEMENTS:-}" ] && [ -f "${SRCROOT}/${CODE_SIGN_ENTITLEMENTS}" ]; then MAIN_ENT="${SRCROOT}/${CODE_SIGN_ENTITLEMENTS}" elif [ "${CONFIGURATION:-}" = "Debug" ] && [ -f "${SRCROOT}/App/Jarvis.Debug.entitlements" ]; then MAIN_ENT="${SRCROOT}/App/Jarvis.Debug.entitlements" elif [ "${CONFIGURATION:-}" = "Release" ] && [ -f "${SRCROOT}/App/Jarvis.Release.entitlements" ]; then MAIN_ENT="${SRCROOT}/App/Jarvis.Release.entitlements" elif [ -f "${SRCROOT}/App/Jarvis.entitlements" ]; then MAIN_ENT="${SRCROOT}/App/Jarvis.entitlements" else echo "error: no main app entitlements resolvable (CONFIGURATION=${CONFIGURATION:-}, CODE_SIGN_ENTITLEMENTS=${CODE_SIGN_ENTITLEMENTS:-})" >&2 exit 1 fi echo "codesign.sh: main entitlements = $MAIN_ENT" if [ ! -f "$MAIN_ENT" ]; then echo "error: main app entitlements not found at $MAIN_ENT" >&2 exit 1 fi echo "codesign (main): $APP" # shellcheck disable=SC2086 /usr/bin/codesign --force --sign "$IDENTITY" $FLAGS \ --entitlements "$MAIN_ENT" \ "$APP" # Note: the -deep flag is intentionally absent — the nested-helpers walk above # is how we enforce per-helper entitlements. verify-codesign-settings.sh lints # the pbxproj for its absence as a belt-and-braces defense. echo "codesign complete"