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
2 changes: 2 additions & 0 deletions .agents/docs/2026-09-29-workspace-build-graph-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ read from the member, or made a value of the plan:
| 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 |
| 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 |
| `${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 |

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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@
> 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.4] - 2026-09-29

This release links a program that a workspace member ships through
`artifacts` with the link line of its own package's closure, and expands
`${mcpp.bin_dir}` to where the declaring member's binaries land. The validation
project's post-release build of 2026.9.29.3 found the first, and its
cross-verification against this release's pull request the second.

### Fixed

- **A program shipped through `artifacts` links with its own closure's line in
a workspace plan.** It was linked with the plan's line, which pools the
dependencies' link flags and not a member's; a library that the program's
package states through its build program (`mcpp::link_lib`) was therefore
missing, and the link failed with undefined references (e2e 838). The
program now has a link group of its own that holds its closure's line and
runtime contract and places nothing, since the members that ship the program
place it.
- **`${mcpp.bin_dir}` in an action a workspace member's build program declares
is that member's product directory.** It was the plan's `bin/`, where a
member's binaries are not, so an action that named a file beside the
member's program (a `.pdb`, say) read or wrote the wrong place (e2e 838 A4).

## [2026.9.29.3] - 2026-09-29

This release completes the runtime placement of a program that a workspace
Expand Down
2 changes: 1 addition & 1 deletion docs/30-build-mcpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@ none to rely on), and the only interpolations are a closed set:
| Variable | Value |
|---|---|
| `${mcpp.out_dir}` | the build output directory |
| `${mcpp.bin_dir}` | where produced binaries land |
| `${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+) |
| `${mcpp.compile_db}` | path to `compile_commands.json` (what clang-tidy's `-p` wants) |
| `${mcpp.target_file:<name>}` | the built file of target `<name>` |
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | the tree `mcpp pack` staged, absolute. `artifact` role only, and only under `mcpp pack --format <name>` |
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/30-build-mcpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ mcpp 会写出 `<暂存树>.stage-manifest` —— 一个兄弟文件,永不
| 变量 | 含义 |
|---|---|
| `${mcpp.out_dir}` | 构建输出目录 |
| `${mcpp.bin_dir}` | 产出的二进制所在目录 |
| `${mcpp.bin_dir}` | 产出的二进制所在目录:`bin/`;工作区成员的构建程序声明的 action 中为该成员的产物目录(2026.9.29.4+) |
| `${mcpp.compile_db}` | `compile_commands.json` 的路径(clang-tidy 的 `-p` 要的就是它) |
| `${mcpp.target_file:<name>}` | target `<name>` 构建出的文件 |
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --format <name>` 下可用 |
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.3"
version = "2026.9.29.4"
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.3";
inline constexpr std::string_view MCPP_VERSION = "2026.9.29.4";

} // namespace mcpp
13 changes: 9 additions & 4 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements) {
std::vector<BuildPlan::DeployFile> placedFiles;
if (!plan.manifest.package.virtualRoot) placedFiles = deployFiles;
for (std::size_t g = 0; g < groupFlags.size(); ++g) {
if (plan.linkGroups[g].linkOnly) continue;
for (auto const& d : groupFlags[g].runtimeDeploy) placedFiles.push_back(d);
for (auto const& pl : plan.linkGroups[g].placements)
placedFiles.push_back({{pl.source}, pl.dest});
Expand Down Expand Up @@ -2844,12 +2845,16 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements) {
// 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.
// program; see `placedFiles`), and the group of a program shipped
// through `artifacts` places none either, so such a program 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) {
if (lu.linkGroup >= 0 || !plan.manifest.package.virtualRoot)
const bool placesDeploy = lu.linkGroup >= 0
? !plan.linkGroups[static_cast<std::size_t>(lu.linkGroup)].linkOnly
: !plan.manifest.package.virtualRoot;
if (placesDeploy)
for (auto const& d : uflags.runtimeDeploy)
orderOnly += " " + escape_ninja_path(d.dest);
if (lu.linkGroup >= 0)
Expand Down
26 changes: 24 additions & 2 deletions src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,11 @@ struct BuildPlan {
// The graph-built shared libraries the member's units load, placed
// beside them (a hard link where the file system allows, §5.3).
std::vector<Placement> placements;
// The group of a program shipped through `artifacts`: it holds the
// link line and the runtime contract of the program's own closure,
// and places nothing, since the members that ship the program place
// it and its runtime files. `member` is empty.
bool linkOnly = false;
};
std::vector<LinkGroup> linkGroups;
};
Expand Down Expand Up @@ -2725,9 +2730,9 @@ make_plan(const mcpp::manifest::Manifest& manifest,
r.target.name, owner, lu.output.generic_string(), other.targetName));

std::set<std::string> closure{owner};
std::set<std::size_t> seen{r.packageIndex};
{
std::vector<std::size_t> work{r.packageIndex};
std::set<std::size_t> seen{r.packageIndex};
while (!work.empty()) {
const auto i = work.back(); work.pop_back();
if (auto it = directPackageDeps.find(i); it != directPackageDeps.end())
Expand Down Expand Up @@ -2795,6 +2800,23 @@ make_plan(const mcpp::manifest::Manifest& manifest,
lu.objects.push_back(cu.object);
}
append_shared_deps_for_linked_objects(lu);
// In a workspace plan the plan's own line pools the dependencies'
// flags and not a member's, so the program links with its closure's
// line, as a member's program does, in a group that places nothing.
if (manifest.package.virtualRoot) {
BuildPlan::LinkGroup group;
group.linkOnly = true;
group.productDir = lu.output.parent_path();
group.ldflags = packages[0].linkUsage.ldflags;
for (auto i : seen)
for (auto const& f : packages[i].linkUsage.ldflags)
group.ldflags.push_back(f);
std::vector<mcpp::modgraph::PackageRoot> closurePackages;
for (auto i : seen) closurePackages.push_back(packages[i]);
derive_runtime(closurePackages, group.productDir, group);
lu.linkGroup = static_cast<int>(plan.linkGroups.size());
plan.linkGroups.push_back(std::move(group));
}
plan.linkUnits.push_back(std::move(lu));
}

Expand Down Expand Up @@ -3082,7 +3104,7 @@ make_plan(const mcpp::manifest::Manifest& manifest,
// its path as a file (a program of the workspace's own package named as a
// member, say).
for (auto const& g : plan.linkGroups) {
if (g.productDir == std::filesystem::path("bin")) continue;
if (g.linkOnly || g.productDir == std::filesystem::path("bin")) continue;
for (auto const& u : plan.linkUnits) {
bool clash = u.output == g.productDir;
for (auto const& a : u.runtimeAliases) clash = clash || a == g.productDir;
Expand Down
10 changes: 9 additions & 1 deletion src/build/prepare/plan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,11 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
// NAMES ids and a collision there costs a duplicate line, not a wrong
// edge.
bool thisActionUsesStageDir = false;
// Where the declaring package's binaries land: `bin/`, or in a
// workspace plan the product directory of the member that declared
// the action (§15 of the 2026-09-29 workspace design). Set per
// package by `collect`.
std::filesystem::path binDir = ctx.plan.outputDir / "bin";
const bool stagePass = !state.overrides.pack_stage_dir.empty();
auto substitute = [&](std::string s, const char* actionId,
mcpp::manifest::BuildAction::Role role) {
Expand All @@ -940,7 +945,7 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
s.replace(p, what.size(), with);
};
rep("${mcpp.out_dir}", ctx.plan.outputDir.string());
rep("${mcpp.bin_dir}", (ctx.plan.outputDir / "bin").string());
rep("${mcpp.bin_dir}", binDir.string());
rep("${mcpp.compile_db}", ctx.plan.compileDbPath.string());
// The engine's own executable, absolute (2026.9.13.1+). An action
// whose command is an argv with no shell has no portable way to
Expand Down Expand Up @@ -1024,6 +1029,9 @@ static std::expected<void, std::string> step13_build_graph_actions(PrepareState&
// program has no idea which package the engine loaded it for.
// mcpp#534's ordering edge is scoped to this name.
auto owner = mcpp::build::qualified_package_name(mm);
binDir = ctx.plan.outputDir / "bin";
for (auto const& g : ctx.plan.linkGroups)
if (!g.linkOnly && g.member == owner) binDir = ctx.plan.outputDir / g.productDir;
for (auto a : mm.buildConfig.actions) {
thisActionUsesStageDir = false;
const auto sub = [&](std::string v) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# requires: elf
# 838_a_program_shipped_through_artifacts_links_its_own_closure.sh --
# workspace design 2026-09-29 §15 and §17.1.
#
# A program a member ships through `artifacts` is linked with the link line of
# its own package's closure. Its package's build program states a library the
# program needs (`mcpp::link_lib`); in a workspace plan that statement belongs
# to the package, not to the plan, and 2026.9.29.3 linked the program with
# the plan's line, so the library was missing ("undefined reference").
#
# A1 `mcpp build --workspace` links the program shipped through `artifacts`;
# A2 the program in the shipping member's product directory runs;
# A3 the program as its own member's product runs too;
# A4 `${mcpp.bin_dir}` in an action the helper's build program declares is
# the helper's product directory, where its binaries land (2026.9.29.3
# expanded it to the plan's `bin/`, so a file named after the program
# was looked for where the program is not).
set -e

TMP=$(mktemp -d)
trap "rm -rf $TMP" EXIT
cd "$TMP"
fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; }
MCPP="${MCPP:-mcpp}"

# A library the helper links through its build program, built as an archive
# by a project of its own and copied into the helper's package.
mkdir -p vend/src
cat > vend/mcpp.toml <<'EOF'
[package]
name = "vend"
version = "0.1.0"

[targets.vend]
kind = "lib"
EOF
printf 'extern "C" int vend_v() { return 7; }\n' > vend/src/vend.cpp
(cd vend && "$MCPP" build > ../vend.log 2>&1) || fail "the archive project did not build" vend.log
archive=$(find vend/target -name 'libvend.a' | head -1)
[ -n "$archive" ] || fail "no libvend.a" vend.log

mkdir -p ws && cd ws
cat > mcpp.toml <<'EOF'
[workspace]
members = ["gui", "helper"]
EOF
mkdir -p gui/src helper/src helper/vendor/lib
cp "../$archive" helper/vendor/lib/
cat > helper/mcpp.toml <<'EOF'
[package]
name = "helper"
version = "0.1.0"

[targets.helper]
kind = "bin"
main = "src/main.cpp"
EOF
cat > helper/build.mcpp <<'EOF'
import mcpp;
int main() {
mcpp::link_search("vendor/lib");
mcpp::link_lib("vend");
mcpp::action a;
a.id = "copy-helper";
a.role = mcpp::roles::artifact;
a.arg("${mcpp.self}").arg("stage").arg("--verify").arg("content")
.arg("--output").arg("${mcpp.bin_dir}/helper.copy")
.arg("${mcpp.target_file:helper}")
.input("${mcpp.target_file:helper}")
.output("${mcpp.bin_dir}/helper.copy")
.submit();
return 0;
}
EOF
printf 'extern "C" int vend_v();\nint main() { return vend_v() == 7 ? 0 : 1; }\n' > helper/src/main.cpp
cat > gui/mcpp.toml <<'EOF'
[package]
name = "gui"
version = "0.1.0"

[dependencies.helper]
path = "../helper"
artifacts = ["helper"]

[targets.app]
kind = "bin"
main = "src/main.cpp"
EOF
printf 'int main() { return 0; }\n' > gui/src/main.cpp

# A1
"$MCPP" build --workspace > b.log 2>&1 || fail "A1: the workspace build failed" b.log

# A2, A3
shipped=$(find target -path '*/bin/gui/helper' -type f | head -1)
[ -n "$shipped" ] || fail "A2: bin/gui/helper is missing" b.log
"$shipped" || fail "A2: bin/gui/helper did not run"
own=$(find target -path '*/bin/helper/helper' -type f | head -1)
[ -n "$own" ] || fail "A3: bin/helper/helper is missing" b.log
"$own" || fail "A3: bin/helper/helper did not run"

# A4
[ -f "$(dirname "$own")/helper.copy" ] \
|| { find target -name 'helper.copy'; fail "A4: \${mcpp.bin_dir} is not the helper's product directory" b.log; }

echo "PASS: 838_a_program_shipped_through_artifacts_links_its_own_closure"
Loading