Skip to content

Commit fbb7c54

Browse files
committed
fix(manifest): 保留前缀内的未知层名,从依赖来是版本差而不是拼写错
⚠️ 这条由生态 CI 实测暴露,而它让层名词表**对已发布的包永远不可扩展**。 `openkal-llvm-runtime` 声明本次新命名的那一层,被上一版引擎读到: error: dependency 'openkal-llvm-runtime': mcpp.toml: error: `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no capability mcpp knows. 保留前缀是闭集,为的是让**拼写错误成为错误而不是一个被静默禁用的行为**。 在解析期直接拒绝,让这个闭集在第二个、没人打算要的意义上也闭上了: 一个声明了「读者发布之后才被命名的层」的包,**整份清单加载不了**。 ── 谁的清单决定答案 ────────────────────────────────────── * **根工程**的清单里出现未知层名 ⇒ **错误**。那是作者自己的拼写, 而他正看着这次构建。 * **依赖**的清单里出现未知层名 ⇒ **警告并忽略该层**。那份清单是对着一个更新的 引擎写的,而「忽略未知的并说出来」正是本引擎对其它每一种未知键已有的做法 (`warn_unknown_xpkg_keys` 的注释:should not fail outright, only tell the user what it ignored)。 ⚠️ 警告放在扫描**全部包**的那个循环里,不放在 `warn_unknown_xpkg_keys`: 后者只走到经索引解析的依赖,而 path / git 依赖自带清单,先前一条警告都收不到。 ⚠️ 这条规定只对**此后的**引擎生效。一个包若要声明某个层名,其使用者的引擎仍须 不早于该层名被引入的版本 —— 本次修的是「从今往后可扩展」,不是追溯。 test: e2e 281 增两格(依赖声明未知层 ⇒ 构建成功且点名被忽略的层; 根工程拼错 ⇒ 报错并列出五个层名);unit 93 passed。 spec: SPEC-002 §5.1 拆成两条;docs/14 中英同步。
1 parent ae34ffb commit fbb7c54

7 files changed

Lines changed: 167 additions & 13 deletions

File tree

docs/14-target-side.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,12 @@ The toolchain family spelling `openkal-llvm` normalises to `llvm`. It named the
288288
same payload and carried a fact about the target side, which the model above
289289
resolves from what packages declare.
290290

291-
An unknown name inside the reserved prefix is an error; an unknown key elsewhere
292-
in a manifest is ignored. A published package therefore continues to load under
293-
an engine predating a key it carries.
291+
An unknown name inside the reserved prefix is an error in the root project's own
292+
manifest and a warning in a dependency's. The first is a misspelling the author
293+
is looking at; the second is a manifest written against a newer engine, and
294+
refusing it would mean the layer vocabulary could never be extended by a
295+
published package. An unknown key elsewhere in a manifest is ignored.
296+
297+
That provision governs future engines only. A package declaring a layer name
298+
still requires its consumers to run an engine no older than the release that
299+
introduced the name.

docs/spec/target-side.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
mcpp:<层名>[=<实现名>]
6363
```
6464

65-
`<层名>` **必须**属于 §1.1 的闭集;不属于时引擎**必须**报错
65+
`<层名>` **必须**属于 §1.1 的闭集;不属于时的处置见 §5.1(根工程报错、依赖警告)
6666
不以 `mcpp:` 开头的条目属于特性系统,引擎**必须**原样透传。
6767

6868
省略 `=<实现名>` 时,该层的实现名取包名。
@@ -159,9 +159,26 @@ mcpp:<层名>[=<实现名>]
159159

160160
### 5.1 未知名字 ✅ 已实现
161161

162-
`mcpp:` 前缀内的未知层名,引擎**必须**报错。
162+
⚠️ **谁的清单决定答案。**
163+
164+
- 出现在**根工程**清单中的未知层名,引擎**必须**报错。
165+
这是作者自己的拼写,而他正看着这次构建。
166+
- 出现在**依赖**清单中的未知层名,引擎**禁止**据此使构建失败;
167+
**必须**警告并忽略该层。该清单是对着一个更新的引擎写的。
168+
163169
清单中其它位置的未知键,引擎**禁止**据此使整份清单加载失败;**应当**警告并忽略。
164170

