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 9682eae9..0cc3a96d 100644 --- a/.agents/docs/2026-09-29-workspace-build-graph-design.md +++ b/.agents/docs/2026-09-29-workspace-build-graph-design.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d89d74..fd0f0abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ > 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.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 diff --git a/docs/30-build-mcpp.md b/docs/30-build-mcpp.md index 41e0db38..1fab5940 100644 --- a/docs/30-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -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:}` | the built file of target `` | | `${mcpp.stage_dir}` *(2026.9.11.1+)* | the tree `mcpp pack` staged, absolute. `artifact` role only, and only under `mcpp pack --format ` | diff --git a/docs/zh/30-build-mcpp.md b/docs/zh/30-build-mcpp.md index b8edd2bb..01480c41 100644 --- a/docs/zh/30-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -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:}` | target `` 构建出的文件 | | `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --format ` 下可用 | diff --git a/mcpp.toml b/mcpp.toml index a7bebe4e..87c738f5 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index a1dae570..42a16012 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.3"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.29.4"; } // namespace mcpp diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 7f3eb68f..5f9bf914 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -1224,6 +1224,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements) { std::vector 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}); @@ -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(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) diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 298aa2e2..dba12722 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -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 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 linkGroups; }; @@ -2725,9 +2730,9 @@ make_plan(const mcpp::manifest::Manifest& manifest, r.target.name, owner, lu.output.generic_string(), other.targetName)); std::set closure{owner}; + std::set seen{r.packageIndex}; { std::vector work{r.packageIndex}; - std::set seen{r.packageIndex}; while (!work.empty()) { const auto i = work.back(); work.pop_back(); if (auto it = directPackageDeps.find(i); it != directPackageDeps.end()) @@ -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 closurePackages; + for (auto i : seen) closurePackages.push_back(packages[i]); + derive_runtime(closurePackages, group.productDir, group); + lu.linkGroup = static_cast(plan.linkGroups.size()); + plan.linkGroups.push_back(std::move(group)); + } plan.linkUnits.push_back(std::move(lu)); } @@ -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; diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index 3df8cc46..7c225754 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -932,6 +932,11 @@ static std::expected 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) { @@ -940,7 +945,7 @@ static std::expected 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 @@ -1024,6 +1029,9 @@ static std::expected 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) { diff --git a/tests/e2e/838_a_program_shipped_through_artifacts_links_its_own_closure.sh b/tests/e2e/838_a_program_shipped_through_artifacts_links_its_own_closure.sh new file mode 100644 index 00000000..2dce87ae --- /dev/null +++ b/tests/e2e/838_a_program_shipped_through_artifacts_links_its_own_closure.sh @@ -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"