diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7dc9c687745..8014f10eea7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,6 @@ on: permissions: contents: read - actions: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -52,6 +51,9 @@ jobs: - name: Validate release asset guard run: node scripts/release_asset_guard.test.js + - name: Validate universal macOS app verifier + run: ./tests/test_verify_universal_macos_app.sh + - name: Validate current GhosttyKit checksum pin run: ./tests/test_ci_ghosttykit_checksum_present.sh @@ -689,6 +691,9 @@ jobs: release-ghostty-cli-helper: runs-on: ${{ vars.MACOS_RUNNER_15 || 'warp-macos-15-arm64-6x' }} timeout-minutes: 20 + permissions: + contents: read + actions: write steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -728,6 +733,9 @@ jobs: # compiles into the same artifact shape as nightly and stable releases. runs-on: ${{ vars.MACOS_RUNNER_26 || 'warp-macos-26-arm64-6x' }} timeout-minutes: 20 + permissions: + contents: read + actions: read steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -817,22 +825,12 @@ jobs: ghostty-cli-helper/ghostty \ build-universal/Build/Products/Release/cmux.app - - name: Validate Release artifact slices + - name: Validate Release artifact contract run: | - set -euo pipefail - APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux" - CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" - HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty" - test -x "$APP_BINARY" - test -x "$CLI_BINARY" - test -x "$HELPER_BINARY" - file "$APP_BINARY" "$CLI_BINARY" "$HELPER_BINARY" - SDK_VERSION="$(otool -l "$APP_BINARY" | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }')" - echo "App SDK version: $SDK_VERSION" - lipo "$APP_BINARY" -verify_arch arm64 x86_64 - lipo "$CLI_BINARY" -verify_arch arm64 x86_64 - lipo "$HELPER_BINARY" -verify_arch arm64 x86_64 - [[ "$SDK_VERSION" == 26.* ]] + ./scripts/verify-universal-macos-app.sh \ + "build-universal/Build/Products/Release/cmux.app" \ + --label "CI Release app" \ + --require-sdk-prefix "26." ui-regressions: runs-on: ${{ vars.MACOS_RUNNER_15 || 'warp-macos-15-arm64-6x' }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8a60c85d3456..acb2d21128e4 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -284,19 +284,10 @@ jobs: - name: Verify nightly binary architectures if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | - set -euo pipefail - APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux" - CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" - HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty" - APP_ARCHS="$(lipo -archs "$APP_BINARY")" - CLI_ARCHS="$(lipo -archs "$CLI_BINARY")" - HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")" - echo "App binary architectures: $APP_ARCHS" - echo "CLI binary architectures: $CLI_ARCHS" - echo "Ghostty helper architectures: $HELPER_ARCHS" - [[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] - [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] - [[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]] + ./scripts/verify-universal-macos-app.sh \ + "build-universal/Build/Products/Release/cmux.app" \ + --label "Nightly app" \ + --require-sdk-prefix "26." - name: Run CLI version memory guard regression if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bac1fd35f30b..98c0177e63b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -229,22 +229,10 @@ jobs: - name: Verify binary architectures if: steps.guard_release_assets.outputs.skip_all != 'true' run: | - set -euo pipefail - APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux" - CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" - HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty" - APP_ARCHS="$(lipo -archs "$APP_BINARY")" - CLI_ARCHS="$(lipo -archs "$CLI_BINARY")" - HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")" - SDK_VERSION="$(otool -l "$APP_BINARY" | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }')" - echo "App binary architectures: $APP_ARCHS" - echo "CLI binary architectures: $CLI_ARCHS" - echo "Ghostty helper architectures: $HELPER_ARCHS" - echo "App SDK version: $SDK_VERSION" - [[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] - [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] - [[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]] - [[ "$SDK_VERSION" == 26.* ]] + ./scripts/verify-universal-macos-app.sh \ + "build-universal/Build/Products/Release/cmux.app" \ + --label "Release app" \ + --require-sdk-prefix "26." - name: Build remote daemon release assets and inject manifest if: steps.guard_release_assets.outputs.skip_all != 'true' diff --git a/scripts/build-sign-upload.sh b/scripts/build-sign-upload.sh index 3f88e7cbbd10..443ccd3311fa 100755 --- a/scripts/build-sign-upload.sh +++ b/scripts/build-sign-upload.sh @@ -71,7 +71,11 @@ cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework # --- Build app (Release, unsigned) --- echo "Building app..." rm -rf build/ -xcodebuild -scheme cmux -configuration Release -derivedDataPath build CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -5 +xcodebuild -project cmux.xcodeproj -scheme cmux -configuration Release -derivedDataPath build \ + -destination 'generic/platform=macOS' \ + ARCHS="arm64 x86_64" \ + ONLY_ACTIVE_ARCH=NO \ + CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -5 echo "Build succeeded" HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty" @@ -79,6 +83,10 @@ if [ ! -x "$HELPER_PATH" ]; then echo "Ghostty theme picker helper not found at $HELPER_PATH" >&2 exit 1 fi +./scripts/verify-universal-macos-app.sh \ + "$APP_PATH" \ + --label "Release app" \ + --require-sdk-prefix "26." # --- Inject Sparkle keys --- echo "Injecting Sparkle keys..." diff --git a/scripts/verify-universal-macos-app.sh b/scripts/verify-universal-macos-app.sh new file mode 100755 index 000000000000..53e1a57db2b2 --- /dev/null +++ b/scripts/verify-universal-macos-app.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: ./scripts/verify-universal-macos-app.sh [--label ] [--require-sdk-prefix ] + +Verifies that the app executable, bundled cmux CLI, and bundled Ghostty helper +all contain both arm64 and x86_64 Mach-O slices. + +When --require-sdk-prefix is provided, also verifies that the app executable's +LC_BUILD_VERSION SDK starts with that prefix, e.g. "26.". +EOF +} + +APP_PATH="" +LABEL="macOS app" +SDK_PREFIX="" +while [[ $# -gt 0 ]]; do + case "$1" in + --label) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "Missing value for --label" >&2 + exit 1 + fi + LABEL="$2" + shift 2 + ;; + --require-sdk-prefix) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "Missing value for --require-sdk-prefix" >&2 + exit 1 + fi + SDK_PREFIX="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + -*) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + *) + if [[ -n "$APP_PATH" ]]; then + echo "Unexpected argument: $1" >&2 + usage >&2 + exit 1 + fi + APP_PATH="$1" + shift + ;; + esac +done + +if [[ -z "$APP_PATH" ]]; then + usage >&2 + exit 1 +fi + +if [[ -z "$LABEL" ]]; then + echo "Missing value for --label" >&2 + exit 1 +fi + +if [[ ! -d "$APP_PATH" ]]; then + echo "error: app bundle not found at $APP_PATH" >&2 + exit 1 +fi + +LIPO_BIN="${CMUX_LIPO:-lipo}" +if ! command -v "$LIPO_BIN" >/dev/null 2>&1; then + echo "error: lipo is required to verify universal macOS binaries" >&2 + exit 1 +fi + +INFO_PLIST="$APP_PATH/Contents/Info.plist" +EXECUTABLE_NAME="" +if [[ -f "$INFO_PLIST" && -x /usr/libexec/PlistBuddy ]]; then + EXECUTABLE_NAME="$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$INFO_PLIST" 2>/dev/null || true)" +fi +if [[ -z "$EXECUTABLE_NAME" ]]; then + EXECUTABLE_NAME="$(basename "$APP_PATH" .app)" +fi + +APP_BINARY="$APP_PATH/Contents/MacOS/$EXECUTABLE_NAME" +CLI_BINARY="$APP_PATH/Contents/Resources/bin/cmux" +HELPER_BINARY="$APP_PATH/Contents/Resources/bin/ghostty" + +verify_binary_archs() { + local name="$1" + local path="$2" + local archs + + if [[ ! -x "$path" ]]; then + echo "error: $name is missing or not executable at $path" >&2 + exit 1 + fi + + if ! archs="$("$LIPO_BIN" -archs "$path")"; then + echo "error: failed to inspect $name architectures at $path" >&2 + exit 1 + fi + + echo "$LABEL $name architectures: $archs" + for expected_arch in arm64 x86_64; do + case " $archs " in + *" $expected_arch "*) + ;; + *) + echo "error: $name at $path is missing $expected_arch slice" >&2 + exit 1 + ;; + esac + done +} + +verify_binary_archs "app binary" "$APP_BINARY" +verify_binary_archs "CLI binary" "$CLI_BINARY" +verify_binary_archs "Ghostty helper" "$HELPER_BINARY" + +if [[ -n "$SDK_PREFIX" ]]; then + OTOOL_BIN="${CMUX_OTOOL:-otool}" + if ! command -v "$OTOOL_BIN" >/dev/null 2>&1; then + echo "error: otool is required to verify the macOS SDK version" >&2 + exit 1 + fi + + if ! SDK_VERSION="$( + "$OTOOL_BIN" -l "$APP_BINARY" \ + | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }' + )"; then + echo "error: failed to inspect app SDK version at $APP_BINARY" >&2 + exit 1 + fi + if [[ -z "$SDK_VERSION" ]]; then + echo "error: failed to inspect app SDK version at $APP_BINARY" >&2 + exit 1 + fi + + echo "$LABEL app SDK version: $SDK_VERSION" + case "$SDK_VERSION" in + "$SDK_PREFIX"*) + ;; + *) + echo "error: app binary at $APP_BINARY was built with SDK $SDK_VERSION, expected prefix $SDK_PREFIX" >&2 + exit 1 + ;; + esac +fi diff --git a/tests/test_ci_release_sdk_lane.sh b/tests/test_ci_release_sdk_lane.sh index 8f4925e8663f..0bbd2e98e037 100755 --- a/tests/test_ci_release_sdk_lane.sh +++ b/tests/test_ci_release_sdk_lane.sh @@ -4,11 +4,11 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" CI_FILE="$ROOT_DIR/.github/workflows/ci.yml" RELEASE_FILE="$ROOT_DIR/.github/workflows/release.yml" +BUILD_SIGN_UPLOAD_FILE="$ROOT_DIR/scripts/build-sign-upload.sh" -# nightly.yml is intentionally not covered here. It builds the macOS 26 SDK app -# with its own inline helper-build model (PR #5077) and self-guards via its -# "Select Xcode" loud-fail and "Verify nightly binary architectures" steps. -# This lane guards the release/CI artifact-download model added by this change. +# nightly.yml builds the macOS 26 SDK app with its own inline helper-build model +# (PR #5077). This lane guards the release/CI artifact-download model added by +# this change; nightly's verifier call is covered by the universal verifier test. job_section() { local file="$1" job="$2" @@ -69,10 +69,15 @@ for workflow in "$CI_FILE" "$RELEASE_FILE"; do exit 1 fi - if ! grep -Fq '[[ "$SDK_VERSION" == 26.* ]]' "$workflow"; then - echo "FAIL: $(basename "$workflow") must verify the app binary was built with a macOS 26 SDK" >&2 + if ! grep -Fq -- '--require-sdk-prefix "26."' "$workflow"; then + echo "FAIL: $(basename "$workflow") must verify the app binary was built with a macOS 26 SDK through the universal app verifier" >&2 exit 1 fi done -echo "PASS: release and CI app builds use macOS 26 SDK with a macOS 15-built Ghostty CLI helper" +if ! grep -Fq -- '--require-sdk-prefix "26."' "$BUILD_SIGN_UPLOAD_FILE"; then + echo "FAIL: build-sign-upload.sh must verify the app binary was built with a macOS 26 SDK through the universal app verifier" >&2 + exit 1 +fi + +echo "PASS: release, CI, and manual app builds use macOS 26 SDK with a macOS 15-built Ghostty CLI helper" diff --git a/tests/test_ci_self_hosted_guard.sh b/tests/test_ci_self_hosted_guard.sh index 7685a488617e..637989d3ecad 100755 --- a/tests/test_ci_self_hosted_guard.sh +++ b/tests/test_ci_self_hosted_guard.sh @@ -13,6 +13,16 @@ CI_FILE="$ROOT_DIR/.github/workflows/ci.yml" GHOSTTYKIT_FILE="$ROOT_DIR/.github/workflows/build-ghosttykit.yml" COMPAT_FILE="$ROOT_DIR/.github/workflows/ci-macos-compat.yml" E2E_FILE="$ROOT_DIR/.github/workflows/test-e2e.yml" +UNIVERSAL_VERIFIER="$ROOT_DIR/scripts/verify-universal-macos-app.sh" + +job_section() { + local file="$1" job="$2" + awk -v job="$job" ' + $0 ~ "^ "job":" { in_job=1; next } + in_job && /^ [^[:space:]#][^:]*:[[:space:]]*(#.*)?$/ { exit } + in_job { print } + ' "$file" +} check_macos_runner() { local file="$1" job="$2" @@ -105,18 +115,26 @@ check_xcode_selection() { } check_release_build_signal() { - if ! grep -Fq 'lipo "$APP_BINARY" -verify_arch arm64 x86_64' "$CI_FILE"; then - echo "FAIL: release-build must verify the Release app binary stays universal" + local section + section="$(job_section "$CI_FILE" "release-build")" + + if ! grep -Eq '^[[:space:]]*\./scripts/verify-universal-macos-app\.sh([[:space:]\\]|$)' <<< "$section"; then + echo "FAIL: release-build must verify the Release artifact through the universal app verifier" + exit 1 + fi + + if ! grep -Fq 'verify_binary_archs "app binary" "$APP_BINARY"' "$UNIVERSAL_VERIFIER"; then + echo "FAIL: universal app verifier must check the Release app binary" exit 1 fi - if ! grep -Fq 'lipo "$CLI_BINARY" -verify_arch arm64 x86_64' "$CI_FILE"; then - echo "FAIL: release-build must verify the bundled CLI stays universal" + if ! grep -Fq 'verify_binary_archs "CLI binary" "$CLI_BINARY"' "$UNIVERSAL_VERIFIER"; then + echo "FAIL: universal app verifier must check the bundled CLI" exit 1 fi - if ! grep -Fq 'lipo "$HELPER_BINARY" -verify_arch arm64 x86_64' "$CI_FILE"; then - echo "FAIL: release-build must verify the bundled Ghostty helper stays universal" + if ! grep -Fq 'verify_binary_archs "Ghostty helper" "$HELPER_BINARY"' "$UNIVERSAL_VERIFIER"; then + echo "FAIL: universal app verifier must check the bundled Ghostty helper" exit 1 fi diff --git a/tests/test_verify_universal_macos_app.sh b/tests/test_verify_universal_macos_app.sh new file mode 100755 index 000000000000..fe3544f95ecc --- /dev/null +++ b/tests/test_verify_universal_macos_app.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cmux-universal-verify.XXXXXX")" +trap 'rm -rf "$TMP_DIR"' EXIT + +APP_PATH="$TMP_DIR/cmux.app" +FAKE_LIPO="$TMP_DIR/lipo" +FAKE_OTOOL="$TMP_DIR/otool" + +mkdir -p "$APP_PATH/Contents/MacOS" "$APP_PATH/Contents/Resources/bin" +cat > "$APP_PATH/Contents/Info.plist" <<'EOF' + + + + + CFBundleExecutable + cmux + + +EOF + +touch "$APP_PATH/Contents/MacOS/cmux" +touch "$APP_PATH/Contents/Resources/bin/cmux" +touch "$APP_PATH/Contents/Resources/bin/ghostty" +chmod 755 \ + "$APP_PATH/Contents/MacOS/cmux" \ + "$APP_PATH/Contents/Resources/bin/cmux" \ + "$APP_PATH/Contents/Resources/bin/ghostty" + +cat > "$FAKE_LIPO" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" != "-archs" || $# -ne 2 ]]; then + echo "unexpected lipo invocation" >&2 + exit 2 +fi +cat "$2.archs" +EOF +chmod +x "$FAKE_LIPO" + +cat > "$FAKE_OTOOL" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" != "-l" || $# -ne 2 ]]; then + echo "unexpected otool invocation" >&2 + exit 2 +fi +SDK_FILE="$2.sdk" +if [[ ! -f "$SDK_FILE" ]]; then + exit 0 +fi +cat < "$1.archs" +} + +set_sdk() { + printf '%s\n' "$2" > "$1.sdk" +} + +VERIFY_SCRIPT="$ROOT_DIR/scripts/verify-universal-macos-app.sh" +export CMUX_LIPO="$FAKE_LIPO" +export CMUX_OTOOL="$FAKE_OTOOL" + +set_archs "$APP_PATH/Contents/MacOS/cmux" "x86_64 arm64" +set_archs "$APP_PATH/Contents/Resources/bin/cmux" "arm64 x86_64" +set_archs "$APP_PATH/Contents/Resources/bin/ghostty" "arm64 x86_64" +set_sdk "$APP_PATH/Contents/MacOS/cmux" "26.1" +"$VERIFY_SCRIPT" "$APP_PATH" --label "fixture app" >/dev/null +"$VERIFY_SCRIPT" "$APP_PATH" --label "fixture app" --require-sdk-prefix "26." >/dev/null + +if "$VERIFY_SCRIPT" "$APP_PATH" --label >"$TMP_DIR/missing-label.out" 2>"$TMP_DIR/missing-label.err"; then + echo "FAIL: verifier accepted --label without a value" >&2 + exit 1 +fi +if ! grep -Fq "Missing value for --label" "$TMP_DIR/missing-label.err"; then + echo "FAIL: verifier did not explain the missing label value" >&2 + cat "$TMP_DIR/missing-label.err" >&2 + exit 1 +fi + +if "$VERIFY_SCRIPT" "$APP_PATH" --require-sdk-prefix >"$TMP_DIR/missing-sdk-prefix.out" 2>"$TMP_DIR/missing-sdk-prefix.err"; then + echo "FAIL: verifier accepted --require-sdk-prefix without a value" >&2 + exit 1 +fi +if ! grep -Fq "Missing value for --require-sdk-prefix" "$TMP_DIR/missing-sdk-prefix.err"; then + echo "FAIL: verifier did not explain the missing SDK prefix value" >&2 + cat "$TMP_DIR/missing-sdk-prefix.err" >&2 + exit 1 +fi + +set_sdk "$APP_PATH/Contents/MacOS/cmux" "15.5" +if "$VERIFY_SCRIPT" "$APP_PATH" --label "fixture app" --require-sdk-prefix "26." >"$TMP_DIR/sdk-prefix.out" 2>"$TMP_DIR/sdk-prefix.err"; then + echo "FAIL: verifier accepted an app built with the wrong SDK" >&2 + exit 1 +fi +if ! grep -Fq "expected prefix 26." "$TMP_DIR/sdk-prefix.err"; then + echo "FAIL: verifier did not explain the wrong SDK prefix" >&2 + cat "$TMP_DIR/sdk-prefix.err" >&2 + exit 1 +fi +set_sdk "$APP_PATH/Contents/MacOS/cmux" "26.1" + +set_archs "$APP_PATH/Contents/Resources/bin/ghostty" "x86_64" +if "$VERIFY_SCRIPT" "$APP_PATH" --label "fixture app" >"$TMP_DIR/missing-arm.out" 2>"$TMP_DIR/missing-arm.err"; then + echo "FAIL: verifier accepted a helper missing the arm64 slice" >&2 + exit 1 +fi +if ! grep -Fq "missing arm64 slice" "$TMP_DIR/missing-arm.err"; then + echo "FAIL: verifier did not explain the missing arm64 slice" >&2 + cat "$TMP_DIR/missing-arm.err" >&2 + exit 1 +fi + +set_archs "$APP_PATH/Contents/Resources/bin/ghostty" "arm64" +if "$VERIFY_SCRIPT" "$APP_PATH" --label "fixture app" >"$TMP_DIR/missing-slice.out" 2>"$TMP_DIR/missing-slice.err"; then + echo "FAIL: verifier accepted a helper missing the x86_64 slice" >&2 + exit 1 +fi +if ! grep -Fq "missing x86_64 slice" "$TMP_DIR/missing-slice.err"; then + echo "FAIL: verifier did not explain the missing x86_64 slice" >&2 + cat "$TMP_DIR/missing-slice.err" >&2 + exit 1 +fi + +rm "$APP_PATH/Contents/Resources/bin/cmux" +if "$VERIFY_SCRIPT" "$APP_PATH" --label "fixture app" >"$TMP_DIR/missing-cli.out" 2>"$TMP_DIR/missing-cli.err"; then + echo "FAIL: verifier accepted an app bundle missing the embedded CLI" >&2 + exit 1 +fi +if ! grep -Fq "CLI binary is missing or not executable" "$TMP_DIR/missing-cli.err"; then + echo "FAIL: verifier did not explain the missing CLI" >&2 + cat "$TMP_DIR/missing-cli.err" >&2 + exit 1 +fi + +echo "PASS: universal macOS app verifier enforces app, CLI, and helper slices"