171+
⚠️ 第二条来自一次实测。在其落地之前,一个声明了新层名的包
172+
**在该层被命名之前发布的每一个引擎上都无法加载**,因此层名词表对已发布的包
173+
永远不可扩展:
174+
175+
error: dependency 'openkal-llvm-runtime': mcpp.toml: error:
176+
`provides = ["mcpp:compiler-runtime=compiler-rt"]` names no
177+
capability mcpp knows.
178+
179+
⚠️ 这条规定只在**未来**的引擎上生效。一个包若要声明某个层名,
180+
其使用者的引擎仍须不早于该层名被引入的版本。
181+
165182
### 5.2 既有拼写 ✅ 已实现
166183

167184
- 能力名 `hosted-standard-library` **必须**继续表示 `c++-abi` 层;

docs/zh/14-target-side.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,10 @@ include_dirs = ["config/picolibc"]
245245
工具链族拼写 `openkal-llvm` 归一为 `llvm`。它命名同一份载荷,
246246
并携带一条关于目标侧的事实,而上述模型从包的声明中解析该事实。
247247

248-
保留前缀内的未知名字是错误;清单中其它位置的未知键被忽略。
249-
因此一个已发布的包在早于其所携带的键的引擎下仍能加载。
248+
保留前缀内的未知名字,在根工程自己的清单中是错误,在依赖的清单中是警告。
249+
前者是作者正看着的一处拼写错误;后者是一份对着更新引擎写成的清单,
250+
拒绝它将意味着层名词表永远不能被一个已发布的包扩展。
251+
清单中其它位置的未知键被忽略。
252+
253+
该规定只约束此后的引擎。一个包若声明某个层名,
254+
其使用者仍须运行不早于该层名被引入的那个版本。

