diff --git a/.agents/docs/2026-09-29-workspace-build-graph-design.md b/.agents/docs/2026-09-29-workspace-build-graph-design.md index d84af43c..9682eae9 100644 --- a/.agents/docs/2026-09-29-workspace-build-graph-design.md +++ b/.agents/docs/2026-09-29-workspace-build-graph-design.md @@ -558,6 +558,7 @@ read from the member, or made a value of the plan: | `[build] linkage` | not copied to the virtual root | a root-position value | unit `RootPositionValuesSeparateMembers` | | unknown `mcpp:` capability, cfg() and schema warnings | not reported for a member | refused and reported for each selected member | e2e 836 M2, M3 | | the shared libraries of a member's closure | only those the member's units link were placed | every graph-built shared library of the closure, with its aliases, as ยง15 states | e2e 835 L3 | +| the runtime files of a program shipped through `artifacts` (2026.9.29.3) | its link waited for the plan's deploy set, which a workspace plan does not place; its own runtime files were not in the member's directory | the link waits for no plan-level file; a member's runtime set includes the closures its `artifacts` edges reach | e2e 833 G9 | Each criterion fails on 2026.9.29.1 and passes on 2026.9.29.2. The resource case also showed a defect of every build: a quoted `#include` in a script was diff --git a/CHANGELOG.md b/CHANGELOG.md index 24faa9fb..f4d89d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.9.29.3] - 2026-09-29 + +This release completes the runtime placement of a program that a workspace +member ships through `artifacts`. The validation project's post-release build +of 2026.9.29.2 found it. + +### Fixed + +- **A program shipped through `artifacts` no longer waits for a runtime file + that a workspace plan never places.** Its link edge depended on the plan's + own deploy set, which a workspace plan does not place (`bin/` holds products + only), so a workspace whose members declare runtime files stopped with + "missing and no known rule to make it" (e2e 833 G9). +- **The runtime files of such a program are beside it.** A member's runtime + set includes the closure of every package whose program the member ships + through `artifacts`, so the program finds its own runtime files in the + member's product directory, as it did beside a root's program in `bin/` + (e2e 833 G9). + ## [2026.9.29.2] - 2026-09-29 This release corrects what a workspace plan reads from its members. The plan's diff --git a/mcpp.toml b/mcpp.toml index 13293072..a7bebe4e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.29.2" +version = "2026.9.29.3" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index a863d91a..a1dae570 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.29.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.29.3"; } // namespace mcpp diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 40fc6f96..7f3eb68f 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -2842,10 +2842,16 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements) { // action populated after the first plan), relinked a program whose // link inputs had not changed. Empty on RPATH platforms (no *.dll // deps), so other targets are unaffected. + // + // A workspace plan places no deploy set of its own (its root has no + // program; see `placedFiles`), so a unit outside the link groups, a + // program shipped through `artifacts`, waits for none: it is placed in + // the members' product directories with their deploy sets. std::string orderOnly; if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary) { - for (auto const& d : uflags.runtimeDeploy) - orderOnly += " " + escape_ninja_path(d.dest); + if (lu.linkGroup >= 0 || !plan.manifest.package.virtualRoot) + for (auto const& d : uflags.runtimeDeploy) + orderOnly += " " + escape_ninja_path(d.dest); if (lu.linkGroup >= 0) for (auto const& pl : plan.linkGroups[static_cast(lu.linkGroup)].placements) orderOnly += " " + escape_ninja_path(pl.dest); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index dc6a3642..298aa2e2 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -2863,9 +2863,23 @@ make_plan(const mcpp::manifest::Manifest& manifest, for (auto i : closureIdx) for (auto const& f : packages[i].linkUsage.ldflags) group.ldflags.push_back(f); + // The runtime files are those of the closure and of every package a + // program the closure ships through `artifacts` reaches: the program + // is placed beside the member's programs and loads its own runtime + // there, as it did beside a root's program in `bin/`. { + std::set runtimeIdx = closureIdx; + std::vector work; + for (auto const& [i, j] : artifactEdges) + if (closureIdx.contains(i) && runtimeIdx.insert(j).second) work.push_back(j); + while (!work.empty()) { + const auto i = work.back(); work.pop_back(); + if (auto it = directPackageDeps.find(i); it != directPackageDeps.end()) + for (auto j : it->second) + if (runtimeIdx.insert(j).second) work.push_back(j); + } std::vector closurePackages; - for (auto i : closureIdx) closurePackages.push_back(packages[i]); + for (auto i : runtimeIdx) closurePackages.push_back(packages[i]); derive_runtime(closurePackages, productDir, group); } const int groupIndex = static_cast(plan.linkGroups.size()); diff --git a/tests/e2e/833_a_workspace_is_one_graph_per_configuration.sh b/tests/e2e/833_a_workspace_is_one_graph_per_configuration.sh index caf33e10..01074c06 100755 --- a/tests/e2e/833_a_workspace_is_one_graph_per_configuration.sh +++ b/tests/e2e/833_a_workspace_is_one_graph_per_configuration.sh @@ -18,7 +18,11 @@ # G7 no member directory receives a build directory, and `clean --stale` # removes the ones members held before, keeping `target/.build-mcpp/`; # G8 a program a member ships through `artifacts` is beside that member's -# program, in its product directory. +# program, in its product directory; +# G9 that program's own runtime files are beside it there, and its link +# waits for no runtime file of another member (2026.9.29.2 made it wait +# for cli's, which a workspace plan places only in bin/cli/, so the +# build stopped with "missing and no known rule to make it"). set -e TMP=$(mktemp -d) @@ -61,6 +65,14 @@ EOF done printf '#include \nimport shared_core;\nint main() { std::printf("%%d\\n", core_v() + 1); return 0; }\n' > cli/src/main.cpp printf '#include \nimport shared_core;\nint main() { std::printf("%%d\\n", core_v() + 2); return 0; }\n' > gui/src/main.cpp +mkdir -p cli/data helper/data +printf 'cli\n' > cli/data/cli.dat +printf 'helper\n' > helper/data/helper.dat +cat >> cli/mcpp.toml <<'EOF' + +[runtime] +deploy_files = ["data/cli.dat"] +EOF cat >> gui/mcpp.toml <<'EOF' [dependencies.helper] @@ -75,6 +87,9 @@ version = "0.1.0" [targets.helper] kind = "bin" main = "src/main.cpp" + +[runtime] +deploy_files = ["data/helper.dat"] EOF printf 'int main() { return 0; }\n' > helper/src/main.cpp @@ -130,15 +145,26 @@ other=$(echo "$dirs" | grep -v "^$main_dir$") [ -f "$main_dir/bin/gui/helper$EXE" ] || fail "G8: bin/gui/helper is missing" ws.log [ -f "$main_dir/bin/helper/helper$EXE" ] || fail "G8: bin/helper/helper is missing" ws.log +# G9: the build above succeeded with a runtime file in cli's closure; the +# program gui ships has its runtime file beside it, and cli's is cli's. +[ -f "$main_dir/bin/gui/helper.dat" ] || fail "G9: bin/gui/helper.dat is missing" ws.log +[ -f "$main_dir/bin/cli/cli.dat" ] || fail "G9: bin/cli/cli.dat is missing" ws.log +[ ! -e "$main_dir/bin/gui/cli.dat" ] || fail "G9: cli's runtime file is in gui's product directory" ws.log + # G1: the core module is compiled once, in the one graph. log="$main_dir/.ninja_log" n=$(grep -c 'core\.m\.o' "$log" || true) [ "$n" = 1 ] || fail "G1: core's object was built $n times" "$log" -# G2: `-p cli` after `--workspace` compiles nothing. -before=$(wc -l < "$log") +# G2: `-p cli` after `--workspace` compiles and links nothing. Counted over +# objects, module interfaces and cli's program: the placement of cli's runtime +# file is one staging edge whose list names the selection, so a plan of +# another selection copies the file again. +built() { awk -F'\t' 'NR > 1 { print $4 }' "$log" \ + | grep -cE '\.(o|obj|gcm|pcm|ifc)$|^bin/cli/app' || true; } +before=$(built) "$MCPP" build -p cli > p.log 2>&1 || fail "G2: -p cli failed" p.log -after=$(wc -l < "$log") +after=$(built) [ "$before" = "$after" ] || fail "G2: -p cli recompiled after --workspace" "$log" # G3: an edit to gui's flags recompiles gui only. @@ -150,7 +176,10 @@ cxxflags = ["-DGUI_EXTRA=1"] EOF before=$(wc -l < "$log") "$MCPP" build --workspace > g3.log 2>&1 || fail "G3: the build after the edit failed" g3.log -tail -n +$((before + 1)) "$log" | cut -f4 > g3.edges +# Objects, module interfaces and programs; the staging of runtime files after +# a plan of another selection is not a rebuild (see G2). +tail -n +$((before + 1)) "$log" | cut -f4 \ + | grep -E '\.(o|obj|gcm|pcm|ifc)$|/app(\.exe)?$' > g3.edges || true grep -q 'gui' g3.edges || fail "G3: gui was not rebuilt after its flags changed" g3.edges if grep -Eq 'core|/cli/|_cli|ns1|ns2' g3.edges; then fail "G3: an edit to gui's flags rebuilt another member" g3.edges