Skip to content

Commit 16a1014

Browse files
committed
fix: refuse [toolchain] system; only mcpp-managed toolchains build
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`.
1 parent 49f875e commit 16a1014

11 files changed

Lines changed: 329 additions & 222 deletions

CHANGELOG.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
完整分析、量化与设计见
1313
[`.agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md`](.agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md)
1414

15-
> **host 依赖的分界是「能不能构建并运行」,不是「干不干净」。** mcpp 自身与生态
16-
> 发布的一切都不依赖 host;用户自己的工程可以另做选择,那是他自己保证的事 ——
17-
> mcpp 提示并指出受支持的路径,但不强行拒绝。而「证明跑不起来」仍然是错误。
15+
> **host 依赖的规则按轴分,而这个分叉是刻意的。** **工具链属于 mcpp 的契约**:
16+
> `import std` 可用、闭包可计算、同一份构建在别的机器和 CI 上一致,都是关于
17+
> 「一个 mcpp 解析得出、叫得出名字的编译器」的陈述,所以 `[toolchain] = "system"`
18+
> **明确拒绝**(`msvc` 是唯一例外)。**而程序链接哪些库是程序自己的事**:
19+
> 工程可以链 host 的库或自己的 `.so`,mcpp 说明代价并指出 mcpp-index 那条路,但不拒绝。
1820
1921
### 修复
2022

@@ -46,10 +48,14 @@
4648
("glob 输入变了而现存文件的 mtime 一个没动")在它当年没有覆盖到的目录里。
4749
workspace 成员之间就是 `path` 依赖,所以这不是边角情况。
4850

49-
- **`[toolchain] system` 配合 `build.mcpp` 直接崩溃(#527 Bug 1)。**
50-
`posix_spawnp('') failed (error 2)`。解析出的编译器绝对路径一直在 `tc->binaryPath`
51-
里,只是没有交给 `build.mcpp` 的编译闭包。**修的是一个没赋值的变量,不是加 host
52-
支持** —— 同一份 manifest 去掉 `build.mcpp` 本来就能构建。
51+
- **`[toolchain] system` 现在被明确拒绝,而不再崩溃(#527 Bug 1)。**
52+
它此前配合 `build.mcpp` 会死在 `posix_spawnp('') failed (error 2)` —— 一条以崩溃形式
53+
出现的"拒绝"不是政策,是穿着政策外衣的 bug。
54+
55+
**mcpp 只用它自己管理的工具链构建。** `PATH` 上的编译器无法被识别、无法被复现,于是
56+
`import std` 可用性、运行期闭包、"同一份构建在另一台机器上"全都不再是 mcpp 能承诺的
57+
东西。拒绝消息给出该写什么、去哪看可选项,并点明 `msvc`**唯一例外**
58+
(它点名的是一个族,mcpp 定位其安装),同时说明**host 库是另一条轴,不在拒绝之列**
5359

5460
- **`standard = 26`(不带引号)被静默忽略。** 键被文档写成字符串,而 `get_string`
5561
裸整数返回空,于是工程按默认档位编译、零诊断。#527 自己的三处示例就是这么写的。

docs/03-toolchains.md

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -305,45 +305,54 @@ Pinned toolsets coexist with each other and with a system Visual Studio.
305305
> that may have been intended. (The family-less `[toolchain] … = "system"` — the PATH
306306
> compiler — is a separate and deliberate escape hatch, and is unaffected.)
307307
308-
### `[toolchain] … = "system"`the PATH compiler
308+
### `[toolchain] … = "system"`refused
309309

310-
The escape hatch: mcpp uses whatever C++ compiler is on `PATH` instead of
311-
resolving one from the xim index. It works for the whole build, `build.mcpp`
312-
included.
313-
314-
It is not recommended, and mcpp says so once per build:
310+
**mcpp builds only with toolchains it manages.** A compiler taken from `PATH` is
311+
not supported, and the configuration is refused rather than warned about:
315312

316313
```
317-
warning: [toolchain] system selects a compiler from PATH
318-
impact: this build depends on what this host has installed, so it is not
319-
reproducible on another machine and cannot be checked by CI
320-
hint: mcpp resolves its own toolchains from the xim index so that
321-
`import std` and the runtime closure are guaranteed on every host;
322-
declare one with [toolchain] linux = "gcc@16.1.0" (or
323-
`mcpp toolchain default`) to get the same compiler here, on a
324-
teammate's machine and in CI
314+
error: [toolchain] linux = "system" is not supported: mcpp builds only with
315+
toolchains it manages.
316+
A compiler taken from PATH cannot be identified or reproduced, so
317+
`import std` availability, the runtime closure and "the same build on
318+
another machine" all stop being things mcpp can promise.
319+
Name one instead — mcpp installs it on first use:
320+
321+
[toolchain]
322+
linux = "gcc@16.1.0"
323+
324+
or set a machine default with `mcpp toolchain default gcc@16.1.0`, and
325+
see `mcpp toolchain list` for what is available.
325326
```
326327

327-
It is a warning and not a refusal, and that division is the general rule for
328-
host dependence in mcpp:
328+
`msvc@system` is **the one exception** and is a different spelling: it names a
329+
*family* whose installation mcpp locates and identifies, on the one platform
330+
where the compiler cannot be redistributed. See the section above.
331+
332+
#### Why the toolchain and the libraries get different answers
333+
334+
mcpp's rule about host dependence is not uniform across axes, and the split is
335+
deliberate:
329336

330337
- **mcpp itself, and everything the mcpp ecosystem publishes, depends on no
331-
host.** Toolchains, payloads and libraries come through xlings — the xim index
332-
or mcpp-index. This is what makes a build reproducible across machines and
333-
Linux distributions.
334-
- **A user's own project may choose otherwise, and that choice is theirs to
335-
guarantee.** mcpp warns and names the supported route; it does not refuse, as
336-
long as the result builds and runs. `--strict` turns the warning into an
337-
error, which is how a CI job enforces "no host dependencies" for a repository
338-
that wants that.
339-
340-
A build that provably *cannot* run is a different matter and stays an error: a
341-
runtime closure that cannot be satisfied is refused, because the artifact will
342-
not start. See [binary distribution](12-binary-distribution.md).
343-
344-
The same wording appears wherever a host dependency is taken — a host library on
345-
the link line points at mcpp-index instead, and notes that contributing the
346-
package there is the supported route when the index does not carry it yet.
338+
host.** Toolchains and payloads come through xlings — the xim index or
339+
mcpp-index. This is what makes a build reproducible across machines and Linux
340+
distributions.
341+
- **The toolchain is part of that contract, so it is not the project's to take
342+
from the host.** Everything mcpp promises — `import std` availability, a
343+
computable runtime closure, the same build on a teammate's machine and in CI
344+
— is a statement about a compiler mcpp resolved and can name. A `PATH`
345+
compiler makes all of it unverifiable, which is why this one is a refusal.
346+
- **The libraries a program links are the program's own business.** A project
347+
may link a host library or its own `.so`. mcpp says what that costs and names
348+
the supported route — declare the provider so it resolves from mcpp-index, and
349+
if the index does not carry it yet, contributing the package is the path — but
350+
it does not refuse, as long as the result builds and runs. The developer owns
351+
the artifact and guarantees it.
352+
353+
A build that provably *cannot* run stays an error on either axis: a runtime
354+
closure that cannot be satisfied is refused, because the artifact will not
355+
start. See [binary distribution](12-binary-distribution.md).
347356

348357
### `msvc@system` — the machine's own Visual Studio
349358

docs/zh/03-toolchains.md

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -284,37 +284,46 @@ pinned toolset 之间、以及与系统 Visual Studio 之间都可以共存。
284284
> `[toolchain] … = "system"` —— 即 PATH 上的编译器 —— 是另一套、也是有意保留的
285285
> 逃生口,不受影响。)
286286
287-
### `[toolchain] … = "system"` —— PATH 上的编译器
287+
### `[toolchain] … = "system"` —— 拒绝
288288

289-
逃生口:mcpp 使用 `PATH` 上现成的 C++ 编译器,而不从 xim 索引解析一个。整个构建都走它,
290-
`build.mcpp` 也不例外。
291-
292-
不推荐,而且 mcpp 每次构建会明说一次:
289+
**mcpp 只用它自己管理的工具链构建。** `PATH` 上现成的编译器不受支持,该配置会被**拒绝**,
290+
而不是提示:
293291

294292
```
295-
warning: [toolchain] system selects a compiler from PATH
296-
impact: this build depends on what this host has installed, so it is not
297-
reproducible on another machine and cannot be checked by CI
298-
hint: mcpp resolves its own toolchains from the xim index so that
299-
`import std` and the runtime closure are guaranteed on every host;
300-
declare one with [toolchain] linux = "gcc@16.1.0" (or
301-
`mcpp toolchain default`) to get the same compiler here, on a
302-
teammate's machine and in CI
293+
error: [toolchain] linux = "system" is not supported: mcpp builds only with
294+
toolchains it manages.
295+
A compiler taken from PATH cannot be identified or reproduced, so
296+
`import std` availability, the runtime closure and "the same build on
297+
another machine" all stop being things mcpp can promise.
298+
Name one instead — mcpp installs it on first use:
299+
300+
[toolchain]
301+
linux = "gcc@16.1.0"
302+
303+
or set a machine default with `mcpp toolchain default gcc@16.1.0`, and
304+
see `mcpp toolchain list` for what is available.
303305
```
304306

305-
它是 warning 而不是拒绝。这条分界就是 mcpp 对待 host 依赖的总规则:
307+
`msvc@system`**唯一的例外**,而且是另一种拼法:它点名的是一个****,mcpp 负责定位并识别
308+
其安装 —— 那是唯一一个编译器不能被重新分发的平台。见上一节。
306309

307-
- **mcpp 自身、以及 mcpp 生态发布的一切,都不依赖任何 host。** 工具链、payload、库都经由
308-
xlings 获得 —— xim 索引或 mcpp-index。这正是构建能跨机器、跨 Linux 发行版复现的原因。
309-
- **用户自己的工程可以另做选择,而这个选择由他自己保证。** mcpp 提示并指出受支持的路径,
310-
但只要结果能构建、能运行,就不强行拒绝。`--strict` 会把该提示变成错误 —— 需要"零 host
311-
依赖"的仓库,CI 就用它来强制。
310+
#### 为什么工具链与库得到的答案不同
312311

313-
而"证明跑不起来"的构建是另一回事,仍然是错误:运行期闭包不可满足时会被拒绝,因为产物根本
314-
起不来。见[二进制分发](12-binary-distribution.md)
312+
mcpp 对 host 依赖的规则并不是各条轴统一的,这个分叉是刻意的:
315313

316-
同样的措辞会出现在每一处取用 host 的地方 —— 链接行上的 host 库会改为指向 mcpp-index,并说明
317-
索引尚未收录时,**把包贡献进 mcpp-index** 才是受支持的路径。
314+
- **mcpp 自身、以及 mcpp 生态发布的一切,都不依赖任何 host。** 工具链与 payload 都经由
315+
xlings 获得 —— xim 索引或 mcpp-index。这正是构建能跨机器、跨 Linux 发行版复现的原因。
316+
- **工具链属于这份契约,所以它不是工程可以从 host 拿的东西。** mcpp 承诺的每一件事 ——
317+
`import std` 可用、运行期闭包可计算、同一份构建在同事机器上和 CI 里一致 —— 都是关于
318+
**一个 mcpp 解析出来、叫得出名字的编译器**的陈述。`PATH` 上的编译器让这些全部无法核验,
319+
这就是这一条是拒绝的原因。
320+
- **程序链接哪些库,是程序自己的事。** 工程可以链 host 的库,也可以链自己的 `.so`。mcpp 会
321+
说明这样做的代价,并指出受支持的路径 —— 声明该 provider 让它从 mcpp-index 解析;索引尚未
322+
收录时,**把包贡献进 mcpp-index** 就是那条路 —— 但只要结果能构建、能运行,就不强行拒绝。
323+
产物是开发者的,由他保证。
324+
325+
而"证明跑不起来"的构建在两条轴上都仍然是错误:运行期闭包不可满足时会被拒绝,因为产物根本
326+
起不来。见[二进制分发](12-binary-distribution.md)
318327

319328
### `msvc@system` —— 机器自己的 Visual Studio
320329

modules/versioning/src/version.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ import std;
3131

3232
export namespace mcpp {
3333

34-
inline constexpr std::string_view MCPP_VERSION = "2026.8.30.1";
34+
inline constexpr std::string_view MCPP_VERSION = "2026.8.30.2";
3535

3636
} // namespace mcpp

src/build/prepare.cppm

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2186,30 +2186,52 @@ prepare_build(bool print_fingerprint,
21862186
chosenBy));
21872187
}
21882188
} else if (tcSpec.has_value() && *tcSpec == "system") {
2189-
// Explicit user opt-in to system PATH compiler — kept as escape hatch.
2189+
// REFUSED. THE COMPILER IS THE ONE AXIS THAT IS NOT THE PROJECT'S TO
2190+
// TAKE FROM THE HOST.
21902191
//
2191-
// WARNED, NOT REFUSED, and the boundary is "does it build and run".
2192-
// mcpp itself depends on no host: its toolchains, its payloads and
2193-
// everything the ecosystem publishes are resolved from the xim index.
2194-
// A USER'S OWN PROJECT may decide otherwise, and that decision is
2195-
// theirs to guarantee — measured, this configuration compiles a
2196-
// project using `import std` on a host that has a new enough compiler.
2197-
// Refusing it would break working setups to enforce a preference; the
2198-
// honest response is to say once what it costs and where the supported
2199-
// version of the same thing lives.
2192+
// mcpp's host-dependence policy is not uniform across axes, and the
2193+
// split is the point rather than an inconsistency:
22002194
//
2201-
// `explicit_compiler` is deliberately left empty here: `detect` below
2202-
// resolves the PATH compiler and records the absolute path in
2203-
// `tc->binaryPath`. Everything that needs a compiler PATH must read it
2204-
// from there — see host_tc_for_build_program, which did not, and spawned
2205-
// the empty string instead (#527).
2206-
mcpp::diag::degraded(
2207-
"build/toolchain",
2208-
"[toolchain] system selects a compiler from PATH",
2209-
"this build depends on what this host has installed, so it is not "
2210-
"reproducible on another machine and cannot be checked by CI",
2211-
mcpp::diag::host_route_hint(
2212-
mcpp::diag::HostDependence::Toolchain));
2195+
// LIBRARIES are the program's business. A project may link a host
2196+
// library or its own `.so`; mcpp says what that costs and what the
2197+
// supported route is, and does not refuse as long as the result
2198+
// builds and runs. The developer owns the artifact and guarantees it.
2199+
//
2200+
// THE TOOLCHAIN is mcpp's own contract. Everything mcpp promises —
2201+
// that `import std` is available, that the runtime closure is
2202+
// computable, that two machines and CI produce the same build — is a
2203+
// statement about a compiler mcpp resolved and can identify. A
2204+
// compiler picked off `PATH` makes every one of those promises
2205+
// unverifiable, and a build tool that cannot state what it built with
2206+
// is answering in the wrong version (see
2207+
// `.agents/docs/…a-build-must-be-able-to-state-its-own-version`).
2208+
//
2209+
// So this is refused rather than warned about, and it is refused HERE,
2210+
// before any resolution work, so the message is the first thing the
2211+
// user sees rather than a consequence three layers down.
2212+
//
2213+
// `msvc@system` is a different spelling and stays supported: it names a
2214+
// FAMILY whose installation mcpp locates and identifies, on the one
2215+
// platform where the compiler cannot be redistributed.
2216+
return std::unexpected(std::format(
2217+
"[toolchain] {} = \"system\" is not supported: mcpp builds only "
2218+
"with toolchains it manages.\n"
2219+
" A compiler taken from PATH cannot be identified or "
2220+
"reproduced, so `import std` availability, the runtime closure and "
2221+
"\"the same build on another machine\" all stop being things mcpp "
2222+
"can promise.\n"
2223+
" Name one instead — mcpp installs it on first use:\n"
2224+
"\n"
2225+
" [toolchain]\n"
2226+
" {} = \"gcc@16.1.0\"\n"
2227+
"\n"
2228+
" or set a machine default with `mcpp toolchain default "
2229+
"gcc@16.1.0`, and see `mcpp toolchain list` for what is available.\n"
2230+
" (On Windows, `msvc@system` is different and remains "
2231+
"supported: it names a family whose installation mcpp locates.)\n"
2232+
" Host LIBRARIES are a separate question and are not refused "
2233+
"— a project may link them and owns the result.",
2234+
kCurrentPlatform, kCurrentPlatform));
22132235
} else if (mcpp::platform::env::offline_mode()
22142236
|| mcpp::platform::env::no_auto_install()) {
22152237
// CI / offline / test opt-out: hard-error instead of silently
@@ -8234,22 +8256,31 @@ prepare_build(bool print_fingerprint,
82348256
// cache key drifted apart in the first place (#344).
82358257
// Which source trees does the fast path have to watch besides this one?
82368258
//
8237-
// A package whose root is neither under `projectRoot` nor under a store
8238-
// root is a `path` dependency — the shape every workspace member takes
8239-
// towards its siblings — and its sources are read on every build. The
8240-
// store test is the same `path_is_under_any` the cache-address code uses a
8241-
// few hundred lines below, so "came from a store" has one definition.
8259+
// A package whose root is neither under `projectRoot` nor under a directory
8260+
// mcpp OWNS is a `path` dependency — the shape every workspace member takes
8261+
// towards its siblings — and its sources are read on every build. See
8262+
// BuildContext::depSourceRoots for what the list is for.
8263+
//
8264+
// WHAT IS EXCLUDED, AND WHY IT IS "WHO WROTE THE DIRECTORY" RATHER THAN
8265+
// "WHICH KIND OF DEPENDENCY". An xpkg payload under the store is written
8266+
// once at install time and never edited. A git checkout under
8267+
// `<mcpp home>/git/<hash>` is a pinned revision in a hash-addressed
8268+
// directory: changing the revision changes the directory name, and the
8269+
// manifest that names it is already swept. Neither can change under a warm
8270+
// build, so sweeping them would buy nothing and cost a directory walk per
8271+
// dependency on every invocation — which is the fast path this whole change
8272+
// exists to keep.
82428273
//
8243-
// Store packages are deliberately excluded: a payload directory is written
8244-
// once at install time and never edited, so sweeping it would cost a
8245-
// directory walk per dependency on every invocation and could never report
8246-
// anything. See BuildContext::depSourceRoots for what this is for.
8274+
// A `path` dependency is the opposite on both counts: it is the user's
8275+
// working tree, and editing it is the point.
82478276
{
8277+
std::vector<std::filesystem::path> owned = storeRoots;
8278+
owned.push_back(mcpp::home::root());
82488279
std::vector<std::filesystem::path> roots;
82498280
for (std::size_t i = 1; i < packages.size(); ++i) {
82508281
const auto& pkgRoot = packages[i].root;
82518282
if (pkgRoot.empty()) continue;
8252-
if (mcpp::build::path_is_under_any(pkgRoot, storeRoots)) continue;
8283+
if (mcpp::build::path_is_under_any(pkgRoot, owned)) continue;
82538284
auto normalized = pkgRoot.lexically_normal();
82548285
if (normalized == root->lexically_normal()) continue;
82558286
if (std::find(roots.begin(), roots.end(), normalized) == roots.end())

0 commit comments

Comments
 (0)