Skip to content

Commit e530c80

Browse files
committed
fix: exclude consumer link inputs from foreign-build contracts
1 parent 627786a commit e530c80

7 files changed

Lines changed: 67 additions & 7 deletions

File tree

‎.agents/docs/2026-11-05-heterogeneous-build-implementation-plan.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,39 @@ latest,实际执行输出 `mcpp 2026.11.5.1`;安装后的 ELF 与公开安
529529

530530
该补充继续使用 PR #889 与 2026.11.5.2。新的最终提交仍须完整 CI 以及其后
531531
独立生态审查,通过后才合入和发布;CLion 两仓库的兼容性结果不受此门禁替代。
532+
533+
## 22. 外部构建契约的消费输入与参数边界(2026-11-05)
534+
535+
插件 PR #52 的 CI 后审查复现了两项不同责任的问题。其 CMake 适配将 JSON argv
536+
拼成原生 flags 字符串,只为带空格的词增加引号,因而丢失字面引号、反斜杠、
537+
空参数和 tab 的边界。插件改为生成目标 toolchain 与按内容命名的编译器 response
538+
files,通过 CMake 的原生 options 交给各生成器转义;不向核心增加 CMake 原语。
539+
540+
另一项最小反例在实际 musl 跨构建中成立:同一 CMake 子工程既安装静态库,又
541+
构建一个独立可执行程序。核心契约将消费程序尚待安装的库包含在 link.args 中,
542+
使该独立程序在子工程安装前就依赖这份库,链接因文件不存在而失败。修正后的
543+
契约复用同一目标 flags 派生,只排除消费包的链接标志和库;目标、sysroot、平台
544+
与 profile 的设置仍由原派生逻辑产生,消费程序自己的实际链接不作删减。
545+
此处的通用边界属于核心,argv 到 CMake 的表示属于插件。
546+
547+
局部验证只重编 flags 和 compile_commands 两个模块,分别约 4.1 s 与 5.7 s,
548+
复用已有对象以及此前验证的进度模块。旧核心配合新插件仍在同一自依赖处失败;
549+
修正核心后,静态库、独立子工程程序和最终 musl 消费程序均实际运行通过。
550+
扩展原 982 E2E,要求消费包的标志不进入外部契约、同时仍存在于消费程序的
551+
build.ninja;旧模块失败,新模块通过。该诊断二进制不替代正式版本构建。
552+
553+
插件的真实编译器检查覆盖 GCC、Clang 的 Ninja/Make、C/C++ 作用域、
554+
try_compile、字面引号、反斜杠、美元符号、分号、tab、空字符串、重复选项次序、
555+
不变参数不重编,以及参数变化后的增量失效。clang-cl 的 response 文件实际
556+
生成 COFF,并核对字面数据;这不能等同原生 MSVC 全生成器资格。
557+
558+
还确认两项边界。纯 CMake 4.4.2/Ninja 工程在源码路径含单引号时也拆错依赖
559+
路径;这一负向控制不含 mcpp、toolchain 或 response files,不归为新插件缺陷。
560+
测试仍在 response-file 路径保留单引号,源码路径覆盖空格、Unicode 和美元符号。
561+
旧 CMake cache 则不会因后来指定 toolchain 而开始加载它,新参数表示因此使用
562+
独立构建目录,避免沿用旧 CMAKE_*_FLAGS。两项结果分别记录,不能靠重试掩盖。
563+
564+
核心继续复用 #889 和 2026.11.5.2,插件继续复用 #52。核心最终完整 CI 及其后
565+
生态审查、发布和公开安装仍须先完成;随后对齐插件 CI 的公开核心 pin,再验证
566+
完整插件候选。现有 Windows E2E shard 在下载 artifact 时 ECONNRESET,尚未运行
567+
测试;不据此判为源代码回归,也不在仍有新源码待验证时原样重复整个 CI。

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
command results and output checks remain authoritative.
1414
- New action snapshots request a normal UI frame, so short phases can appear
1515
between Ninja status lines without waiting for the clock redraw.
16+
- Foreign-build contracts exclude the consuming package's link flags and
17+
libraries. A subproject can link its own executable without depending on
18+
an installation that the same subproject has yet to produce.
1619
- The macOS clean bootstrap keeps its stage-one compiler outside the output tree
1720
removed by the second stage.
1821

