Files
Jarvis/scripts/codesign.sh
james.maes 21644001b5 fix(codesign): Debug must sign WITHOUT runtime flag — AMFI rejects HR + adhoc
Debug builds were silently coming out (adhoc,runtime) despite project.yml
setting ENABLE_HARDENED_RUNTIME: NO and OTHER_CODE_SIGN_FLAGS: "" on the
Debug config. Two compounding bugs:

  1. project.yml's Debug-config OTHER_CODE_SIGN_FLAGS="" override didn't
     consistently propagate to the codesign.sh build-phase env (the
     parent Run Script env vs xcodegen-generated PBX project setting
     scope diverges).
  2. codesign.sh's ${OTHER_CODE_SIGN_FLAGS:-default} treats empty same as
     unset, re-introducing --options=runtime even if the override DID
     reach the script.

The combined effect: Hardened Runtime stayed ON, AMFI's strict
non-platform-Team-IDs-must-match rule fired against the adhoc-signed
Jarvis.debug.dylib, dyld aborted at __abort_with_payload (svc 0x209)
before any user code ran. App couldn't launch from Xcode (⌘R) or open.

Fix: explicit ${CONFIGURATION:-Release} == Debug branch in codesign.sh
that signs Debug with --timestamp=none and no runtime flag at all.
Result: Debug bundle is flags=0x2(adhoc) only — AMFI accepts it, app
launches, writes 'Jarvis launching' to system.log.

Also tried SWIFT_ENABLE_DEBUG_DYLIB: NO on Debug — the setting didn't
take in Xcode 26 (dylib still produced) but the codesign fix made that
moot. Kept the project.yml line as documentation; harmless if Xcode
ignores it.

Next blocker (Phase 1 self-check): app reaches main and logs
'Jarvis launching' but then hard-blocks on JarvisEntitlementsVerified
missing from Info.plist. That marker is supposed to be written by
verify-entitlements.sh --post-codesign. Investigate that script's
post-codesign Info.plist write path next session.
2026-04-26 07:18:21 -05:00

138 lines
6.5 KiB
Bash
Executable File

#!/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 (<HelperName>.app/Contents/Resources/<HelperName>.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/<binary> — 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"