4545// inside a directory named for a browser, which is a worse failure than a
4646// refusal naming the one target this format serves.
4747//
48- // `cp` PER FILE, ARGV ONLY, NO SHELL -- AND THAT IS WHY THIS MEMBER IS
49- // POSIX-HOST ONLY FOR NOW. `dist/appimage.cppm`'s own copies are single
48+ // THE ENGINE COPIES, NOT `cp`. Each staged file was one `cp SRC DST` action,
49+ // argv only, no shell -- exactly the shape `mcpp::action` is built for (a
50+ // graph edge per file, skippable on a cache hit) -- and that made this
51+ // member POSIX-host only, because neither precedent (`dist/appimage.cppm`'s
5052// files handed straight to `appimagetool`'s own argument list; `dist/
51- // apple.cppm` copies whole directories with `ditto`, which exists only on
52- // macOS. Neither precedent is a portable multi-file copier this member
53- // could reuse on Windows, so it declares one `cp SRC DST` action per file in
54- // the discovered set instead of shelling out to a recursive copy -- exactly
55- // the shape `mcpp::action` is built for (a graph edge per file, skippable on
56- // a cache hit), and the one the design record's implementation notes accept
57- // ("one cp/copy per file is acceptable"). The README states the POSIX-host
58- // limitation; lifting it needs either a `copy`-argv branch on the host OS or
59- // a small copier this member carries itself, and neither is written here
60- // because nothing in this collection has needed one yet.
53+ // apple.cppm`'s directories copied with `ditto`, macOS only) is a portable
54+ // multi-file copier. Lifting that needed either a `copy`-argv branch on the
55+ // host OS or a small copier this member carried itself, and both are the
56+ // wrong shape: `cmd /c copy` is a shell, is the 8191-character limit, and is
57+ // the switch-quoting this repository has already been bitten by twice; a
58+ // copier carried by the member is a host tool sub-build (#355) for one `cp`.
59+ //
60+ // The engine already has the copier. `mcpp stage --output <dst> <src>` is
61+ // the subcommand every `stage_file` edge in `build.ninja` already runs: it
62+ // creates the destination's parent, compares content and writes only on
63+ // difference. `${mcpp.self}`, an action argv substitution for the engine's
64+ // own absolute path, is what lets an action NAME it, so each copy step is
65+ // `{ "${mcpp.self}", "stage", "--verify", "content", "--output", dst, src }`
66+ // -- `--verify content` spelled out rather than defaulted, because a
67+ // contract must not depend on which of "the help text's default" (`size`)
68+ // and "the code's default" (`content`) a reader believes. Plan-time
69+ // `create_directories` is gone with it: `stage` creates the destination's
70+ // parent itself.
71+ //
72+ // `${mcpp.self}` AND `mcpp stage`'S ARGUMENT SHAPE ARE THE ENGINE CONTRACT
73+ // SINCE 2026.9.13.1, not a convenience this member happens to use -- see
74+ // docs/30's substitution table in that release. This member's floor is that
75+ // release for exactly this reason: an older engine leaves `${mcpp.self}`
76+ // literal in the command, and the action fails at run time with a
77+ // not-found for a path that reads as a token.
6178//
6279// WHY `index.html` IS WRITTEN AT PLAN TIME TO A SIDE FILE AND COPIED, RATHER
6380// THAN WRITTEN DIRECTLY TO ITS FINAL PATH. `dist/apple.cppm`'s own header
@@ -317,14 +334,9 @@ inline plan plan_for(options opt = {}) {
317334
318335 for (auto const & rel : relFiles) {
319336 const std::string dst = webDir + " /" + rel;
320- // Directories are created here, at plan time -- cheap (a handful of
321- // path segments, never hundreds of megabytes) and the same trade
322- // `write_if_different` already makes for `index.html`'s own parent.
323- // The CONTENT copy is the action; an empty directory existing a
324- // build early is not a cache-visible effect.
325- std::error_code ec;
326- std::filesystem::create_directories (
327- std::filesystem::path (dst).parent_path (), ec);
337+ // No `create_directories` here: `mcpp stage` creates the
338+ // destination's parent itself, which is the part of this step the
339+ // engine now does that the member used to.
328340
329341 step s;
330342 s.id = " mcpp.dist.web.file" ;
@@ -334,9 +346,13 @@ inline plan plan_for(options opt = {}) {
334346 // program just read `stageBin` from -- the same reasoning every
335347 // other member gives: the path in the graph and the path here
336348 // cannot disagree, and naming it earns this action the engine's
337- // automatic dependency on the staged tree's manifest.
349+ // automatic dependency on the staged tree's manifest. `${mcpp.self}`
350+ // is the same substitution family, naming the engine's own
351+ // executable so this action's command is an argv the engine
352+ // interprets on every host, with no shell and no host-specific copy
353+ // tool.
338354 const std::string src = " ${mcpp.stage_dir}/bin/" + rel;
339- s.argv = { " cp " , src, dst };
355+ s.argv = { " ${mcpp.self} " , " stage " , " --verify " , " content " , " --output " , dst, src };
340356 s.inputs = { src };
341357 s.outputs = { dst };
342358 p.steps .push_back (std::move (s));
@@ -346,7 +362,8 @@ inline plan plan_for(options opt = {}) {
346362 page.id = " mcpp.dist.web.index" ;
347363 page.role = " artifact" ;
348364 page.description = " INDEX.HTML" ;
349- page.argv = { " cp" , indexSrc, webDir + " /index.html" };
365+ page.argv = { " ${mcpp.self}" , " stage" , " --verify" , " content" , " --output" ,
366+ webDir + " /index.html" , indexSrc };
350367 page.inputs = { indexSrc };
351368 page.outputs = { webDir + " /index.html" };
352369 p.steps .push_back (std::move (page));
0 commit comments