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
13 changes: 13 additions & 0 deletions .github/tools/check_version_pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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@<unreleased>' 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
Expand Down
19 changes: 18 additions & 1 deletion .github/workflows/ci-aarch64-fresh-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion .xlings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"workspace": {
"mcpp": "2026.8.4.1"
"mcpp": "2026.8.5.1"
}
}
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)。
Expand Down
22 changes: 19 additions & 3 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `<namespace>.<name>`), 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/<name>.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.
Expand Down
19 changes: 16 additions & 3 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 在同一条命令里** ——
Expand All @@ -796,9 +796,22 @@ mcpp 会把该包的 lib 根模块**为 host 编译,且与 `build.mcpp` 在同
于是规则**有版本、能测试、能通过你已有的包管理器分发**,而且是用 **C++** 写的
—— 不引入第二门语言,这正是 `build.mcpp` 存在的理由。

**模块名就是包的 `name`。** mcpp 用依赖的裸 `package.name`(而**不是**
`<namespace>.<name>`)注册这个 host 模块,所以规则包的名字必须是合法的 C++ 模块名:
`grpcgen` 可以,`grpc-rules` 不行 —— 连字符在包名里合法、在模块名里非法,而且报出来
的是 `module 'grpc_rules' not found`,不会提示你名字有问题。

lib 根必须在 `src/<name>.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 所有权原则(新字段准入标准)

> **语法封闭,词汇开放**:谁拥有解析语义谁定义键;谁拥有领域知识谁定义值。
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.8.5.1"
version = "2026.8.5.2"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
66 changes: 45 additions & 21 deletions src/build/build_program.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,22 @@ std::expected<void, std::string> 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.
Expand All @@ -499,27 +515,6 @@ std::expected<void, std::string> 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<fs::path> 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
Expand Down Expand Up @@ -606,6 +601,35 @@ std::expected<void, std::string> 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<fs::path> hostModuleObjects;
for (auto const& [logical, ifacePath] : env.hostModules) {
std::vector<std::string> 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<std::string> compileArgv = { hostCompiler.string() };
Expand Down
34 changes: 34 additions & 0 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion 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.8.5.1";
inline constexpr std::string_view MCPP_VERSION = "2026.8.5.2";

} // namespace mcpp
45 changes: 45 additions & 0 deletions tests/e2e/189_host_module_rules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading