The debugger agent diagnosed the JarvisAppTests xctest launch failure (Runningboard error 5 / Launchd job spawn failed). Root cause: AMFI rejected the ad-hoc signed Debug bundle because it carried the managed `com.apple.developer.speech-recognition-assets` entitlement. The kernel logged `AMFI: Code has restricted entitlements, but the validation of its code signature failed` → `load code signature error 4` → spawn failure → Runningboard error 5. **Changes:** - Split `App/Jarvis.entitlements` → `Jarvis.Debug.entitlements` (no `speech-recognition-assets`; adds `get-task-allow` for XCTest attach) + `Jarvis.Release.entitlements` (full managed set, unchanged). - `project.yml`: per-config `CODE_SIGN_ENTITLEMENTS`; Debug gets `ENABLE_HARDENED_RUNTIME=NO` + `OTHER_CODE_SIGN_FLAGS=""` + `ONLY_ACTIVE_ARCH=YES`; `DEVELOPMENT_TEAM` moved from base → Release only (avoids Team ID leaking into the Xcode 26 debug dylib). - `scripts/codesign.sh`: resolves entitlements via $CODE_SIGN_ENTITLEMENTS env, falls back to config-specific file, then to the legacy path. Also signs `Contents/MacOS/*.debug.dylib` now that Xcode 26 emits it. - `scripts/verify-entitlements.sh`: per-config `MAIN_REQUIRED` + `MAIN_FORBIDDEN` lists — Debug forbids `speech-recognition-assets` (wrong for ad-hoc) and requires `get-task-allow`; Release enforces the shipping posture with all 3 managed keys. **Residual known issue** (documented in `.planning/debug/xctest-launch-runningboard-error-5.md` and `01-HUMAN-UAT.md` §4): Xcode 26 still emits `Jarvis.debug.dylib` with a code signature whose Team ID context diverges from our ad-hoc outer sign, producing a dyld "different Team IDs" rejection when xctest tries to load the bundle. Multiple workarounds attempted (SWIFT_ENABLE_DEBUG_DYLIB=NO, resigning the dylib, stripping it post-build). `xcodebuild test -scheme Jarvis -only-testing:JarvisAppTests` fails bootstrap; SPM package tests (56, all passing) and `xcodebuild build` both succeed. Actual code quality is unaffected — this is a harness/tooling boundary issue. Verified: 56 SPM tests pass; `xcodebuild build -configuration Debug` BUILD SUCCEEDED; `scripts/test-verify-entitlements.sh` self-test PASS=3 FAIL=0.
128 lines
6.0 KiB
Bash
Executable File
128 lines
6.0 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:-}"
|
|
FLAGS="${OTHER_CODE_SIGN_FLAGS:---options=runtime --timestamp}"
|
|
|
|
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"
|