Skip to content

Commit b2c38ca

Browse files
committed
feat(build.mcpp): a missing mcpp:: API now says the engine may be too old
A package that adopts a new typed directive cannot be used by an older mcpp, and until now the only thing the reader saw was error: 'runner' is not a member of 'mcpp' which reads like the package author's typo rather than the reader's engine being out of date. The wire protocol's own answer (protocol_error names `mcpp self update`) never reaches this case: it needs the program to have compiled first. The package cannot solve it either. Measured: if constexpr (requires { mcpp::runner("qemu"); }) // hard error A requires-expression over a qualified name that does not exist is ill-formed, not `false`, so there is no in-language feature probe. So the compile failure carries the hint, matched across all three frontends' spellings and anchored on our namespace so an ordinary error does not get it. This covers every future typed-API addition, not just `runner`. Also corrects docs/07 (both languages), which still claimed an unknown directive "can only be a typo" within one protocol version — the engine's own diagnostic was corrected away from that when link-script landed: the protocol number is stamped by whichever mcpp compiled the program, so two matching numbers say nothing about whether the key is from the future.
1 parent 287e43e commit b2c38ca

5 files changed

Lines changed: 173 additions & 7 deletions

File tree

.agents/docs/2026-08-19-baremetal-phase3-usable-plan.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,32 @@ mcpp::runner("-kernel");
132132
⭐ **决定:先按「什么都不加」发布,并把包级 `requires-mcpp` 作为一个独立探针(P-COMPAT)排在后面。**
133133
理由:诊断已经可行动,而一个会让老客户端硬失败的新键代价高得多 —— 且这个代价**尚未测过**。
134134
135+
#### ⚠️ 实施时探到的:上表第 2 行「诊断可行动」对**类型化 API 是假的**
136+
137+
那条可行动的诊断(`protocol_error()`,点名 `mcpp self update`)只对**wire 键**生效,
138+
而它要求程序**先编译得过**。BSP 用的是 `mcpp::runner(...)`,老 mcpp 上根本编不过,
139+
拿到的是:
140+
141+
```
142+
error: 'runner' is not a member of 'mcpp'
143+
```
144+
145+
—— 读起来像**包作者拼错了**,而不是**读者的引擎旧了**。
146+
147+
我先试了在包里做优雅降级,**实测证伪**:
148+
149+
```cpp
150+
if constexpr (requires { mcpp::runner("qemu"); }) // ✗ 名字不存在 ⇒ 硬错误
151+
```
152+
153+
`requires` 表达式作用在**不存在的限定名**上是 ill-formed,**不是求值为 `false`**
154+
**语言内没有特性探测**,包侧无路可走。
155+
156+
**所以补在引擎侧**:`build.mcpp` 编译失败且错误里出现「不是 `mcpp` 的成员」时,
157+
追加一段点名 `mcpp self update` 并报出当前版本的提示(三个前端三种写法都认)。
158+
**这对以后每一次类型化 API 新增都生效**,不只是 `runner`
159+
判据:`mentions_missing_mcpp_api` 单测钉三种拼写 + 三条不该误报的普通错误。
160+
135161
### 3.3 收录进 `mcpplibs/mcpp-index`
136162

137163
`pkgs/r/riscv-virt-rt.lua``pkgs/m/mcpplibs.std.freestanding.lua`

docs/07-build-mcpp.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,16 +326,44 @@ write it yourself). mcpp uses that two ways:
326326
with an upgrade hint. Continuing would silently drop directives the build
327327
depends on — and "the build succeeded but the flag never arrived" is the
328328
worst class of build bug.
329-
- Because the two sides then provably agree, an **unrecognized directive is an
330-
error** rather than a warning: within one protocol version it can only be a
331-
typo.
329+
- An **unrecognized directive is an error** rather than a warning, and the error
330+
names *both* possible causes. It cannot name one: the protocol number is
331+
stamped by whichever mcpp **compiled** the program, not carried by the
332+
package, so a package written for a newer mcpp arrives at an older one
333+
wearing the older engine's number. Two matching numbers therefore say nothing
334+
about whether the key came from the future.
332335

333336
A `printf`-style program announces nothing, so it keeps the historical
334337
warn-and-ignore behaviour. That surface is **frozen at the eleven directives in
335338
the table above** — it still works and will keep working, but new capabilities
336339
land only in the typed API. Prefer `import mcpp;` for anything intended to
337340
maintain.
338341

342+
#### A package that needs a newer mcpp
343+
344+
When a published package calls a typed function this mcpp does not have, the
345+
compile error naming it is followed by:
346+
347+
```
348+
The `mcpp` build module this engine bundles does not have that name.
349+
Either the package was written for a newer mcpp (try `mcpp self update`;
350+
this is mcpp 2026.8.19.2), or the name is misspelled …
351+
```
352+
353+
The package cannot handle this itself, and it is worth knowing why — the
354+
obvious guard does not compile:
355+
356+
```cpp
357+
if constexpr (requires { mcpp::runner("qemu"); }) // ✗ hard error when absent
358+
mcpp::runner("qemu");
359+
```
360+
361+
A `requires`-expression over a **qualified name that does not exist** is
362+
ill-formed, not `false`. So there is no in-language feature probe, and a
363+
package that adopts a new directive states its floor in prose (its README) and
364+
relies on the diagnostic above. Such a package should name the mcpp version it
365+
requires.
366+
339367
### `import std;` (mcpp 2026.8.2.1+)
340368
341369
A `build.mcpp` may `import std;` (and `import std.compat;`), alone or together

