Skip to content

Commit 083f23c

Browse files
committed
fix(dist-apple): the staged tree is optional, because on macOS it cannot exist
2026.9.11.2 made the dispatch reachable and the member then refused one layer up, for the same underlying reason: error: no action claimed --format 'app' That message is the ENGINE's. The member's own explanation -- "mcpp reported no staged tree" -- went to stderr, which is discarded on a successful build, so the refusal was unreadable. And the refusal was wrong. `mcpp pack`'s built-in closure walk refuses a Mach-O program because it uses `LD_TRACE_LOADED_OBJECTS`, which dyld answers by RUNNING the program -- so on macOS there is no staged tree to have, and a member that requires one can never run there. A `.app` needs ONE program, not a tree: `${mcpp.target_file:<name>}` names it, which is the placeholder dist/wix.cppm uses for exactly this reason and what section 6 of the design record recommends for a member packaging a named target. The tree is still preferred when it exists -- a `--mode vendored` tree carries the program's dependencies beside it and a bundle should keep them. Without one the bundle carries the program alone, which is correct for a self-contained Mach-O and is what the platform's own default produces. `${mcpp.stage_dir}` is now named only on the path that has a tree, because the engine REFUSES that placeholder when there is none -- that is its contract, and a member naming it unconditionally is a member that cannot run on a target whose built-in staging is refused. `CFBundleExecutable` takes the target's own name on that path, since a placeholder's basename is not knowable before the engine expands it. All eleven features still compile against released 2026.9.11.2.
1 parent 006124b commit 083f23c

1 file changed

Lines changed: 45 additions & 9 deletions

File tree

dist/apple.cppm

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -374,13 +374,29 @@ inline plan plan_for(options opt = {}) {
374374
return p;
375375
}
376376

377+
// THE STAGED TREE IS OPTIONAL, AND THAT IS THE WHOLE FINDING.
378+
//
379+
// This required it, and on macOS it cannot exist: `mcpp pack`'s built-in
380+
// closure walk refuses a Mach-O program, because it uses
381+
// `LD_TRACE_LOADED_OBJECTS` and dyld answers that by RUNNING the program.
382+
// mcpp 2026.9.11.2 made staging a service rather than a precondition, so
383+
// the dispatch now reaches this member -- and the member then refused for
384+
// the same underlying reason, one layer up, with
385+
//
386+
// error: no action claimed --format 'app'
387+
//
388+
// from the engine, because a member's stderr on a successful build is
389+
// discarded. A refusal nobody can read.
390+
//
391+
// A `.app` needs ONE program, not a tree. `${mcpp.target_file:<name>}` is
392+
// what names it -- the same placeholder `dist/wix.cppm` uses for exactly
393+
// this reason, and what section 6 of the design record recommends for a
394+
// member that packages a named target. The staged tree is still preferred
395+
// when it exists, because a `--mode vendored` tree carries the program's
396+
// dependencies beside it and a bundle should keep them; without one the
397+
// bundle carries the program alone, which is correct for a self-contained
398+
// Mach-O and is what the platform's own default produces.
377399
const std::string stage = mcpp::pack_stage_dir();
378-
if (stage.empty()) {
379-
std::cerr << "mcpp.dist.apple: mcpp reported no staged tree. This "
380-
"member needs mcpp 2026.9.11.1 or newer.\n";
381-
p.reason = "no staged tree";
382-
return p;
383-
}
384400

385401
const std::string target = target_for(opt);
386402
if (target.empty()) {
@@ -390,7 +406,9 @@ inline plan plan_for(options opt = {}) {
390406
return p;
391407
}
392408

393-
const std::string launcher = launcher_in(stage, target);
409+
const std::string launcher = stage.empty()
410+
? std::format("${{mcpp.target_file:{}}}", target)
411+
: launcher_in(stage, target);
394412
if (launcher.empty()) {
395413
std::cerr << std::format(
396414
"mcpp.dist.apple: the staged tree at {0} carries no launcher for "
@@ -400,7 +418,12 @@ inline plan plan_for(options opt = {}) {
400418
p.reason = "no launcher in the staged tree";
401419
return p;
402420
}
403-
const std::string executableName = bundle_executable_name(launcher);
421+
// CFBundleExecutable is a bare filename (see the note above). With no
422+
// staged tree the launcher is a PLACEHOLDER the engine expands later, so
423+
// its basename cannot be taken from the string -- the target's own name is
424+
// what the expansion will produce.
425+
const std::string executableName = stage.empty()
426+
? target : bundle_executable_name(launcher);
404427

405428
if (!opt.icon.empty() && !is_file(opt.icon)) {
406429
std::cerr << std::format("mcpp.dist.apple: the icon {} was not found", opt.icon) << '\n';
@@ -470,7 +493,20 @@ inline plan plan_for(options opt = {}) {
470493
// this action the engine's automatic dependency on the staged tree's
471494
// manifest (`docs/30-build-mcpp.md`, "An action that names
472495
// `${mcpp.stage_dir}` gains a dependency on the tree's manifest").
473-
layout.argv = { "ditto", "${mcpp.stage_dir}", contents + "/MacOS" };
496+
//
497+
// WITH NO STAGED TREE IT COPIES THE ONE PROGRAM. `${mcpp.stage_dir}`
498+
// REFUSES when there is no tree -- that is the engine's contract, and a
499+
// member that names it unconditionally cannot run on a target whose
500+
// built-in staging is refused. So the source is the tree when there is
501+
// one and the program when there is not, and the `inputs` entry is the
502+
// same either way: the program is what this bundle is FOR, and naming it
503+
// is what orders this action after the link.
504+
layout.argv = stage.empty()
505+
? std::vector<std::string>{ "ditto",
506+
std::format("${{mcpp.target_file:{}}}", target),
507+
contents + "/MacOS/" + executableName }
508+
: std::vector<std::string>{ "ditto", "${mcpp.stage_dir}",
509+
contents + "/MacOS" };
474510
layout.inputs = { std::format("${{mcpp.target_file:{}}}", target) };
475511
layout.output = contents + "/MacOS/" + executableName;
476512
p.steps.push_back(layout);

0 commit comments

Comments
 (0)