Skip to content

Commit 44f3a72

Browse files
committed
fix(dist-apple): the bundle executes the staged program, not the tree's entry script (mcpp#634)
From the engine release that reads a Mach-O program's closure, `mcpp pack` also writes `<tree>/<target>`, a shell script that executes `bin/<target>` from the tree's root. launcher_in took that root entry first, so the bundle's executable was the script, and it executed Contents/MacOS/bin/<target>, which no bundle carries (macos-15, run 34821164486, job 103902829865: "cannot execute: No such file or directory", exit 126). The search now takes bin/<target> first; the framework rpath resolves against the program's own directory, which has to be Contents/MacOS/. check-apple-plan.sh and check-ios-plan.sh fabricate the entry script beside the program, as the engine writes it, and assert that the layout step copies bin/<target>; both fail against the previous member.
1 parent 419578a commit 44f3a72

3 files changed

Lines changed: 39 additions & 20 deletions

File tree

dist/apple.cppm

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -389,13 +389,24 @@ inline std::string app_name_for(const options& opt) {
389389
return (n && *n) ? std::string(n) : std::string("app");
390390
}
391391

392-
// The staged tree's top-level launcher -- the same search
393-
// `dist/appimage.cppm` performs, and for the same reason: `mcpp pack` writes
394-
// one per mode and names it after the binary, so this member states no
395-
// convention mcpp has not already put on disk.
392+
// The program a bundle executes, found in the staged tree: `bin/<target>`
393+
// first, then the tree's top-level entry, then `run.sh`.
394+
//
395+
// THE PROGRAM, NOT THE TREE'S ENTRY SCRIPT. From the release that reads a
396+
// Mach-O program's closure (mcpp 2026.9.14.2), the engine also writes
397+
// `<tree>/<target>`, a shell script that executes `bin/<target>` from the
398+
// tree's root. The search used to take that root entry first, which is the
399+
// order `dist/appimage.cppm` needs, and a bundle then executed the script,
400+
// which executed `Contents/MacOS/bin/<target>` -- a file no bundle carries:
401+
// "cannot execute: No such file or directory", exit 126 (macos-15, run
402+
// 34821164486). The script has no work to do inside a bundle, where
403+
// `CFBundleExecutable` names the program directly, and the framework rpath
404+
// `@executable_path/../Frameworks` resolves against the program's own
405+
// directory, which has to be `Contents/MacOS/`. The root entry and `run.sh`
406+
// remain for a tree that carries no `bin/<target>`.
396407
inline std::string launcher_in(const std::string& stage, const std::string& target) {
397-
for (auto candidate : {stage + "/" + target,
398-
stage + "/bin/" + target,
408+
for (auto candidate : {stage + "/bin/" + target,
409+
stage + "/" + target,
399410
stage + "/run.sh"})
400411
if (is_file(candidate)) return candidate;
401412
return {};
@@ -407,15 +418,9 @@ inline std::string launcher_in(const std::string& stage, const std::string& targ
407418
// within `Contents/MacOS/`, and real-world bundle tooling has hit this
408419
// directly enough to be a filed CMake defect: "CFBundleExecutable path in a
409420
// bundle should not be a relative path into bundle." So this member takes
410-
// only the basename of whatever `launcher_in` finds. That is exactly correct
411-
// when the staged tree's launcher sits at the tree's ROOT, which is the
412-
// default `--mode vendored` shape `dist/appimage.cppm`'s own header comment
413-
// describes ("a top-level launcher"). A staged tree whose launcher were
414-
// nested under `bin/` instead would still copy correctly by the layout step
415-
// below, but the resulting bundle would name an executable that is not at
416-
// the top of `Contents/MacOS/`, and this member does not flatten that case --
417-
// it is not exercised by the default staging mode, and nothing here can
418-
// exercise it on Linux to find out how macOS actually responds.
421+
// only the basename of whatever `launcher_in` finds, and the layout step
422+
// copies that file to the top of the executable directory under that name, so
423+
// the name and the file agree wherever in the staged tree it was found.
419424
inline std::string bundle_executable_name(const std::string& launcher_path) {
420425
return std::filesystem::path(launcher_path).filename().string();
421426
}
@@ -630,7 +635,7 @@ inline plan plan_for(options opt = {}) {
630635
return refuse(p, "no launcher in the staged tree", std::format(
631636
"mcpp.dist.apple: the staged tree at {0} carries no launcher for "
632637
"target '{1}'.\n"
633-
" expected one of: {0}/{1}, {0}/bin/{1}, {0}/run.sh",
638+
" expected one of: {0}/bin/{1}, {0}/{1}, {0}/run.sh",
634639
stage, target));
635640
}
636641
// CFBundleExecutable is a bare filename (see the note above). With no

tests/app-framework-consumer/check-apple-plan.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ rm -rf target
2222
BIN=target/.build-mcpp/build.mcpp.bin
2323
[ -x "$BIN" ] || fail "no compiled build.mcpp.bin at $BIN" build.log
2424

25-
# stage_tree <closure> : a staged tree with the program, its dylib and a
26-
# deployed resource, and the stage manifest the engine writes for it.
25+
# stage_tree <closure> : a staged tree with the program, its dylib, a deployed
26+
# resource and the entry script the engine writes at the tree's root, and the
27+
# stage manifest the engine writes for it.
2728
stage_tree() {
2829
local stage
2930
stage=$(mktemp -d)
3031
mkdir -p "$stage/bin/data"
3132
head -c 5000 /dev/urandom > "$stage/bin/app-framework-consumer"
33+
printf '#!/bin/sh\nhere=$(cd "$(dirname "$0")" && pwd)\nexec "$here/bin/app-framework-consumer" "$@"\n' \
34+
> "$stage/app-framework-consumer"
3235
head -c 4000 /dev/urandom > "$stage/bin/libapp-framework-dep.dylib"
3336
echo hello > "$stage/bin/data/greeting.txt"
3437
{
@@ -83,6 +86,11 @@ echo "== --format app on macOS, with a staged dylib =="
8386
stage=$(stage_tree walked)
8487
run_program app macos "" "$stage" /tmp/apple-plan-app.log
8588
check /tmp/apple-plan-app.log '
89+
layout = actions.get("mcpp.dist.apple.layout")
90+
if not layout: fail("no layout step")
91+
if not layout["command"][1].endswith("/bin/app-framework-consumer") or \
92+
not layout["command"][2].endswith("/AppFrameworkConsumer.app/Contents/MacOS/app-framework-consumer"):
93+
fail("the bundle executable is not the staged program bin/app-framework-consumer: " + repr(layout["command"]))
8694
fw = actions.get("mcpp.dist.apple.framework.libapp-framework-dep.dylib")
8795
if not fw: fail("no framework step for the staged dylib")
8896
if not fw["outputs"] or not fw["outputs"][0].endswith("/AppFrameworkConsumer.app/Contents/Frameworks/libapp-framework-dep.dylib"):
@@ -109,7 +117,7 @@ if "mcpp:link-flag=-Wl,-rpath,@executable_path/../Frameworks" not in lines:
109117
if "mcpp:runner-named=app:macapp-run" not in lines:
110118
fail("the app runner was not supplied")
111119
'
112-
echo "ok: the dylib is a framework signed ad hoc, not a resource; the bundle is signed after it; the rpath and the runner are declared"
120+
echo "ok: the program, not the tree's entry script, is the bundle executable; the dylib is a framework signed ad hoc, not a resource; the bundle is signed after it; the rpath and the runner are declared"
113121

114122
echo "== --format dmg on macOS =="
115123
run_program dmg macos "" "$stage" /tmp/apple-plan-dmg.log

tests/ios-app-consumer/check-ios-plan.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ run_row() {
102102
stage=$(mktemp -d)
103103
mkdir -p "$stage/bin"
104104
head -c 5000 /dev/urandom > "$stage/bin/ios-app-consumer"
105+
# The entry script the engine writes at the tree's root beside a Mach-O
106+
# program (mcpp 2026.9.14.2+), which a bundle must not execute.
107+
printf '#!/bin/sh\nhere=$(cd "$(dirname "$0")" && pwd)\nexec "$here/bin/ios-app-consumer" "$@"\n' \
108+
> "$stage/ios-app-consumer"
105109
fi
106110
out=$(mktemp -d)
107111
env -i PATH="$PATH" \
@@ -147,9 +151,11 @@ grep -q 'AppIcon76x76@2x~ipad</string>' "$plist" || fail "the icon list is missi
147151
# bytes in it to stay quiet.
148152
grep 'mcpp.dist.apple.layout' "$log" | grep -q '\${mcpp\.stage_dir}' \
149153
|| fail "the layout step did not name \${mcpp.stage_dir} for a non-empty staged tree" "$log"
154+
grep 'mcpp.dist.apple.layout' "$log" | grep -q '"command":\["ditto","[^"]*/bin/ios-app-consumer",' \
155+
|| fail "the bundle executable is not the staged program bin/ios-app-consumer" "$log"
150156
grep -q 'holds only' "$log" \
151157
&& fail "a non-empty staged tree still produced the 0-byte staged-tree warning" "$log"
152-
echo "ok: flat layout, no codesign, a named warning, and every iOS-only plist key"
158+
echo "ok: flat layout, the staged program as the executable, no codesign, a named warning, and every iOS-only plist key"
153159
check_terminal_bundle "$log"
154160

155161
echo "== iOS Simulator row, an empty pack_stage_dir (#622 B2 defect 1) =="

0 commit comments

Comments
 (0)