src/build/prepare.cppm

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ namespace mcpp::build {
9999
// should not fail outright, only tell the user what it ignored.
100100
inline void warn_unknown_xpkg_keys(const mcpp::manifest::Manifest& dm,
101101
std::string_view depLabel) {
102+
// ⚠️ A LAYER NAME THIS ENGINE DOES NOT KNOW IS A VERSION GAP, NOT A TYPO,
103+
// WHEN IT ARRIVES FROM A DEPENDENCY.
104+
//
105+
// The reserved `mcpp:` prefix is a closed set so a misspelling cannot
106+
// silently disable a behaviour. Refusing a DEPENDENCY's manifest for it made
107+
// the set closed in a second sense nobody intended: a published package
108+
// could never declare a layer named after the reader was released.
109+
// Ignoring the layer and saying so is what this engine already does for
110+
// every other unknown key, and it is the only response that lets the
111+
// vocabulary grow.
112+
for (auto const& cap : dm.unknownCapabilities) {
113+
mcpp::ui::warning(std::format(
114+
"dependency '{}': `{}` names a target-side layer this mcpp does not "
115+
"know — ignored. A newer mcpp may resolve it; this build proceeds "
116+
"without that layer.", depLabel, cap));
117+
}
102118
for (auto const& key : dm.xpkgUnknownKeys) {
103119
auto suggestion = mcpp::manifest::closest_known_xpkg_key(key);
104120
if (suggestion.empty())
@@ -923,6 +939,21 @@ prepare_build(bool print_fingerprint,
923939
: mcpp::manifest::load(*root / "mcpp.toml");
924940
if (!m) return std::unexpected(m.error().format());
925941

942+
// ⚠️ AND ONLY FOR THE ROOT. A layer name this engine does not know is a
943+
// typo in the manifest the author is looking at, and a version gap in a
944+
// dependency's. The reserved `mcpp:` prefix exists so the first is an error
945+
// rather than a silently disabled behaviour; refusing the second as well
946+
// meant the layer vocabulary could never be extended by a published package
947+
// (`warn_unknown_xpkg_keys` carries that half).
948+
if (!m->unknownCapabilities.empty()) {
949+
auto const& cap = m->unknownCapabilities.front();
950+
auto why = mcpp::targetside::parse_capability(cap);
951+
return std::unexpected(std::format(
952+
"{}: {}", (*root / "mcpp.toml").string(),
953+
why ? std::format("`{}` names no capability mcpp knows.", cap)
954+
: why.error()));
955+
}
956+
926957
// A DISTRIBUTION package is not a source tree, and building "in" one is a
927958
// failure that looks like a success: `interface/` holds declarations whose
928959
// definitions are in the prebuilt archive, so the build compiles the
@@ -5563,6 +5594,20 @@ prepare_build(bool print_fingerprint,
55635594
: std::format("{}@{}", pkg.manifest.package.name,
55645595
pkg.manifest.package.version);
55655596

5597+
// ⚠️ EVERY PACKAGE KIND, NOT ONLY THE ONES WITH AN XPKG
5598+
// DESCRIPTOR. `warn_unknown_xpkg_keys` reaches a dependency
5599+
// resolved through the index; a path or git dependency carries a
5600+
// manifest of its own and reached no warning at all, so a layer
5601+
// this engine does not know went by in silence. This loop sees
5602+
// every package in the graph.
5603+
for (auto const& cap : pkg.manifest.unknownCapabilities) {
5604+
if (&pkg == &packages.front()) continue; // root: already refused
5605+
mcpp::ui::warning(std::format(
5606+
"package '{}': `{}` names a target-side layer this mcpp does "
5607+
"not know — ignored. A newer mcpp may resolve it; this build "
5608+
"proceeds without that layer.", pkgId, cap));
5609+
}
5610+
55665611
for (auto const& entry : pkg.manifest.provides) {
55675612
std::optional<tsd::CapDecl> decl;
55685613
if (auto parsed = tsd::parse_capability(entry); parsed && *parsed)
@@ -5610,10 +5655,10 @@ prepare_build(bool print_fingerprint,
56105655
// one is: a typo would otherwise disable a check silently.
56115656
for (auto const& entry : pkg.manifest.requires_) {
56125657
auto parsed = tsd::parse_capability(entry);
5613-
if (!parsed)
5614-
return std::unexpected(std::format(
5615-
"package '{}': {}", pkgId, parsed.error()));
5616-
if (!*parsed) continue; // not in mcpp's namespace
5658+
// An unknown layer name is reported where the manifest was
5659+
// read — as an error for the root and a warning for a
5660+
// dependency — so it is skipped rather than refused twice.
5661+
if (!parsed || !*parsed) continue;
56175662
requirements.push_back({ pkgId, (*parsed)->layer,
56185663
(*parsed)->interfaceName });
56195664
}

src/manifest/toml.cppm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,15 +471,15 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
471471
if (auto v = doc->get_string_array("package.provides")) {
472472
for (auto const& entry : *v)
473473
if (auto cap = mcpp::targetside::parse_capability(entry); !cap)
474-
return std::unexpected(error(origin, cap.error()));
474+
m.unknownCapabilities.push_back(entry);
475475
m.provides = *v;
476476
}
477477
// [package] requires — validated exactly like `provides`: names under the
478478
// reserved prefix are a closed set, everything else passes through.
479479
if (auto v = doc->get_string_array("package.requires")) {
480480
for (auto const& entry : *v)
481481
if (auto cap = mcpp::targetside::parse_capability(entry); !cap)
482-
return std::unexpected(error(origin, cap.error()));
482+
m.unknownCapabilities.push_back(entry);
483483
m.requires_ = *v;
484484
}
485485
// std-module / std-compat-module / std-module-flags.

src/manifest/types.cppm

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,29 @@ struct Manifest {
890890
// schema evolution is loud in lint instead of invisible.
891891
std::vector<std::string> xpkgUnknownKeys;
892892

893+
// ⚠️ CAPABILITY NAMES INSIDE THE RESERVED `mcpp:` PREFIX THAT THIS ENGINE
894+
// DOES NOT KNOW, AND WHY THEY ARE RECORDED RATHER THAN REFUSED HERE.
895+
//
896+
// The reserved prefix is a closed set so that a misspelled layer name is an
897+
// error instead of a silently disabled behaviour. Refusing at PARSE time
898+
// made the set closed in a second, unintended sense: a package declaring a
899+
// layer added after the reader was released failed to load AT ALL, so the
900+
// vocabulary could never be extended by a published package.
901+
//
902+
// Measured 2026-08-24, `openkal-llvm-runtime` declaring the newly named
903+
// compiler-runtime layer, read by the release before it:
904+
//
905+
// error: dependency 'openkal-llvm-runtime': mcpp.toml: error:
906+
// `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no
907+
// capability mcpp knows.
908+
//
909+
// Whose manifest it is decides the answer. A name in the ROOT project's own
910+
// manifest is the author's to fix and they are looking at the build — an
911+
// error. A name in a DEPENDENCY's manifest was written against a newer
912+
// engine, and the correct response is to ignore the layer and say so, which
913+
// is what this engine already does for every other unknown key.
914+
std::vector<std::string> unknownCapabilities;
915+
893916
Package package;
894917
Language language;
895918
Modules modules;

tests/e2e/281_target_side_rules.sh

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,62 @@ out="$("$MCPP" build 2>&1)" || rc=$?
131131
grep -q "not a layer a package can supply" <<< "$out" || {
132132
echo "the refusal does not say why:"; echo "$out"; exit 1; }
133133

134+
# ── The layer vocabulary must be extensible by a published package ──────────
135+
#
136+
# ⚠️ This one comes from a measurement, and the behaviour it replaces made the
137+
# vocabulary permanently un-extendable:
138+
#
139+
# error: dependency 'openkal-llvm-runtime': mcpp.toml: error:
140+
# `provides = ["mcpp:compiler-runtime=compiler-rt"]` names no
141+
# capability mcpp knows.
142+
#
143+
# A package declaring a newly named layer failed to load under every engine
144+
# released before that layer was named. Whose manifest it is decides the
145+
# answer: the root's is a misspelling the author is looking at, a dependency's
146+
# is a manifest written against a newer engine.
147+
cd ..
148+
mkdir -p future app4/src
149+
cat > future/mcpp.toml <<'TOML'
150+
[package]
151+
namespace = "probe"
152+
name = "from-the-future"
153+
version = "0.1.0"
154+
provides = ["mcpp:quantum-abi=spooky"]
155+
156+
[build]
157+
sources = []
158+
TOML
159+
cat > app4/mcpp.toml <<'TOML'
160+
[package]
161+
name = "app4"
162+
version = "0.1.0"
163+
164+
[dependencies]
165+
from-the-future = { path = "../future" }
166+
TOML
167+
printf 'int main(){ return 0; }\n' > app4/src/main.cpp
168+
cd app4
169+
rc=0
170+
out="$("$MCPP" build 2>&1)" || rc=$?
171+
[[ "$rc" -eq 0 ]] || {
172+
echo "a dependency naming a layer this engine does not know must not fail the build:"
173+
echo "$out"; exit 1; }
174+
grep -q "quantum-abi" <<< "$out" || {
175+
echo "the ignored layer must be named:"; echo "$out"; exit 1; }
176+
177+
# And the same name in the ROOT's own manifest is the author's to fix.
178+
cat > mcpp.toml <<'TOML'
179+
[package]
180+
name = "app4"
181+
version = "0.1.0"
182+
provides = ["mcpp:quantum-abi=spooky"]
183+
TOML
184+
rc=0
185+
out="$("$MCPP" build 2>&1)" || rc=$?
186+
[[ "$rc" -ne 0 ]] || {
187+
echo "a misspelled layer name in the root manifest must be an error:"
188+
echo "$out"; exit 1; }
189+
grep -q "names no capability mcpp knows" <<< "$out" || {
190+
echo "the refusal does not say what is wrong:"; echo "$out"; exit 1; }
191+
134192
echo "OK"

0 commit comments

Comments
 (0)