|
| 1 | +#!/usr/bin/env bash |
| 2 | +# dist-apple on macOS, measured on the bundle and the image (mcpp#634, B1 to B3). |
| 3 | +# |
| 4 | +# The plan-level half is `check-apple-plan.sh`; this half needs the tools only |
| 5 | +# a Mac has (`otool`, `codesign`, `hdiutil`) and runs the program. |
| 6 | +# |
| 7 | +# 1. The program is linked with the rpath `@executable_path/../Frameworks`, |
| 8 | +# as written: the engine anchors no rpath that begins with a loader token |
| 9 | +# (mcpp 2026.9.14.2+), and the member adds it through `mcpp::link_flag`. |
| 10 | +# 2. The bundle carries the dependency's dylib in `Contents/Frameworks/` and |
| 11 | +# not among the resources, keeps the deployed file under |
| 12 | +# `Contents/Resources/`, and is signed ad hoc so that `codesign --verify |
| 13 | +# --deep --strict` accepts it. |
| 14 | +# 3. The bundled program loads the framework copy and exits 7 with its |
| 15 | +# output; a copy of the bundle without the framework stops with "Library |
| 16 | +# not loaded". |
| 17 | +# 4. `mcpp run --format app`, with no runner in the manifest, runs the bundle |
| 18 | +# through `macapp-run` and returns the program's 7 with its output and |
| 19 | +# arguments; `--runner app` does the same. |
| 20 | +# 5. `mcpp pack --format dmg` writes an image `hdiutil verify` accepts, which |
| 21 | +# attaches read-only holding the bundle and an `Applications` link. |
| 22 | +# |
| 23 | +# Usage: MCPP=<mcpp 2026.9.14.2+> ./check-apple-bundle.sh (run from this directory) |
| 24 | +set -eu |
| 25 | + |
| 26 | +MCPP="${MCPP:-mcpp}" |
| 27 | +NAME=AppFrameworkConsumer |
| 28 | +EXE=app-framework-consumer |
| 29 | +DYLIB=libapp-framework-dep.dylib |
| 30 | +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } |
| 31 | +reading() { printf 'READING %s: %s\n' "$1" "$2"; } |
| 32 | +packed() { sed -n 's/^ *Packed //p' "$1" | tail -1; } |
| 33 | +work=$(mktemp -d) |
| 34 | + |
| 35 | +# ── 1. the rpath, at link time ───────────────────────────────────────────── |
| 36 | +echo "== 1. the program's rpath ==" |
| 37 | +rm -rf target |
| 38 | +"$MCPP" build > bundle-build.log 2>&1 || fail "mcpp build failed" bundle-build.log |
| 39 | +if find target -name '*.app' | grep -q .; then fail "a plain build produced a bundle"; fi |
| 40 | +programs=$(find target -type f -name "$EXE" ! -path '*/.build-mcpp/*') |
| 41 | +[ "$(printf '%s\n' "$programs" | grep -c .)" = 1 ] \ |
| 42 | + || fail "expected one linked $EXE under target/, found: $(echo $programs)" bundle-build.log |
| 43 | +otool -l "$programs" > bundle-loadcommands.log |
| 44 | +rpaths=$(awk '/cmd LC_RPATH/ { getline; getline; print $2 }' bundle-loadcommands.log) |
| 45 | +reading rpaths "$(echo $rpaths)" |
| 46 | +printf '%s\n' "$rpaths" | grep -qxF '@executable_path/../Frameworks' \ |
| 47 | + || fail "the program has no LC_RPATH @executable_path/../Frameworks" bundle-loadcommands.log |
| 48 | +if printf '%s\n' "$rpaths" | grep -q '.@executable_path'; then |
| 49 | + fail "an rpath carries @executable_path after a directory, anchored" bundle-loadcommands.log |
| 50 | +fi |
| 51 | +otool -L "$programs" > bundle-needed.log |
| 52 | +grep -q "@rpath/$DYLIB" bundle-needed.log || fail "the program does not load @rpath/$DYLIB" bundle-needed.log |
| 53 | +echo "ok: LC_RPATH @executable_path/../Frameworks, as written, and the dependency is @rpath/$DYLIB" |
| 54 | + |
| 55 | +# ── 2. the bundle ────────────────────────────────────────────────────────── |
| 56 | +echo "== 2. the bundle carries the framework and is signed ad hoc ==" |
| 57 | +"$MCPP" pack --format app > bundle-pack.log 2>&1 || fail "mcpp pack --format app failed" bundle-pack.log |
| 58 | +app=$(packed bundle-pack.log) |
| 59 | +case "$app" in *"/$NAME.app") ;; *) fail "the pack reported '$app', not $NAME.app" bundle-pack.log ;; esac |
| 60 | +[ -d "$app" ] || fail "the reported bundle $app is not a directory" bundle-pack.log |
| 61 | +[ -f "$app/Contents/Frameworks/$DYLIB" ] || { find "$app" | sort; fail "no Contents/Frameworks/$DYLIB"; } |
| 62 | +if find "$app/Contents/Resources" "$app/Contents/MacOS" -name '*.dylib' 2>/dev/null | grep -q .; then |
| 63 | + find "$app" | sort; fail "a dylib is under Contents/Resources or Contents/MacOS" |
| 64 | +fi |
| 65 | +[ -f "$app/Contents/Resources/data/greeting.txt" ] || { find "$app" | sort; fail "the deployed file is not under Contents/Resources"; } |
| 66 | +codesign --verify --deep --strict --verbose=2 "$app" > bundle-codesign.log 2>&1 \ |
| 67 | + || fail "codesign --verify --deep --strict refused the bundle" bundle-codesign.log |
| 68 | +reading codesign-verify "$(tr '\n' ' ' < bundle-codesign.log)" |
| 69 | +# A linked arm64 program is already signed ad hoc by the linker, so the bundle's |
| 70 | +# own signature is read from its sealed resources, and the framework's from |
| 71 | +# the absence of the linker's flag. |
| 72 | +codesign -dv "$app" > bundle-signature.log 2>&1 || true |
| 73 | +reading bundle-signature "$(grep -E '^(Signature|CodeDirectory|Sealed Resources)' bundle-signature.log | tr '\n' ' ')" |
| 74 | +grep -q '^Signature=adhoc' bundle-signature.log || fail "the bundle's signature is not ad hoc" bundle-signature.log |
| 75 | +grep -q '^Sealed Resources' bundle-signature.log || fail "the bundle's resources are not sealed" bundle-signature.log |
| 76 | +codesign -dv "$app/Contents/Frameworks/$DYLIB" > bundle-framework-signature.log 2>&1 || true |
| 77 | +reading framework-signature "$(grep -E '^(Signature|CodeDirectory)' bundle-framework-signature.log | tr '\n' ' ')" |
| 78 | +grep -q '^Signature=adhoc' bundle-framework-signature.log \ |
| 79 | + || fail "the framework is not signed" bundle-framework-signature.log |
| 80 | +if grep -q 'linker-signed' bundle-framework-signature.log; then |
| 81 | + fail "the framework carries the linker's signature, not one of its own" bundle-framework-signature.log |
| 82 | +fi |
| 83 | +echo "ok: Contents/Frameworks/$DYLIB, nothing loadable among the resources, and codesign --verify --deep --strict passes" |
| 84 | + |
| 85 | +# ── 3. the bundled program ───────────────────────────────────────────────── |
| 86 | +echo "== 3. the bundled program loads the framework ==" |
| 87 | +rc=0 |
| 88 | +DYLD_PRINT_LIBRARIES=1 "$app/Contents/MacOS/$EXE" > bundle-run.out 2> bundle-run.err || rc=$? |
| 89 | +reading bundle-run "exit=$rc $(cat bundle-run.out)" |
| 90 | +[ "$rc" -eq 7 ] || fail "the bundled program exited $rc, not 7" bundle-run.out bundle-run.err |
| 91 | +grep -qx 'framework-1-2-3 argc=1' bundle-run.out || fail "the bundled program's output is missing" bundle-run.out |
| 92 | +loaded=$(grep "$DYLIB" bundle-run.err | head -1) |
| 93 | +reading loaded "$loaded" |
| 94 | +case "$loaded" in *"/$NAME.app/Contents/Frameworks/$DYLIB") ;; |
| 95 | + *) fail "the program did not load the framework copy" bundle-run.err ;; esac |
| 96 | +cp -R "$app" "$work/" |
| 97 | +rm "$work/$NAME.app/Contents/Frameworks/$DYLIB" |
| 98 | +rc=0 |
| 99 | +"$work/$NAME.app/Contents/MacOS/$EXE" > bundle-noframework.out 2>&1 || rc=$? |
| 100 | +reading without-framework "exit=$rc $(head -2 bundle-noframework.out | tr '\n' ' ')" |
| 101 | +[ "$rc" -ne 0 ] || fail "the program ran without its framework" bundle-noframework.out |
| 102 | +grep -q 'Library not loaded' bundle-noframework.out || fail "the failure is not 'Library not loaded'" bundle-noframework.out |
| 103 | +echo "ok: exit 7 through the framework copy; without it, exit $rc and 'Library not loaded'" |
| 104 | + |
| 105 | +# ── 4. mcpp run --format app ─────────────────────────────────────────────── |
| 106 | +echo "== 4. mcpp run --format app, through the runner dist-apple supplies ==" |
| 107 | +if grep -q 'runner' mcpp.toml; then fail "the fixture's manifest names a runner" mcpp.toml; fi |
| 108 | +# mcpp_run <variant> <mcpp run arguments...> |
| 109 | +mcpp_run() { |
| 110 | + local variant="$1"; shift |
| 111 | + rc=0 |
| 112 | + "$MCPP" run "$@" > "bundle-mcpp-run-$variant.log" 2>&1 || rc=$? |
| 113 | + reading "mcpp-run-$variant" "exit=$rc $(grep -m1 'Running' "bundle-mcpp-run-$variant.log" || echo 'no Running line')" |
| 114 | + [ "$rc" -eq 7 ] || fail "mcpp run $* exited $rc, not 7" "bundle-mcpp-run-$variant.log" |
| 115 | + grep -q 'Running `.*macapp-run' "bundle-mcpp-run-$variant.log" \ |
| 116 | + || fail "the status line does not name macapp-run" "bundle-mcpp-run-$variant.log" |
| 117 | + grep -qx 'framework-1-2-3 argc=2' "bundle-mcpp-run-$variant.log" \ |
| 118 | + || fail "the program's output with one argument is missing" "bundle-mcpp-run-$variant.log" |
| 119 | +} |
| 120 | +mcpp_run format --format app -- extra |
| 121 | +mcpp_run runner --format app --runner app -- extra |
| 122 | +echo "ok: mcpp run --format app, and with --runner app, return 7 with the program's output and argument" |
| 123 | + |
| 124 | +# ── 5. the disk image ────────────────────────────────────────────────────── |
| 125 | +echo "== 5. mcpp pack --format dmg ==" |
| 126 | +"$MCPP" pack --format dmg > bundle-dmg.log 2>&1 || fail "mcpp pack --format dmg failed" bundle-dmg.log |
| 127 | +dmg=$(packed bundle-dmg.log) |
| 128 | +case "$dmg" in *"/$NAME.dmg") ;; *) fail "the pack reported '$dmg', not $NAME.dmg" bundle-dmg.log ;; esac |
| 129 | +[ -f "$dmg" ] || fail "the reported image $dmg does not exist" bundle-dmg.log |
| 130 | +hdiutil verify "$dmg" > bundle-hdiutil-verify.log 2>&1 || fail "hdiutil verify refused the image" bundle-hdiutil-verify.log |
| 131 | +reading hdiutil-verify "$(grep -i 'checksum' bundle-hdiutil-verify.log | tail -1)" |
| 132 | +mnt="$work/mnt" |
| 133 | +mkdir -p "$mnt" |
| 134 | +hdiutil attach -nobrowse -readonly -mountpoint "$mnt" "$dmg" > bundle-attach.log 2>&1 \ |
| 135 | + || fail "hdiutil attach failed" bundle-attach.log |
| 136 | +detach() { hdiutil detach "$mnt" > /dev/null 2>&1 || hdiutil detach -force "$mnt" > /dev/null 2>&1 || true; } |
| 137 | +trap detach EXIT |
| 138 | +reading image-root "$(ls -1 "$mnt" | tr '\n' ' ')" |
| 139 | +[ -x "$mnt/$NAME.app/Contents/MacOS/$EXE" ] || fail "the image does not hold the bundle's program" |
| 140 | +[ -f "$mnt/$NAME.app/Contents/Frameworks/$DYLIB" ] || fail "the image's bundle lacks the framework" |
| 141 | +[ -L "$mnt/Applications" ] || fail "the image holds no Applications link" |
| 142 | +[ "$(readlink "$mnt/Applications")" = /Applications ] || fail "the Applications link points to $(readlink "$mnt/Applications")" |
| 143 | +codesign --verify --deep --strict "$mnt/$NAME.app" > bundle-image-codesign.log 2>&1 \ |
| 144 | + || fail "the bundle inside the image does not verify" bundle-image-codesign.log |
| 145 | +detach |
| 146 | +trap - EXIT |
| 147 | +echo "ok: the image verifies and attaches read-only with the signed bundle and Applications -> /Applications" |
| 148 | + |
| 149 | +echo "PASS: dist-apple's framework, rpath, ad-hoc signature, app runner and disk image, on the bundle" |
0 commit comments