fix: 六处「答案已记录、做决定的代码不读它」+ 明确拒绝 system 工具链 (2026.8.30.2) - #538
Merged
Sunrisepeak merged 5 commits intoAug 30, 2026
Conversation
…2026.8.30.2) Analysis, measurements and design: `.agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md`. Two families. A record exists and the code that decides does not read it; or the runtime search path mixes what was declared with what this machine happens to have installed. The host-dependence policy that settles every severity here is stated once in that document's §1.2 and in docs/03: mcpp and everything the ecosystem publishes depend on no host, a user's own project may and that choice is theirs to guarantee, and the boundary between a warning and a refusal is whether the result builds and runs. `mcpp test` re-derived an unchanged answer on every invocation (#529). Both post-link ELF passes were written with a read-back keyed on the artifact's stat, and `prepare_build` rewrites `resolution.json` from a fresh json object at the start of every run, carrying neither record — so each invocation began by deleting the memo its own backend was about to look for. The records move to `.mcpp-runtime-verdicts.json`, which survives, and `resolution.json` keeps publishing a copy, which is the shape `sync_resolution_verdict` already had. Pruning now asks whether the artifact left the DISK rather than whether it left the current command's plan: `build` and `test` share one output directory with different link-unit sets and were deleting each other's verdicts. The record's invalidation key gains the SubOS farm stamp and MCPP_ALLOW_HOST_LIBS, because making a memo durable creates a staleness obligation that did not exist while the answer was recomputed. Measured on ten link units, all warm: `mcpp test` after a `mcpp build` 3.15s -> 0.36s. A new source file in a `path` dependency was invisible to the fast path, which swept only the project being built. `mcpp build` printed `Finished dev in 0.00s` and the module was never compiled. Content edits were caught, but by the post-link snapshot rather than by the sweep. This is #359's shape in a directory that fix did not reach, and workspace members depend on each other by `path`, so it is not an edge case. The dependency source roots are recorded in `.build_cache` and swept, manifest included. `[toolchain] system` with a `build.mcpp` died as `posix_spawnp('') failed` (#527 Bug 1). The resolved compiler path was in `tc->binaryPath` the whole time and was not handed to the build.mcpp closure. This fills an unset variable; it adds no host capability, and the same manifest without a build.mcpp already built. The host-dependence warning states the cost once per build and names the xim route. `standard = 26` — the spelling #527 uses in three of its own examples — was silently ignored, because the key is documented as a string and `get_string` returns nothing for a bare integer. Both spellings are now accepted. `[workspace.package]` and `[workspace.build]` (#527 Bug 2, RFC 3). The workspace root's `[build]` reached no member. Scalars are inherited when the member did not DECLARE the key, vectors append workspace-first, and "declared" is recorded by both parse paths rather than inferred by comparing against the default — a member pinning `standard = "c++23"` under a c++26 workspace must keep it, and that is the same bytes as the default. This is the precondition the cpp20 design doc's §9-Q3 wrote down and deferred. Both inheritance sites now call one function. `allow_host_libs` is refused there: it turns a correctness gate off, and a root able to set it once would disable it for members added later by someone who never read that file. A dependency declaring a standard above the graph's is now reported instead of silently discarded, degraded and promoted by --strict, and scoped to manifests the author controls: every index descriptor with an mcpp segment declares `language` (782 of 782 measured locally, 756 of 774 being C libraries carrying a boilerplate "c++23"), so declaredness does not mean authorship there. A dialect-class flag in `cxxflags` that never reaches the `import std` prebuild is refused before compiling, naming `dialect_cxxflags`. Read from the effective flag set, so a profile or target block is covered too; silent when nothing in the graph imports std, and scoped to the root package because a dependency carrying the flag may legitimately not import std at all. Tests: tests/e2e/321..326, each with the negative case that keeps the check honest. Docs: 03, 05, 06 in both languages.
The host-dependence rule is not uniform across axes, and the split is the point rather than an inconsistency. THE TOOLCHAIN IS MCPP'S OWN CONTRACT. Everything mcpp promises — that `import std` is available, that the runtime closure is computable, that two machines and CI produce the same build — is a statement about a compiler mcpp resolved and can identify. A compiler taken from PATH makes every one of those unverifiable, so `[toolchain] … = "system"` is refused rather than warned about. `msvc@system` is the single exception and is a different spelling: it names a FAMILY whose installation mcpp locates, on the one platform where the compiler cannot be redistributed. THE LIBRARIES A PROGRAM LINKS ARE THE PROGRAM'S BUSINESS. A project may link a host library or its own `.so`; mcpp names the supported route — declare the provider so it resolves from mcpp-index, contribute the package if the index does not carry it yet — and does not refuse while the result builds and runs. The developer owns the artifact and guarantees it. This replaces the previous commit's treatment of #527 Bug 1, which filled in the resolved compiler path and warned. The crash it removed was real — `posix_spawnp('') failed (error 2)` as soon as the project had a build.mcpp — but a refusal that arrives as a crash three layers down is not a policy, it is a bug wearing one. The refusal now fires during toolchain resolution, before anything tries to compile the build program, and says what to write instead. Three existing tests referenced the escape hatch and each needed a different answer: 14_toolchain_fallback asserted only that `system` did NOT produce "no toolchain configured". That predicate stays satisfied by any other error, so the test went on passing while its stated intent inverted — a negative-only assertion cannot tell "it worked" from "it failed differently". Both halves are checked now. 293_…_name_one_os used `system` to point a Linux compiler at a Windows target. The refusal fires first, so the test began taking its skip branch — and its own header says a skip there has to be earned or the test cannot see a revert. The refusal is now an accepted PASS branch with its own reason, because the invariant holds by a stronger mechanism: that door is closed entirely. 105_asm_sources_nasm genuinely unaffected; its broken-MCPP_HOME bootstrap error still fires first. Verified, not assumed. 325 is rewritten accordingly, and asserts the refusal reaches the user before the build program starts, that it fires for the environment side channel too, that it names the msvc@system exception and the library axis, and — the denominator — that a project with no `[toolchain]` at all still builds. `mcpp.diag`'s host-route helper is reverted: with the toolchain axis refusing rather than warning, and the library-provenance work not in this change, it had no consumer. Shipping an unread field is the defect this branch is about. Also fixes the version constant: `modules/versioning/src/version.cppm` is the second source of truth `check_version_pins.sh` enforces, and CI caught it — that mismatch is what failed e2e on all three platforms and the Windows `SubsystemContracts.TheBinaryVersionMatchesTheRootManifest`.
…reference doc The design document argued a single boundary — "does it build and run" — for every host dependency, and D15 followed it to "warn, do not refuse". §1.2 is corrected to state the rule per axis: the toolchain is mcpp's contract and is refused, the libraries a program links are the program's own and stay a warning. §7, §9, §10, §11.7, §12 and the review record follow. The measurement behind the earlier conclusion was right and stays in the document — `[toolchain] system` does build a project using `import std`. What was wrong was carrying it across an axis boundary, which is the failure this document keeps finding from the other side. Also drops a second-person sentence from docs/06 that check_docs_style.sh refuses in a reference doc.
Found by re-reading the merge, not by a failure. `[workspace.build] include_dirs = ["shared/inc"]` was prepended to each member verbatim, so every member resolved it against its OWN directory — looking for `<member>/shared/inc` for a directory that lives at `<workspace>/shared/inc`. This is #224 for a new key. `[indices].path` and `[workspace.dependencies] path` are anchored to the workspace root for exactly this reason, and a third relative-path key that skipped it fails as a missing header three members deep, naming neither the manifest that declared it nor the root it was written against. Anchored rather than refused: `expandIncludeDirs` already accepts an absolute include directory, so the anchored form needs nothing downstream. 321 now includes a header from the workspace root, so the anchoring has an assertion rather than a comment.
The ecosystem job runs this file and checks that each test "ran to its conclusion" — an assertion that exists so an early exit cannot masquerade as a pass. My branch for the toolchain refusal did `exit 0`, which skipped half two entirely: four correct cross builds that this file also guards. CI caught it; a local run did not, because locally the exit code is all a caller sees. The branch now records that half one is settled and lets the script continue. The "names both systems" assertion is asked only of the OS-mismatch refusal — the toolchain refusal is a different sentence about a different decision, one that never resolved a target at all, and demanding both triples from it would be asserting on the wrong object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
分析、量化与设计:
.agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md。每一条都有复现,数字都是实测的。
关闭 #529,修复 #527 的 Bug 1 / Bug 2 / RFC 2 / RFC 3。
host 依赖的规则按轴分,这个分叉是刻意的
import std可用、闭包可计算、"同一份构建在别的机器和 CI 上一致",全都是关于一个 mcpp 解析得出、叫得出名字的编译器的陈述;PATH上的编译器让这些全部无法核验。msvc@system是唯一例外(它点名一个族,mcpp 定位其安装).so;mcpp 说明代价并指出 mcpp-index 那条路(索引没有就贡献一个),但只要能构建能运行就不拦。产物是开发者的,由他保证库这条轴内部仍沿用旧判据:证明跑不起来才拒绝(现有的闭包检查不变)。
修复
1.
mcpp test每次调用都在重算一份没变的答案(#529)两个 post-link ELF pass 都写了读回优化,而
prepare_build每次调用都用一个全新的json 对象重写
resolution.json—— 里面没有这两条记录。每次调用一开始就删掉了自己后端待会儿要找的备忘录。
.mcpp-runtime-verdicts.json(它活得过prepare_build),resolution.json继续发布副本 —— 这正是
sync_resolution_verdict已有的形态;build与test共用一个输出目录而 link unit 集合不同,前一个判据让两条命令互删对方的记录;
.xlings.json戳与MCPP_ALLOW_HOST_LIBS—— 让备忘录持久化,就产生了一条以前不存在的正确性义务。
实测(10 个 link unit × 11.5MB,全热):
mcpp build -p <member>mcpp test -p <member>(紧接 build)mcpp test -p <member>(连续)2.
path依赖里新增源文件,fast path 看不见(没有人报过)内容改动能被抓到不是靠扫描,是靠 ninja 重链后的事后放弃。这是 #359 那条形态在它当年
没覆盖到的目录里 —— workspace 成员之间就是
path依赖,所以不是边角情况。3.
[toolchain] system现在被明确拒绝(#527 Bug 1)它此前配合
build.mcpp会死在posix_spawnp('') failed (error 2)。一条以崩溃形式出现的"拒绝"不是政策,是穿着政策外衣的 bug。
拒绝在读取工具链声明处触发(早于任何编译),消息给出该写什么、去哪看可选项,并点明
msvc@system例外与「host 库是另一条轴」。14_toolchain_fallback293_…_name_one_ossystem把 Linux 编译器指向 Windows 目标;拒绝先触发,于是它开始走 skip 分支 —— 而它自己的注释写着「skip 必须挣来,否则测试看不见回退」105_asm_sources_nasmMCPP_HOME的 bootstrap 错误仍先触发)4.
standard = 26(不带引号)被静默忽略get_string对裸整数返回空 ⇒ 按默认档位编译、零诊断。#527 自己的三处示例就是这么写的。5.
[workspace.package]/[workspace.build](#527 Bug 2 + RFC 3)标量按「成员声明过就成员优先」继承,向量按 workspace 在前追加。「声明过」是解析时
记录的事实,不是与默认值比较的推断 —— 成员刻意写
standard = "c++23"必须保住,而那与默认值同为一串字节。这正是 cpp20 设计文档 §9-Q3 记下并推迟的前置条件。
allow_host_libs明确不可继承;不认识的键拒绝而不是忽略。6. 依赖声明了高于当前图的 standard 时会说出来
degraded,
--strict提升,只对工程作者自己拥有的 manifest 生效:索引里带 mcpp 段的描述符 782/782 全都声明了
language,其中 756/774 是import_std = false的 C 库带的样板值。
7. 方言标志没进
import std预编译时,编译前拒绝读生效后的标志集合;图中没有
import std时不触发;只对根包生效(依赖带着该标志却不 import std 是合法的)。
-fno-exceptions/-fno-rtti仍然不自动提升。测试
tests/e2e/321..326,每条都带让检查保持诚实的否定用例与分母。本机 96 unit 全过;e2e 全量跑过(唯一红的
168用已发布的 mcpp 跑同样红 —— 本机musl 工具链是 gcc 13.3.0,无 std module,与本 PR 无关)。