Skip to content

Commit 832f623

Browse files
authored
2026.9.29.4: a program shipped through artifacts links with its own closure's line, and ${mcpp.bin_dir} is the declaring member's product directory, in a workspace plan (#741)
The validation project's post-release build of 2026.9.29.3, and its cross-verification against this pull request, found two plan-level values used where a workspace member's own value is meant. - A program shipped through artifacts links with its own closure's line in a workspace plan: a link group of its own closure (LinkGroup::linkOnly) holds the link line and runtime contract and places nothing. The plan's line pools the dependencies' flags and not a member's, so a library the program's package states through its build program was missing (e2e 838 A1-A3). - ${mcpp.bin_dir} in an action a workspace member's build program declares is that member's product directory (e2e 838 A4; docs/30 in both languages). - Version 2026.9.29.4; design document section 17.1. Merged after both gates passed: this pull request's CI, and the validation project's build, run, pack and release layout with the mcpp built from this branch (Sunrisepeak/GalTranslPP run 36537600583). Refs #734.
1 parent c73725b commit 832f623

10 files changed

Lines changed: 178 additions & 11 deletions

File tree

‎.agents/docs/2026-09-29-workspace-build-graph-design.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,8 @@ read from the member, or made a value of the plan:
559559
| unknown `mcpp:` capability, cfg() and schema warnings | not reported for a member | refused and reported for each selected member | e2e 836 M2, M3 |
560560
| 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 |
561561
| 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 |
562+
| the link line of a program shipped through `artifacts` (2026.9.29.4) | the plan's line, which pools the dependencies' flags and not a member's, so a library its package's build program states was missing | a link group of its own closure that places nothing (`LinkGroup::linkOnly`) | e2e 838 |
563+
| `${mcpp.bin_dir}` in a member's action (2026.9.29.4) | the plan's `bin/` | the declaring member's product directory | e2e 838 A4 |
562564

563565
Each criterion fails on 2026.9.29.1 and passes on 2026.9.29.2. The resource
564566
case also showed a defect of every build: a quoted `#include` in a script was

‎CHANGELOG.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@
44
> Each `## [<version>]` section is that release's notes. Entries are written in English
55
> from 2026.9.28.3 on; earlier entries remain as written.
66

7+
## [2026.9.29.4] - 2026-09-29
8+
9+
This release links a program that a workspace member ships through
10+
`artifacts` with the link line of its own package's closure, and expands
11+
`${mcpp.bin_dir}` to where the declaring member's binaries land. The validation
12+
project's post-release build of 2026.9.29.3 found the first, and its
13+
cross-verification against this release's pull request the second.
14+
15+
### Fixed
16+
17+
- **A program shipped through `artifacts` links with its own closure's line in
18+
a workspace plan.** It was linked with the plan's line, which pools the
19+
dependencies' link flags and not a member's; a library that the program's
20+
package states through its build program (`mcpp::link_lib`) was therefore
21+
missing, and the link failed with undefined references (e2e 838). The
22+
program now has a link group of its own that holds its closure's line and
23+
runtime contract and places nothing, since the members that ship the program
24+
place it.
25+
- **`${mcpp.bin_dir}` in an action a workspace member's build program declares
26+
is that member's product directory.** It was the plan's `bin/`, where a
27+
member's binaries are not, so an action that named a file beside the
28+
member's program (a `.pdb`, say) read or wrote the wrong place (e2e 838 A4).
29+
730
## [2026.9.29.3] - 2026-09-29
831

932
This release completes the runtime placement of a program that a workspace

‎docs/30-build-mcpp.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ none to rely on), and the only interpolations are a closed set:
10031003
| Variable | Value |
10041004
|---|---|
10051005
| `${mcpp.out_dir}` | the build output directory |
1006-
| `${mcpp.bin_dir}` | where produced binaries land |
1006+
| `${mcpp.bin_dir}` | where produced binaries land: `bin/`, or, for an action a workspace member's build program declares, that member's product directory (2026.9.29.4+) |
10071007
| `${mcpp.compile_db}` | path to `compile_commands.json` (what clang-tidy's `-p` wants) |
10081008
| `${mcpp.target_file:<name>}` | the built file of target `<name>` |
10091009
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | the tree `mcpp pack` staged, absolute. `artifact` role only, and only under `mcpp pack --format <name>` |

‎docs/zh/30-build-mcpp.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,7 @@ mcpp 会写出 `<暂存树>.stage-manifest` —— 一个兄弟文件,永不
848848
| 变量 | 含义 |
849849
|---|---|
850850
| `${mcpp.out_dir}` | 构建输出目录 |
851-
| `${mcpp.bin_dir}` | 产出的二进制所在目录 |
851+
| `${mcpp.bin_dir}` | 产出的二进制所在目录:`bin/`;工作区成员的构建程序声明的 action 中为该成员的产物目录(2026.9.29.4+) |
852852
| `${mcpp.compile_db}` | `compile_commands.json` 的路径(clang-tidy 的 `-p` 要的就是它) |
853853
| `${mcpp.target_file:<name>}` | target `<name>` 构建出的文件 |
854854
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --format <name>` 下可用 |

‎mcpp.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.9.29.3"
3+
version = "2026.9.29.4"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

‎modules/versioning/src/version.cppm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ import std;
3131

3232
export namespace mcpp {
3333

34-
inline constexpr std::string_view MCPP_VERSION = "2026.9.29.3";
34+
inline constexpr std::string_view MCPP_VERSION = "2026.9.29.4";
3535

3636
} // namespace mcpp

‎src/build/ninja_backend.cppm‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements) {
12241224
std::vector<BuildPlan::DeployFile> placedFiles;
12251225
if (!plan.manifest.package.virtualRoot) placedFiles = deployFiles;
12261226
for (std::size_t g = 0; g < groupFlags.size(); ++g) {
1227+
if (plan.linkGroups[g].linkOnly) continue;
12271228
for (auto const& d : groupFlags[g].runtimeDeploy) placedFiles.push_back(d);
12281229
for (auto const& pl : plan.linkGroups[g].placements)
12291230
placedFiles.push_back({{pl.source}, pl.dest});
@@ -2844,12 +2845,16 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements) {
28442845
// deps), so other targets are unaffected.
28452846
//
28462847
// A workspace plan places no deploy set of its own (its root has no
2847-
// program; see `placedFiles`), so a unit outside the link groups, a
2848-
// program shipped through `artifacts`, waits for none: it is placed in
2849-
// the members' product directories with their deploy sets.
2848+
// program; see `placedFiles`), and the group of a program shipped
2849+
// through `artifacts` places none either, so such a program waits for
2850+
// none: it is placed in the members' product directories with their
2851+
// deploy sets.
28502852
std::string orderOnly;
28512853
if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary) {
2852-
if (lu.linkGroup >= 0 || !plan.manifest.package.virtualRoot)
2854+
const bool placesDeploy = lu.linkGroup >= 0
2855+
? !plan.linkGroups[static_cast<std::size_t>(lu.linkGroup)].linkOnly
2856+
: !plan.manifest.package.virtualRoot;
2857+
if (placesDeploy)
28532858
for (auto const& d : uflags.runtimeDeploy)
28542859
orderOnly += " " + escape_ninja_path(d.dest);
28552860
if (lu.linkGroup >= 0)

‎src/build/plan.cppm‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,11 @@ struct BuildPlan {
533533
// The graph-built shared libraries the member's units load, placed
534534
// beside them (a hard link where the file system allows, §5.3).
535535
std::vector<Placement> placements;
536+
// The group of a program shipped through `artifacts`: it holds the
537+
// link line and the runtime contract of the program's own closure,
538+
// and places nothing, since the members that ship the program place
539+
// it and its runtime files. `member` is empty.
540+
bool linkOnly = false;
536541
};
537542
std::vector<LinkGroup> linkGroups;
538543
};
@@ -2725,9 +2730,9 @@ make_plan(const mcpp::manifest::Manifest& manifest,
27252730
r.target.name, owner, lu.output.generic_string(), other.targetName));
27262731

27272732
std::set<std::string> closure{owner};
2733+
std::set<std::size_t> seen{r.packageIndex};
27282734
{
27292735
std::vector<std::size_t> work{r.packageIndex};
2730-
std::set<std::size_t> seen{r.packageIndex};
27312736
while (!work.empty()) {
27322737
const auto i = work.back(); work.pop_back();
27332738
if (auto it = directPackageDeps.find(i); it != directPackageDeps.end())
@@ -2795,6 +2800,23 @@ make_plan(const mcpp::manifest::Manifest& manifest,
27952800
lu.objects.push_back(cu.object);
27962801
}
27972802
append_shared_deps_for_linked_objects(lu);
2803+
// In a workspace plan the plan's own line pools the dependencies'
2804+
// flags and not a member's, so the program links with its closure's
2805+
// line, as a member's program does, in a group that places nothing.
2806+
if (manifest.package.virtualRoot) {
2807+
BuildPlan::LinkGroup group;
2808+
group.linkOnly = true;
2809+
group.productDir = lu.output.parent_path();
2810+
group.ldflags = packages[0].linkUsage.ldflags;
2811+
for (auto i : seen)
2812+
for (auto const& f : packages[i].linkUsage.ldflags)
2813+
group.ldflags.push_back(f);
2814+
std::vector<mcpp::modgraph::PackageRoot> closurePackages;
2815+
for (auto i : seen) closurePackages.push_back(packages[i]);
2816+
derive_runtime(closurePackages, group.productDir, group);
2817+
lu.linkGroup = static_cast<int>(plan.linkGroups.size());
2818+
plan.linkGroups.push_back(std::move(group));
2819+
}
27982820
plan.linkUnits.push_back(std::move(lu));
27992821
}
28002822

@@ -3082,7 +3104,7 @@ make_plan(const mcpp::manifest::Manifest& manifest,
30823104
// its path as a file (a program of the workspace's own package named as a
30833105
// member, say).
30843106
for (auto const& g : plan.linkGroups) {
3085-
if (g.productDir == std::filesystem::path("bin")) continue;
3107+
if (g.linkOnly || g.productDir == std::filesystem::path("bin")) continue;
30863108
for (auto const& u : plan.linkUnits) {
30873109
bool clash = u.output == g.productDir;
30883110
for (auto const& a : u.runtimeAliases) clash = clash || a == g.productDir;

‎src/build/prepare/plan.cpp‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,11 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
932932
// NAMES ids and a collision there costs a duplicate line, not a wrong
933933
// edge.
934934
bool thisActionUsesStageDir = false;
935+
// Where the declaring package's binaries land: `bin/`, or in a
936+
// workspace plan the product directory of the member that declared
937+
// the action (§15 of the 2026-09-29 workspace design). Set per
938+
// package by `collect`.
939+
std::filesystem::path binDir = ctx.plan.outputDir / "bin";
935940
const bool stagePass = !state.overrides.pack_stage_dir.empty();
936941
auto substitute = [&](std::string s, const char* actionId,
937942
mcpp::manifest::BuildAction::Role role) {
@@ -940,7 +945,7 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
940945
s.replace(p, what.size(), with);
941946
};
942947
rep("${mcpp.out_dir}", ctx.plan.outputDir.string());
943-
rep("${mcpp.bin_dir}", (ctx.plan.outputDir / "bin").string());
948+
rep("${mcpp.bin_dir}", binDir.string());
944949
rep("${mcpp.compile_db}", ctx.plan.compileDbPath.string());
945950
// The engine's own executable, absolute (2026.9.13.1+). An action
946951
// whose command is an argv with no shell has no portable way to
@@ -1024,6 +1029,9 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
10241029
// program has no idea which package the engine loaded it for.
10251030
// mcpp#534's ordering edge is scoped to this name.
10261031
auto owner = mcpp::build::qualified_package_name(mm);
1032+
binDir = ctx.plan.outputDir / "bin";
1033+
for (auto const& g : ctx.plan.linkGroups)
1034+
if (!g.linkOnly && g.member == owner) binDir = ctx.plan.outputDir / g.productDir;
10271035
for (auto a : mm.buildConfig.actions) {
10281036
thisActionUsesStageDir = false;
10291037
const auto sub = [&](std::string v) {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env bash
2+
# requires: elf
3+
# 838_a_program_shipped_through_artifacts_links_its_own_closure.sh --
4+
# workspace design 2026-09-29 §15 and §17.1.
5+
#
6+
# A program a member ships through `artifacts` is linked with the link line of
7+
# its own package's closure. Its package's build program states a library the
8+
# program needs (`mcpp::link_lib`); in a workspace plan that statement belongs
9+
# to the package, not to the plan, and 2026.9.29.3 linked the program with
10+
# the plan's line, so the library was missing ("undefined reference").
11+
#
12+
# A1 `mcpp build --workspace` links the program shipped through `artifacts`;
13+
# A2 the program in the shipping member's product directory runs;
14+
# A3 the program as its own member's product runs too;
15+
# A4 `${mcpp.bin_dir}` in an action the helper's build program declares is
16+
# the helper's product directory, where its binaries land (2026.9.29.3
17+
# expanded it to the plan's `bin/`, so a file named after the program
18+
# was looked for where the program is not).
19+
set -e
20+
21+
TMP=$(mktemp -d)
22+
trap "rm -rf $TMP" EXIT
23+
cd "$TMP"
24+
fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; }
25+
MCPP="${MCPP:-mcpp}"
26+
27+
# A library the helper links through its build program, built as an archive
28+
# by a project of its own and copied into the helper's package.
29+
mkdir -p vend/src
30+
cat > vend/mcpp.toml <<'EOF'
31+
[package]
32+
name = "vend"
33+
version = "0.1.0"
34+
35+
[targets.vend]
36+
kind = "lib"
37+
EOF
38+
printf 'extern "C" int vend_v() { return 7; }\n' > vend/src/vend.cpp
39+
(cd vend && "$MCPP" build > ../vend.log 2>&1) || fail "the archive project did not build" vend.log
40+
archive=$(find vend/target -name 'libvend.a' | head -1)
41+
[ -n "$archive" ] || fail "no libvend.a" vend.log
42+
43+
mkdir -p ws && cd ws
44+
cat > mcpp.toml <<'EOF'
45+
[workspace]
46+
members = ["gui", "helper"]
47+
EOF
48+
mkdir -p gui/src helper/src helper/vendor/lib
49+
cp "../$archive" helper/vendor/lib/
50+
cat > helper/mcpp.toml <<'EOF'
51+
[package]
52+
name = "helper"
53+
version = "0.1.0"
54+
55+
[targets.helper]
56+
kind = "bin"
57+
main = "src/main.cpp"
58+
EOF
59+
cat > helper/build.mcpp <<'EOF'
60+
import mcpp;
61+
int main() {
62+
mcpp::link_search("vendor/lib");
63+
mcpp::link_lib("vend");
64+
mcpp::action a;
65+
a.id = "copy-helper";
66+
a.role = mcpp::roles::artifact;
67+
a.arg("${mcpp.self}").arg("stage").arg("--verify").arg("content")
68+
.arg("--output").arg("${mcpp.bin_dir}/helper.copy")
69+
.arg("${mcpp.target_file:helper}")
70+
.input("${mcpp.target_file:helper}")
71+
.output("${mcpp.bin_dir}/helper.copy")
72+
.submit();
73+
return 0;
74+
}
75+
EOF
76+
printf 'extern "C" int vend_v();\nint main() { return vend_v() == 7 ? 0 : 1; }\n' > helper/src/main.cpp
77+
cat > gui/mcpp.toml <<'EOF'
78+
[package]
79+
name = "gui"
80+
version = "0.1.0"
81+
82+
[dependencies.helper]
83+
path = "../helper"
84+
artifacts = ["helper"]
85+
86+
[targets.app]
87+
kind = "bin"
88+
main = "src/main.cpp"
89+
EOF
90+
printf 'int main() { return 0; }\n' > gui/src/main.cpp
91+
92+
# A1
93+
"$MCPP" build --workspace > b.log 2>&1 || fail "A1: the workspace build failed" b.log
94+
95+
# A2, A3
96+
shipped=$(find target -path '*/bin/gui/helper' -type f | head -1)
97+
[ -n "$shipped" ] || fail "A2: bin/gui/helper is missing" b.log
98+
"$shipped" || fail "A2: bin/gui/helper did not run"
99+
own=$(find target -path '*/bin/helper/helper' -type f | head -1)
100+
[ -n "$own" ] || fail "A3: bin/helper/helper is missing" b.log
101+
"$own" || fail "A3: bin/helper/helper did not run"
102+
103+
# A4
104+
[ -f "$(dirname "$own")/helper.copy" ] \
105+
|| { find target -name 'helper.copy'; fail "A4: \${mcpp.bin_dir} is not the helper's product directory" b.log; }
106+
107+
echo "PASS: 838_a_program_shipped_through_artifacts_links_its_own_closure"

0 commit comments

Comments
 (0)