‎docs/30-build-mcpp.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ none to rely on), and the only interpolations are a closed set:
10971097
| `${mcpp.out_dir}` | the build output directory |
10981098
| `${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+) |
10991099
| `${mcpp.compile_db}` | path to `compile_commands.json` (what clang-tidy's `-p` wants) |
1100-
| `${mcpp.contract}` *(2026.10.30.1+)* | path to the configuration's contract, `mcpp-contract.json`, beside its compile database: the compiler of each language with the words every unit is compiled with (target, sysroot, target-side headers, platform and profile flags), and the link driver, its words and the archiver. A foreign build system reads it to compile for the target as the engine does; a package's own include directories are not in it, nor the engine's C++ dialect and module words (`-std=`, `-fmodules`), which are its choice for its own units |
1100+
| `${mcpp.contract}` *(2026.10.30.1+)* | path to the configuration's contract, `mcpp-contract.json`, beside its compile database: the compiler of each language with the words every unit is compiled with (target, sysroot, target-side headers, platform and profile flags), and the link driver, its words and the archiver. A foreign build system reads it to compile for the target as the engine does; a package's own include directories are not in it, nor the engine's C++ dialect and module words (`-std=`, `-fmodules`), which are its choice for its own units. Since 2026.11.5.2, link arguments describe the target configuration; the consuming package's link flags and libraries are excluded, since they can name outputs the foreign build has yet to produce |
11011101
| `${mcpp.target_file:<name>}` | the built file of target `<name>` |
11021102
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | the tree `mcpp pack` staged, absolute. `artifact` role only, and only under `mcpp pack --pkg-format <name>` |
11031103
| `${mcpp.self}` *(2026.9.13.1+)* | the engine's own executable, absolute. An action's command is an argv with no shell, so a build program has no portable way to copy a file; the engine is present wherever a build runs, and `${mcpp.self} stage --verify content --output <dst> <src>` copies one file, creates the destination's parent, and writes only when the bytes differ. That argument shape is a contract from 2026.9.13.1 on; a build program that names it declares that release as its floor |

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,7 @@ mcpp 会写出 `<暂存树>.stage-manifest` —— 一个兄弟文件,永不
933933
| `${mcpp.out_dir}` | 构建输出目录 |
934934
| `${mcpp.bin_dir}` | 产出的二进制所在目录:`bin/`;工作区成员的构建程序声明的 action 中为该成员的产物目录(2026.9.29.4+) |
935935
| `${mcpp.compile_db}` | `compile_commands.json` 的路径(clang-tidy 的 `-p` 要的就是它) |
936-
| `${mcpp.contract}` *(2026.10.30.1+)* | 配置的契约 `mcpp-contract.json` 的路径,与该配置的编译数据库同目录:每种语言的编译器与每个单元都带的词(目标、sysroot、目标侧的头文件、平台与 profile 的标志),以及链接驱动、它的词与归档工具。外部构建系统读它,以引擎的方式为目标编译;包自己的包含目录不在其中,引擎为自己的单元选择的 C++ 方言与模块词(`-std=`、`-fmodules`)也不在其中 |
936+
| `${mcpp.contract}` *(2026.10.30.1+)* | 配置的契约 `mcpp-contract.json` 的路径,与该配置的编译数据库同目录:每种语言的编译器与每个单元都带的词(目标、sysroot、目标侧的头文件、平台与 profile 的标志),以及链接驱动、它的词与归档工具。外部构建系统读它,以引擎的方式为目标编译;包自己的包含目录不在其中,引擎为自己的单元选择的 C++ 方言与模块词(`-std=`、`-fmodules`)也不在其中。自 2026.11.5.2 起,链接参数描述目标配置,不含消费包自己的链接标志和库;这些库可能正是外部构建尚待生成的产物 |
937937
| `${mcpp.target_file:<name>}` | target `<name>` 构建出的文件 |
938938
| `${mcpp.stage_dir}` *(2026.9.11.1+)* | `mcpp pack` 暂存出的那棵树,绝对路径。仅 `artifact` role 可用,且仅在 `mcpp pack --pkg-format <name>` 下可用 |
939939
| `${mcpp.self}` *(2026.9.13.1+)* | 引擎自己的可执行文件,绝对路径。action 的命令是没有 shell 的 argv,构建程序因此没有可移植的拷贝手段;引擎在构建运行的每台机器上都在,`${mcpp.self} stage --verify content --output <dst> <src>` 拷贝一个文件、创建目标的父目录、只在字节不同时写入。这个参数形状自 2026.9.13.1 起是契约;写下它的构建程序即以该版本为下限 |

‎src/build/compile_commands.cppm‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ write_compile_commands(const BuildPlan& plan, const CompileFlags& flags);
204204
// calls mcpp back. A tool for the host is built in the host graph, whose
205205
// configuration has its own contract.
206206
//
207-
// {"schema": 1, "target": "<triple>", "dialect": "<gcc|clang|msvc>",
207+
// {"schema": 1, "target": "<triple>", "dialect": "<gnu|msvc>",
208208
// "compile": {"c": {"program", "args"}, "cxx": {...}, "asm": {...}},
209209
// "link": {"program", "args", "archiver"}}
210210
std::filesystem::path contract_path(const BuildPlan& plan);
@@ -901,8 +901,10 @@ std::expected<void, std::string> write_build_contract(const BuildPlan& plan, con
901901
doc["compile"]["asm"] = tool(flags.ccBinary, flags.as, true);
902902
target_side(doc["compile"]["asm"], plan.contractTargetSide.cflags, true);
903903
for (auto const& w : plan.contractTargetSide.asmflags) doc["compile"]["asm"]["args"].push_back(w);
904-
auto link = tool(flags.ldDriver.empty() ? flags.cxxBinary : flags.ldDriver, flags.ld, false);
905-
link["archiver"] = flags.arBinary.generic_string();
904+
const auto contractFlags = compute_build_contract_flags(plan);
905+
auto link = tool(contractFlags.ldDriver.empty() ? contractFlags.cxxBinary : contractFlags.ldDriver,
906+
contractFlags.ld, false);
907+
link["archiver"] = contractFlags.arBinary.generic_string();
906908
doc["link"] = std::move(link);
907909
const auto path = contract_path(plan);
908910
std::error_code ec;

‎src/build/flags.cppm‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ std::string render_link_intent_flags(
188188
LinkIntentFlavor flavor);
189189

190190
CompileFlags compute_flags(const BuildPlan& plan);
191+
// A foreign build inherits target configuration, not the consuming
192+
// package's link inputs (which may be outputs of that foreign build).
193+
CompileFlags compute_build_contract_flags(const BuildPlan& plan);
191194

192195
// The object format the TARGET produces (#647 E3). One derivation, read by
193196
// `compute_flags` for the runtime contract table and the link-line shape, and
@@ -707,7 +710,7 @@ std::string program_msvc_crt_linkage(const mcpp::manifest::Manifest& m,
707710
? "static" : "dynamic";
708711
}
709712

710-
CompileFlags compute_flags(const BuildPlan& plan) {
713+
CompileFlags compute_flags_impl(const BuildPlan& plan, bool packageLinkFlags) {
711714
CompileFlags f;
712715

713716
// Central query points for per-toolchain decisions — prefer these over
@@ -1289,7 +1292,10 @@ CompileFlags compute_flags(const BuildPlan& plan) {
12891292
// `__cxa_throw` and the personality first, and libc++abi answered the rest
12901293
// (mcpp#838, measured: `catch (const std::exception&)` missed).
12911294
std::string user_ldflags;
1292-
for (auto const& word : mcpp::manifest::flag_words(plan.manifest.buildConfig.ldflags)) {
1295+
const auto linkWords = packageLinkFlags
1296+
? mcpp::manifest::flag_words(plan.manifest.buildConfig.ldflags)
1297+
: std::vector<std::string>{};
1298+
for (auto const& word : linkWords) {
12931299
const auto rendered = ninja_command_word(normalize_ldflag(plan.projectRoot, word));
12941300
if (plan.cxxIsland && std::ranges::find(plan.cxxIsland->namedWords, word)
12951301
!= plan.cxxIsland->namedWords.end()) {
@@ -2637,4 +2643,12 @@ CompileFlags compute_flags(const BuildPlan& plan) {
26372643
return f;
26382644
}
26392645

2646+
CompileFlags compute_flags(const BuildPlan& plan) {
2647+
return compute_flags_impl(plan, true);
2648+
}
2649+
2650+
CompileFlags compute_build_contract_flags(const BuildPlan& plan) {
2651+
return compute_flags_impl(plan, false);
2652+
}
2653+
26402654
} // namespace mcpp::build

‎tests/e2e/982_an_action_reads_the_builds_contract.sh‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ cat > build.mcpp <<'EOF'
2121
#include <string>
2222
import mcpp;
2323
int main() {
24+
mcpp::link_flag("-Wl,--defsym,mcpp_contract_consumer=1");
2425
const std::string out = std::string(mcpp::out_dir()) + "/seen-contract.json";
2526
mcpp::action a;
2627
a.id = "copy-contract";
@@ -47,8 +48,12 @@ assert any(a.startswith("-O") for a in cxx), cxx
4748
assert not any(a.startswith("-std=") or a == "-fmodules" or a.startswith("-fmodule-") for a in cxx), cxx
4849
assert os.path.isabs(d["link"]["program"]) and isinstance(d["link"]["args"], list), d["link"]
4950
assert "archiver" in d["link"], d["link"]
51+
assert not any("mcpp_contract_consumer" in a for a in d["link"]["args"]), d["link"]
5052
print("contract:", d["target"], d["dialect"], d["compile"]["cxx"]["program"])
5153
PY
5254
cfg=$(dirname "$(find target -name compile_commands.json -not -path '*/.build-mcpp/*' | head -1)")
5355
[[ -f "$cfg/mcpp-contract.json" ]] || { echo "FAIL: no contract beside the compile database"; exit 1; }
56+
grep -q 'mcpp_contract_consumer=1' "$cfg/build.ninja" || {
57+
echo "FAIL: excluding package link inputs from the contract removed them from the consumer"; exit 1;
58+
}
5459
echo "PASS: 982"

0 commit comments

Comments
 (0)