@@ -713,16 +713,66 @@ inline plan plan_for(options opt = {}) {
713713 // one and the program when there is not, and the `inputs` entry is the
714714 // same either way: the program is what this bundle is FOR, and naming it
715715 // is what orders this action after the link.
716+ //
717+ // WITH A STAGED TREE IT COPIES THE PROGRAM, NOT THE TREE. The engine
718+ // stages a Mach-O program as `bin/<name>` with every deployed file under
719+ // `bin/<to>/...` beside it (mcpp#630, item 3a: staging precedes the
720+ // closure walk, and the tree is handed over with `closure = not-walked`
721+ // in its manifest). Copying that tree whole into `Contents/MacOS/` put
722+ // the executable at `Contents/MacOS/bin/<name>`, which is not where
723+ // `CFBundleExecutable` says it is, and the resources beside a program
724+ // rather than in `Contents/Resources/`, where `NSBundle` looks. So the
725+ // launcher goes to the executable directory by itself, and the staged
726+ // `bin/` entries that are not the launcher go to the bundle's resource
727+ // destination below, at the relative path `mcpp::deploy`'s `to` gave
728+ // them. `${mcpp.stage_dir}` is still named as an input of the launcher
729+ // copy, which is what orders every step here after staging and gives
730+ // the action its dependency on the tree's manifest.
716731 layout.argv = stage.empty ()
717732 ? std::vector<std::string>{ " ditto" ,
718733 std::format (" ${{mcpp.target_file:{}}}" , target),
719734 execDir + " /" + executableName }
720- : std::vector<std::string>{ " ditto" , " ${mcpp.stage_dir} " , execDir };
735+ : std::vector<std::string>{ " ditto" , launcher , execDir + " / " + executableName };
721736 layout.inputs = { std::format (" ${{mcpp.target_file:{}}}" , target) };
737+ if (!stage.empty ()) layout.inputs .push_back (" ${mcpp.stage_dir}" );
722738 layout.outputs = { execDir + " /" + executableName };
723739 p.steps .push_back (layout);
724740 assembled.push_back (execDir + " /" + executableName);
725741
742+ // THE DEPLOYED FILES, AT THE BUNDLE'S RESOURCE DESTINATION.
743+ //
744+ // Read at plan time against the staged tree, the way `dist-apk` reads
745+ // its assets: the second pass of `mcpp pack --format app` runs this
746+ // program after the tree is staged, so the entries exist on disk here.
747+ // One action per top-level entry under `bin/` that is not the launcher,
748+ // a file or a whole directory, each named by its own path so that the
749+ // graph carries an edge per deployed thing rather than one edge for
750+ // "everything". `ditto <dir> <dir>` copies the source's CONTENTS into
751+ // the destination, which is the flat layout iOS wants at the bundle
752+ // root and the `Contents/Resources/<to>/` layout macOS wants.
753+ if (!stage.empty ()) {
754+ const std::filesystem::path stageBin = std::filesystem::path (stage) / " bin" ;
755+ const std::filesystem::path launcherPath = std::filesystem::path (launcher);
756+ std::error_code ec;
757+ std::vector<std::filesystem::path> entries;
758+ if (std::filesystem::is_directory (stageBin, ec))
759+ for (auto const & e : std::filesystem::directory_iterator (stageBin, ec))
760+ if (!ec && e.path () != launcherPath) entries.push_back (e.path ());
761+ std::ranges::sort (entries);
762+ for (auto const & e : entries) {
763+ const std::string rel = e.filename ().string ();
764+ step res;
765+ res.id = " mcpp.dist.apple.resource." + rel;
766+ res.role = " artifact" ;
767+ res.description = " APP RESOURCE " + rel;
768+ res.argv = { " ditto" , e.string (), resourceDir + " /" + rel };
769+ res.inputs = { e.string (), " ${mcpp.stage_dir}" };
770+ res.outputs = { resourceDir + " /" + rel };
771+ p.steps .push_back (res);
772+ assembled.push_back (resourceDir + " /" + rel);
773+ }
774+ }
775+
726776 if (!opt.icon .empty ()) {
727777 step icon;
728778 icon.id = " mcpp.dist.apple.icon" ;
0 commit comments