diff --git a/.github/tools/check_version_pins.sh b/.github/tools/check_version_pins.sh index 36b6fecc..b108e478 100755 --- a/.github/tools/check_version_pins.sh +++ b/.github/tools/check_version_pins.sh @@ -140,6 +140,19 @@ fi # what an earlier revision of this script got wrong: it sent CI to install a # version that did not exist yet, and every job died with # `package 'mcpp@' not found`. +# +# But it may lag by exactly ONE release, no more. xim-pkgindex carries a +# SINGLE mcpp version — the newest — so the moment a release lands, every +# older pin names something that is no longer installable: +# +# [error] xlings: version '2026.8.4.1' not found for 'mcpp' +# [error] available: 2026.8.5.1 +# +# which is how the aarch64 fresh-install job died after 2026.8.5.1 shipped. +# The rule that satisfies both directions: after publishing release N, the +# post-release commit sets this pin to N. Never ahead (check (c)), never +# more than one behind (not checkable here — it needs the index — so it +# surfaces as the error above, and this note is where to look when it does). # (c) …and the bootstrap pin must never run AHEAD of the version being built. # Four-key numeric sort, so the date scheme orders correctly (a plain diff --git a/.github/workflows/ci-aarch64-fresh-install.yml b/.github/workflows/ci-aarch64-fresh-install.yml index 679d5123..ad878224 100644 --- a/.github/workflows/ci-aarch64-fresh-install.yml +++ b/.github/workflows/ci-aarch64-fresh-install.yml @@ -99,8 +99,25 @@ jobs: run: | # mcpp/xlings manifests pin a glibc default toolchain; on aarch64 the # musl-static target is the published path, so build with --target. - git clone --depth 1 https://github.com/mcpp-community/mcpp /tmp/mcpp-src + # + # On a pull_request, self-host the code UNDER REVIEW rather than + # upstream main. Cloning main meant this gate never saw the PR at + # all: a change that breaks the from-source build passed here and + # only failed after merge, and — the way this surfaced — a fix to + # the repo's own bootstrap pin was untestable, because the fix was + # on the branch while the clone was of main. Outside a PR there is + # no head ref and main is exactly right. + ref='${{ github.event.pull_request.head.sha }}' + repo='${{ github.event.pull_request.head.repo.clone_url }}' + [ -n "$ref" ] || ref='${{ github.sha }}' + [ -n "$repo" ] || repo='https://github.com/mcpp-community/mcpp' + # fetch-by-sha rather than `clone --depth 1`, which cannot take one. + git init -q /tmp/mcpp-src cd /tmp/mcpp-src + git remote add origin "$repo" + git fetch -q --depth 1 origin "$ref" + git checkout -q FETCH_HEAD + echo "self-hosting $repo @ $ref" mcpp self config --mirror GLOBAL 2>/dev/null || true mcpp build --target aarch64-linux-musl m=$(find target/aarch64-linux-musl -type f -path '*/bin/mcpp' | head -1) diff --git a/.xlings.json b/.xlings.json index 4cd7c236..b030603e 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.8.4.1" + "mcpp": "2026.8.5.1" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e4cd72..c799ac66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.8.5.2] — 2026-08-05 + +修复 `host-module = true`(规则包)的两个缺陷。二者都是 2026.8.5.1 引入的,合起来的效果是:**规则包只能写「手工 printf 指令」的玩具规则**,一旦规则要用它本该用的 API 就编不过。第一个真实使用者(`grpc-m` 的 protoc/gRPC codegen 规则)在第一分钟就同时撞上了这两个。 + +### 修复 + +- **规则里的 `import std;`。** `build_program.cppm` 先编 host module、后建 std 模块,所以规则拿到的 `stdFlags` 是空的,报 `module 'std' not found`。而且「是否需要 std」只扫了 `build.mcpp` 的源码 —— 一条只有**规则**用 `import std;` 的构建根本不会去建 std 模块。现在两处都修:扫描把规则接口一并计入,std 那一段整体挪到编译 host module **之前**。 + + 这里的顺序是承重的,不是风格问题:BMI 必须先存在、`stdFlags` 必须先指向它,规则才可能 import。 + +- **规则里的 `import mcpp;`。** `host-module = true` 只做了「注册这个模块」,从没把这个包移出消费者的**普通依赖图** —— 于是同一个 `.cppm` 又被当作消费者的一个普通库编译了一遍,而在那次编译里内置 `mcpp` 模块并不存在,报 `fatal error: module 'mcpp' not found`。 + + 一条构建规则是**纯构建期**的东西,本来就不该进消费者的二进制(Cargo 用 `[build-dependencies]` 划的是同一条界线)。现在只经由 root 的 host-module 边到达的包会被清空源码集,不再参与编译与链接;仍会被解析落盘,因为 lib 根要从那里读。 + + **有护栏**:同一个包完全可以既是规则、又是别处(或 root 另一种拼法)的普通库,此时不清空 —— 否则会变成一个离现场很远的 undefined reference。 + +### 文档 + +- `docs/05-mcpp-toml.md` 的示例此前写的是 `import mcpp.rules.protobuf;`,**做不到**:mcpp 用依赖的裸 `package.name` 注册 host 模块,而 SPEC-001 要求 `name` 是单一原子段。随之澄清一条会咬人的约束:规则包的名字必须是**合法 C++ 模块名**(`grpcgen` 可以,`grpc-rules` 不行),否则报的是 `module 'grpc_rules' not found`,不会提示你名字有问题。 + ## [2026.8.5.1] — 2026-08-05 `build.mcpp` 机制的**架构地基**:把「一条指令是什么」收敛成一张表,并补上三个今天就存在的稳定性缺口。架构分析见 `.agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md`(本次实现其中的步 0 与步 1)。 diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 69db02fa..e5c3baf1 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -1026,14 +1026,14 @@ library package and import it: ```toml [dependencies] -"mcpp.rules.protobuf" = { version = "0.1.0", host-module = true } +protobufgen = { version = "0.1.0", host-module = true } ``` ```cpp // build.mcpp import mcpp; -import mcpp.rules.protobuf; -int main() { mcpp::rules::protobuf::generate(/* … */); } +import protobufgen; +int main() { return protobufgen::generate({"schema"}) ? 0 : 1; } ``` mcpp compiles that package's lib-root module **for the host, in the same @@ -1045,9 +1045,25 @@ Rules are therefore versioned, testable and distributable through the package manager you already have, written in **C++** — no second language, which is the whole point of `build.mcpp` existing. +**The module name is the package's `name`.** mcpp registers the host module +under the dependency's bare `package.name` (not `.`), so a +rule package's name has to be a legal C++ module name: `grpcgen` works, +`grpc-rules` does not — the hyphen is fine in a package name and illegal in a +module name, and the mismatch surfaces as `module 'grpc_rules' not found` +rather than as a complaint about the name. + +The lib root must be at `src/.cppm` (or wherever `[lib] path` points); a +missing one is reported as *"host module 'x': no interface unit at …"*. + *Limit:* the rule interface is compiled alone, so it may import `std` and the bundled `mcpp` module, but not a third package. A rule package is a leaf. +*Build-time only:* a `host-module = true` dependency is **not** compiled into +or linked with your target. It exists to run during `build.mcpp` and nowhere +else — the same separation Cargo draws with `[build-dependencies]`. (Before +2026.8.5.2 it was also built as an ordinary library, which made `import mcpp;` +inside a rule fail: the bundled module does not exist in that second compile.) + ## Appendix A. Schema Ownership Principle (admission criteria for new fields) > **Closed syntax, open vocabulary**: whoever owns the parsing semantics defines the keys; whoever owns the domain knowledge defines the values. diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index 3c1ebe58..ddffe660 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -779,14 +779,14 @@ MCPP_TOOL_PROTOBUF_PROTOC=/usr/bin/protoc mcpp build ```toml [dependencies] -"mcpp.rules.protobuf" = { version = "0.1.0", host-module = true } +protobufgen = { version = "0.1.0", host-module = true } ``` ```cpp // build.mcpp import mcpp; -import mcpp.rules.protobuf; -int main() { mcpp::rules::protobuf::generate(/* … */); } +import protobufgen; +int main() { return protobufgen::generate({"schema"}) ? 0 : 1; } ``` mcpp 会把该包的 lib 根模块**为 host 编译,且与 `build.mcpp` 在同一条命令里** —— @@ -796,9 +796,22 @@ mcpp 会把该包的 lib 根模块**为 host 编译,且与 `build.mcpp` 在同 于是规则**有版本、能测试、能通过你已有的包管理器分发**,而且是用 **C++** 写的 —— 不引入第二门语言,这正是 `build.mcpp` 存在的理由。 +**模块名就是包的 `name`。** mcpp 用依赖的裸 `package.name`(而**不是** +`.`)注册这个 host 模块,所以规则包的名字必须是合法的 C++ 模块名: +`grpcgen` 可以,`grpc-rules` 不行 —— 连字符在包名里合法、在模块名里非法,而且报出来 +的是 `module 'grpc_rules' not found`,不会提示你名字有问题。 + +lib 根必须在 `src/.cppm`(或 `[lib] path` 指向的位置);缺失时报 +*"host module 'x': no interface unit at …"*。 + *限制:* 规则接口是单独编译的,因此可以 import `std` 与内置 `mcpp` 模块, 但不能 import 第三个包。规则包按构造是叶子。 +*仅构建期:* `host-module = true` 的依赖**不会**被编进、也不会被链进你的 target。 +它只在 `build.mcpp` 期间运行,别处都不出现 —— 与 Cargo 用 `[build-dependencies]` +划出的是同一条界线。(2026.8.5.2 之前它还会被当作普通库再编一遍,这正是规则里 +`import mcpp;` 失败的原因:在那第二次编译里内置模块并不存在。) + ## 附录 A. Schema 所有权原则(新字段准入标准) > **语法封闭,词汇开放**:谁拥有解析语义谁定义键;谁拥有领域知识谁定义值。 diff --git a/mcpp.toml b/mcpp.toml index 561204f3..c8d63b1e 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.8.5.1" +version = "2026.8.5.2" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index 4944c0dc..f878a4db 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -478,6 +478,22 @@ std::expected run_build_program( bool usesStdCompat = imports_module(srcText, "std.compat"); bool usesStd = usesStdCompat || imports_module(srcText, "std"); + // A rule package's interface is compiled by this same function, so what IT + // imports decides what has to be built just as much as what build.mcpp + // imports. Scanning only build.mcpp made a rule that said `import std;` + // fail with `module 'std' not found` — the std module was never built, + // because the program that triggers the build did not mention it. + for (auto const& [logical, ifacePath] : env.hostModules) { + std::ifstream is(ifacePath); + if (!is) continue; // a missing interface is diagnosed by build_host_module + std::ostringstream ss; ss << is.rdbuf(); + const std::string t = ss.str(); + if (t.find("import mcpp") != std::string::npos) usesModule = true; + if (imports_module(t, "std.compat")) usesStdCompat = true; + if (imports_module(t, "std")) usesStd = true; + } + usesStd = usesStd || usesStdCompat; + // The toolchain's own environment (MSVC's INCLUDE / LIB / VSLANG, which // detection synthesized from the located VC tools + Windows SDK). Needed // by every compile below, the module precompile included. @@ -499,27 +515,6 @@ std::expected run_build_program( mcppModuleObject = std::move(mf->object); } - // #355 step 5: dependency-provided host modules (reusable build rules - // shipped as ordinary packages). Compiled HERE, with `base` and `std_flag` - // — the same flags the build.mcpp compile below gets — because a BMI is - // only usable by a compile that agrees with it. Doing this in a separate - // sub-build would leave that agreement to chance, and disagreement shows - // up as `module X CRC mismatch`, not as a clear error. - std::vector hostModuleObjects; - for (auto const& [logical, ifacePath] : env.hostModules) { - auto hm = build_host_module(bdir, hostCompiler, base, std_flag, tc, - compileEnv, logical, ifacePath, moduleFlags); - if (!hm) return std::unexpected(hm.error()); - for (auto& f : hm->useFlags) { - // GCC's marker is just `-fmodules`, already present when the - // bundled module was built; repeating it is harmless but noisy. - if (std::find(moduleFlags.begin(), moduleFlags.end(), f) - == moduleFlags.end()) - moduleFlags.push_back(f); - } - hostModuleObjects.push_back(std::move(hm->object)); - } - // ── `import std;` in build.mcpp ───────────────────────────────────────── // // mcpp asks projects to `import std;` everywhere and then made their build @@ -606,6 +601,35 @@ std::expected run_build_program( stdObjects.push_back(sm->compatObjectPath.string()); } + // #355 step 5: dependency-provided host modules (reusable build rules + // shipped as ordinary packages). Compiled HERE, with `base` and `std_flag` + // — the same flags the build.mcpp compile below gets — because a BMI is + // only usable by a compile that agrees with it. Doing this in a separate + // sub-build would leave that agreement to chance, and disagreement shows + // up as `module X CRC mismatch`, not as a clear error. + // + // AFTER the std block, and that ordering is load-bearing: a rule may + // `import std;` just as build.mcpp may, and it can only do so once the std + // BMI exists and `stdFlags` names it. Compiling rules first — which is what + // 2026.8.5.1 did — handed them an empty `stdFlags` and failed with + // `module 'std' not found`. + std::vector hostModuleObjects; + for (auto const& [logical, ifacePath] : env.hostModules) { + std::vector use = moduleFlags; + use.insert(use.end(), stdFlags.begin(), stdFlags.end()); + auto hm = build_host_module(bdir, hostCompiler, base, std_flag, tc, + compileEnv, logical, ifacePath, use); + if (!hm) return std::unexpected(hm.error()); + for (auto& f : hm->useFlags) { + // GCC's marker is just `-fmodules`, already present when the + // bundled module was built; repeating it is harmless but noisy. + if (std::find(moduleFlags.begin(), moduleFlags.end(), f) + == moduleFlags.end()) + moduleFlags.push_back(f); + } + hostModuleObjects.push_back(std::move(hm->object)); + } + // `-x c++` is required: the `.mcpp` extension is unknown to the compiler, so // without it the driver hands build.mcpp to the linker as a linker script. std::vector compileArgv = { hostCompiler.string() }; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 18645253..8bfe86ae 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -4026,6 +4026,40 @@ prepare_build(bool print_fingerprint, if (!hit) continue; auto rel = mcpp::manifest::resolve_lib_root_path(depPkg.manifest); hostModulesByConsumer[0].emplace_back(canon, depPkg.root / rel); + + // A build rule is BUILD-TIME ONLY. Registering the module + // is not enough: the package is still an ordinary node of + // the consumer's graph, so its interface was ALSO compiled + // as a normal library and linked into the target. That is + // wrong on its own terms — a rule has no business in the + // consumer's binary — and it made the feature nearly + // unusable, because in that second compile the bundled + // `mcpp` module does not exist: any rule that actually used + // the API it exists to wrap died with + // `fatal error: module 'mcpp' not found` (2026.8.5.1). + // + // Emptying the source globs is how a package is removed + // from the compile set here — the same mechanism the + // feature-gated-sources drop above uses. Resolution is + // untouched: the package still lands on disk, which is + // what `resolve_lib_root_path` just read. + // + // Guarded on the package being reached ONLY from the root's + // host-module edge. A package can legitimately be both a + // rule and a library — for something else in the graph, or + // for the root itself under a second spelling — and + // silently dropping its objects then would surface as an + // undefined reference far from here. + bool hostOnly = true; + for (auto const& e : dependencyEdges) + if (e.dependencyPackageIndex == d + && e.consumerPackageIndex != 0) hostOnly = false; + if (hostOnly) { + auto& dm = packages[d].manifest; + dm.buildConfig.sources.clear(); + dm.buildConfig.featureSources.clear(); + dm.modules.sources.clear(); + } break; } } diff --git a/src/version.cppm b/src/version.cppm index 51d39889..a4eed405 100644 --- a/src/version.cppm +++ b/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.8.5.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.8.5.2"; } // namespace mcpp diff --git a/tests/e2e/189_host_module_rules.sh b/tests/e2e/189_host_module_rules.sh index 1f27ea9b..feea1476 100755 --- a/tests/e2e/189_host_module_rules.sh +++ b/tests/e2e/189_host_module_rules.sh @@ -95,6 +95,51 @@ out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" [[ "$out" == "ANSWER=43" ]] || { echo "FAIL: the edited rule did not take effect: $out"; exit 1; } +# ── a rule that uses the API it exists to wrap ────────────────────────────── +# The two properties below are what make the feature usable for a REAL rule +# rather than one that hand-prints directives, and 2026.8.5.1 had neither: +# +# `import std;` the rule was compiled before the std module was built, so +# it failed with `module 'std' not found`. The scan for +# `import std` read build.mcpp only, so a rule that needed it +# did not even trigger the build. +# `import mcpp;` registering the host module never removed the package from +# the consumer's ORDINARY graph, so the same .cppm was also +# compiled as a normal library — where `mcpp` does not exist. +cd "$TMP" +mv rules/src/rules.cppm rules/src/rules.cppm.bak +cat > rules/src/rules.cppm <<'EOF' +export module rules; +import std; // must be usable: a rule is a normal C++23 module +import mcpp; // must be usable: the typed wrapper is the whole point + +export namespace rules { +inline void banner(const char* macro) { mcpp::define(macro); } +inline void define_answer(int n) { + mcpp::cxxflag(std::format("-DRULE_ANSWER={}", n).c_str()); +} +} +EOF +cd app && rm -rf target +"$MCPP" build > b4.log 2>&1 || { + cat b4.log + echo "FAIL: a rule using import std + import mcpp did not build"; exit 1; } +out="$("$MCPP" run 2>&1 | grep '^ANSWER=' | tail -1)" +[[ "$out" == "ANSWER=42" ]] || { + echo "FAIL: the std/mcpp-based rule's directives did not land: $out"; exit 1; } + +# The rule package must NOT be compiled into the consumer's target. It is +# build-time-only; an object under obj/rules/ means it was also treated as an +# ordinary library, which is what made `import mcpp;` fail in the first place. +if find target -path '*obj/rules/*' -name '*.o' | grep -q .; then + find target -path '*obj/rules/*' -name '*.o' + echo "FAIL: the host-module package was also built as a normal library"; exit 1 +fi + +cd "$TMP" +rm -f rules/src/rules.cppm +mv rules/src/rules.cppm.bak rules/src/rules.cppm + # A missing lib root must say so, not fail three edges later. cd "$TMP" mv rules/src/rules.cppm rules/src/elsewhere.cppm