docs/zh/07-build-mcpp.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,13 +290,36 @@ mcpp 会播下一个带着该声明的占位文件,使 prepare 期的扫描与
290290

291291
- 程序声明的协议**高于** mcpp 所理解的 → **拒绝执行**,并给出升级提示。继续跑会
292292
静默丢掉构建依赖的指令,而「构建成功了但那个 flag 根本没到」是最难查的一类问题。
293-
- 既然双方已被证明一致,**未知指令就是错误**而不是警告:在同一个协议版本内,
294-
它只可能是拼写错误。
293+
- **未知指令是错误**而不是警告,而且这条错误会把**两种可能的原因都说出来**
294+
它没法只说一种:协议号是由**编译**该程序的那个 mcpp 现场打上的,并不由包本身携带
295+
—— 于是一个写给新 mcpp 的包到了老 mcpp 手里,身上戴的是老引擎的号。
296+
**两个号一致因此完全不能说明这个键是不是来自未来。**
295297

296298
`printf` 风格的程序什么都不声明,因此保留历史上的「警告并忽略」行为。这一面
297299
**冻结在上表的 11 条指令**上——它仍然能用、也会继续能用,但新能力只在类型化 API 里
298300
落地。**要长期维护的程序请用 `import mcpp;`**
299301

302+
#### 一个需要更新 mcpp 的包
303+
304+
当已发布的包调用了当前 mcpp 没有的类型化函数时,点名的编译错误之后会跟着:
305+
306+
```
307+
The `mcpp` build module this engine bundles does not have that name.
308+
Either the package was written for a newer mcpp (try `mcpp self update`;
309+
this is mcpp 2026.8.19.2), or the name is misspelled …
310+
```
311+
312+
**包自己处理不了这件事**,而原因值得知道 —— 最直觉的那道防护编译不过:
313+
314+
```cpp
315+
if constexpr (requires { mcpp::runner("qemu"); }) // ✗ 名字不存在时是硬错误
316+
mcpp::runner("qemu");
317+
```
318+
319+
`requires` 表达式作用在一个**不存在的限定名**上时是 ill-formed,而**不是求值为
320+
`false`**。所以语言内没有特性探测这条路:采用了新指令的包只能在自己的 README 里
321+
用文字写明版本下限,并依赖上面那条诊断。**这类包应当写清楚它需要哪个版本的 mcpp。**
322+
300323
### `import std;`(mcpp 2026.8.2.1+)
301324
302325
`build.mcpp` 可以 `import std;`(以及 `import std.compat;`),单用或与

src/build/build_program.cppm

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import mcpp.toolchain.registry; // archive_tool
2828
import mcpp.toolchain.stdmod; // ensure_built — the SAME std BMI the main build uses
2929
import mcpp.toolchain.triple; // host_triple (MCPP_HOST contract value)
3030
import mcpp.ui;
31+
import mcpp.version; // MCPP_VERSION — the hint names the engine the reader is on
3132

