Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/docs/2026-09-29-workspace-build-graph-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@
> Each `## [<version>]` 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
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion modules/versioning/src/version.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 8 additions & 2 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::size_t>(lu.linkGroup)].placements)
orderOnly += " " + escape_ninja_path(pl.dest);
Expand Down
16 changes: 15 additions & 1 deletion src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::size_t> runtimeIdx = closureIdx;
std::vector<std::size_t> 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<mcpp::modgraph::PackageRoot> 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<int>(plan.linkGroups.size());
Expand Down
39 changes: 34 additions & 5 deletions tests/e2e/833_a_workspace_is_one_graph_per_configuration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -61,6 +65,14 @@ EOF
done
printf '#include <cstdio>\nimport shared_core;\nint main() { std::printf("%%d\\n", core_v() + 1); return 0; }\n' > cli/src/main.cpp
printf '#include <cstdio>\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]
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
Loading