3233
export namespace mcpp::build {
3334

@@ -94,6 +95,30 @@ inline std::string xpkg_env_var(std::string_view ns, std::string_view name) {
9495
return out;
9596
}
9697

98+
// Does a compiler's output say the program asked for something the bundled
99+
// `mcpp` module does not have?
100+
//
101+
// ⚠️ This is the ONLY place an engine-too-old situation can be caught for the
102+
// TYPED api, and it exists because the in-language probe does not:
103+
//
104+
// if constexpr (requires { mcpp::runner("x"); }) // ← hard error,
105+
// mcpp::runner("x"); // measured
106+
//
107+
// A requires-expression over a qualified name that does not exist is
108+
// ill-formed, not `false`, so a package CANNOT degrade gracefully across mcpp
109+
// versions the way it could across, say, a header's feature macro. The wire
110+
// protocol has its own answer for this (protocol_error() names `mcpp self
111+
// update` for an unknown `mcpp:` key), but that answer needs the program to
112+
// have COMPILED — and a package written against a newer mcpp does not get
113+
// that far. So the raw compiler error is the message, and on its own it says
114+
// only `'runner' is not a member of 'mcpp'`, which reads like the author's
115+
// typo instead of the reader's out-of-date engine.
116+
//
117+
// Deliberately spelling-based, and deliberately broad across the three
118+
// frontends (they phrase it three ways). A false positive costs one extra
119+
// hint line under a genuine typo; a false negative costs a user an afternoon.
120+
bool mentions_missing_mcpp_api(std::string_view compilerOutput);
121+
97122
// Compile + run `<root>/build.mcpp` (if present) with `hostCompiler` (the resolved
98123
// HOST frontend — under a cross --target the caller resolves a host toolchain;
99124
// the program always compiles AND runs on the host) and apply its directives to
@@ -128,6 +153,28 @@ bool glob_inputs_stale(const std::filesystem::path& projectRoot);
128153

129154
namespace mcpp::build {
130155

156+
// See the declaration for why this exists at all.
157+
//
158+
// Three frontends, three spellings of the same fact — and MSVC's does not even
159+
// contain the word "member" in the same order, so each is matched literally
160+
// rather than by a shared substring:
161+
//
162+
// gcc error: 'runner' is not a member of 'mcpp'
163+
// clang error: no member named 'runner' in namespace 'mcpp'
164+
// cl.exe error C2039: 'runner': is not a member of 'mcpp'
165+
//
166+
// The trailing `'mcpp'` is what keeps this off unrelated failures: a package's
167+
// own missing symbol names its own namespace, not ours.
168+
bool mentions_missing_mcpp_api(std::string_view out) {
169+
static constexpr std::string_view kNeedles[] = {
170+
"is not a member of 'mcpp'", // gcc, and cl.exe's tail
171+
"in namespace 'mcpp'", // clang
172+
};
173+
for (auto n : kNeedles)
174+
if (out.find(n) != std::string_view::npos) return true;
175+
return false;
176+
}
177+
131178
namespace {
132179

133180
namespace fs = std::filesystem;
@@ -778,8 +825,21 @@ std::expected<void, std::string> run_build_program(
778825
auto cres = mcpp::platform::process::capture_exec(compileArgv, compileEnv,
779826
compileCwd);
780827
if (cres.exit_code != 0) {
781-
return std::unexpected(std::format(
782-
"build.mcpp failed to compile (exit {}):\n{}", cres.exit_code, cres.output));
828+
std::string msg = std::format("build.mcpp failed to compile (exit {}):\n{}",
829+
cres.exit_code, cres.output);
830+
if (mentions_missing_mcpp_api(cres.output)) {
831+
msg += std::format(
832+
"\n The `mcpp` build module this engine bundles does not have "
833+
"that name.\n"
834+
" Either the package was written for a newer mcpp (try "
835+
"`mcpp self update`;\n"
836+
" this is mcpp {}), or the name is misspelled — the compiler "
837+
"cannot tell\n"
838+
" the two apart, because the module is generated by whichever "
839+
"mcpp is running.",
840+
mcpp::MCPP_VERSION);
841+
}
842+
return std::unexpected(std::move(msg));
783843
}
784844

785845
// ── Run it; capture stdout(+stderr) and parse directives ────────────────

tests/unit/test_freestanding.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,35 @@ TEST(XpkgEnvVar, BothSpellingsAreDerivedFromOneSanitizer) {
228228
"MCPP_XPKG_PICOLIBC_RISCV_DIR");
229229
}
230230

231+
// ── engine floor: a package that needs a newer mcpp ─────────────────────────
232+
233+
TEST(BuildProgramCompatHint, RecognisesAllThreeFrontendSpellings) {
234+
using mcpp::build::mentions_missing_mcpp_api;
235+
// ⚠️ Measured, not assumed: `if constexpr (requires { mcpp::runner("x"); })`
236+
// is a HARD ERROR when the name is absent, so a package cannot probe for a
237+
// newer API in-language. The compiler's error IS the compat channel, and
238+
// these are the three ways it arrives.
239+
EXPECT_TRUE(mentions_missing_mcpp_api(
240+
"build.mcpp:57:11: error: 'runner' is not a member of 'mcpp'")); // gcc
241+
EXPECT_TRUE(mentions_missing_mcpp_api(
242+
"build.mcpp:57:11: error: no member named 'runner' in namespace 'mcpp'"));// clang
243+
EXPECT_TRUE(mentions_missing_mcpp_api(
244+
"build.mcpp(57): error C2039: 'runner': is not a member of 'mcpp'")); // cl.exe
245+
}
246+
247+
TEST(BuildProgramCompatHint, StaysOffFailuresThatAreNotAboutOurApi) {
248+
using mcpp::build::mentions_missing_mcpp_api;
249+
// The hint says "your mcpp may be too old". Attaching that to an ordinary
250+
// compile error would send the reader to the wrong place, so the match is
251+
// anchored on OUR namespace — a package's own missing symbol names its own.
252+
EXPECT_FALSE(mentions_missing_mcpp_api(
253+
"build.mcpp:12:5: error: 'runner' is not a member of 'board'"));
254+
EXPECT_FALSE(mentions_missing_mcpp_api(
255+
"build.mcpp:3:1: error: expected ';' after top level declarator"));
256+
EXPECT_FALSE(mentions_missing_mcpp_api(
257+
"build.mcpp:9:9: error: use of undeclared identifier 'mcpp_runner'"));
258+
}
259+
231260
// ── artifact set ───────────────────────────────────────────────────────────
232261

233262
TEST(FreestandingArtifacts, SizeOutputIsParsedNotPassedThrough) {

0 commit comments

Comments
 (0)