Skip to content

Commit 5807e36

Browse files
authored
batch: #261 windows cmdline, #257 clang depfile, #258 conditional per-glob flags, #254 host/target axis (#264) — 0.0.102
Four issues in one batch, joined by two architectural threads: "one decision, one derivation" (#254 and #258 are both a decision derived in two places that had drifted apart), and a new invariant, "failures must be audible" — three of the four were the engine quietly doing less when a precondition was not met. #261 — the clang scan rule produced P1689 JSON via shell redirection, which forced a `cmd /c` wrapper on Windows and with it cmd.exe's 8191-char ceiling, a quarter of CreateProcess's. clang-scan-deps has -o since LLVM 17, so the redirect and the wrapper are both gone. The unbounded axis is separately addressed by extending #247's response-file mitigation from the link rules to every rule carrying $local_includes. #257 — editing a file pulled into a module interface via a purview #include did not retrigger the rebuild under Clang; the build silently reused a stale BMI. 0.0.97 had conflated "emit a depfile" with "filter GCC's reversed module rules" into one predicate, and scoped both to GCC because Clang's shape was unverified. Measured: Clang emits a single plain rule with nothing to filter, so the gate was guarding against a shape that does not exist, at the cost of the correctness contract bundled with it. Split into two predicates. Also gives C and GAS edges the depfile they never had on any toolchain. #258 — `sources` was OS-conditional but `flags` was not, so a manifest covering three OSes had to show each OS the other two's flag entries, all matching zero sources by construction. The vendored-opencv port paid 703 stub files, 32 globs and ~23 guaranteed dead-glob warnings for that. The fix is the type behind the feature: mcpp's two conditional axes each hand-picked which build fields they could carry and picked different subsets, so BuildInputs makes membership answerable by the type rather than by a list someone must remember to update. #254 — every xpkg per-OS splice and xpm asset table keyed on a compile-time HOST constant while `[target.'cfg(...)']` was evaluated against the resolved target. Cross builds got the wrong leg; native builds cannot observe it, which is why three-platform CI never caught it. The axes are now distinct types with no conversion and no construction from a bare string, and the platform parameter lost its host default — a silent default is how this survived. #256 — no mcpp defect (the crash is in the Clang frontend on the import side), but mcpp bundles LLVM and the hazard lands on the module-package pattern this project recommends. Adds documented guidance and a state canary over the bundled toolchains. Review and CI found four defects this batch introduced, all fixed here: diag::flush was never called in production (the channel built to stop silent degradation was silently degrading); -MMD was applied to `.s`, which is not preprocessed; the response file swallowed backslashes in paths this file does not own, breaking every `import std;` on Windows; and the target platform was computed before the target triple was fully resolved. Deliberately out of scope: #259 (root cause traced to a silent per-dep skip on the xlings side; investigation posted there) and the manifest unknown-key policy (#263). Design: .agents/docs/2026-07-22-v0.0.102-batch-254-261-design.md Triage: .agents/docs/2026-07-22-issue-triage-254-261.md
1 parent af25d18 commit 5807e36

27 files changed

Lines changed: 2698 additions & 196 deletions

.agents/docs/2026-07-19-issues-230-243-batch-ledger-and-architecture-assessment.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,33 @@
117117
- 每落地一个 issue:更新 §1 对应行的状态 + commit,并在 CHANGELOG 追加(带 `#nnn`)。
118118
- 凡"根因在仓外 / 转后续"的,§1 标 ⛔ 并附外部 issue 链接,**不得**在本仓用 band-aid 标记已修。
119119
- 本批次的发布仍走既定链路:release 四平台 → 镜像 xlings-res(gh+gtc)→ xim-pkgindex 索引 → bump bootstrap pin;随后 mcpplibs #79 CI pin 升级解阻塞(#240#79 的合并门)。
120+
121+
---
122+
123+
## 6. 架构不变量(跨批次,持续生效)
124+
125+
### 6.1 同一决策不许两处推导(§5.2 起,持续)
126+
127+
已命中的实例:#242 的 feature 请求集解析×激活两处不自洽;#253 的 per-OS `features`;**0.0.102 新增两例**——
128+
- **#254**:平台选择在 xpkg 通道按宿主编译期常量推导,在 manifest 通道按 resolved target 推导。native 构建 host == target 使两者巧合一致,缺陷因此对三平台 CI 完全不可见。收敛手段是**类型化**(`mcpp.platform.axis`),让"传错轴"成为编译错误而非运行期巧合。
129+
- **#258**:"哪些构建输入可被条件化"在 cfg 轴与 feature 轴各推导一遍,且答案不同。收敛手段是把该集合提炼为**类型**(`BuildInputs`)而非一张手工维护的键表——表会与结构体漂移,类型不会。
130+
131+
判据:当一个决策需要在两处表达时,优先找一个**类型**来承载它,其次才是把两处调用同一个函数。
132+
133+
### 6.2 失败必须响,不许静默降级(0.0.102 新立)
134+
135+
> 任何"因为条件不满足而少做一步"的分支,必须要么返回错误,要么经 `mcpp::diag::degraded` 上报;
136+
> `log::debug` / `log::verbose` **不算**用户可见。
137+
> 丢弃 `std::expected` 返回值(`(void)expr` 或不接收)在本仓视为缺陷。
138+
139+
由来:0.0.102 批次的四条 issue 里有三条的实质是同一失效模式——引擎遇到没准备好的情况时安静地少做一点事。#257 在 clang 上不发 depfile(构建"成功",产物用陈旧 BMI);#258 在条件段静默丢弃 `flags`(构建"成功",规则从未生效);#259 的 sysroot 兜底是死代码且返回值被 `(void)` 丢弃(安装"成功",二进制不能 exec)。
140+
141+
值得注意的是本仓在**别处**早已把这条纪律做得很好(xpkg 未知键 → `xpkgUnknownKeys` + did-you-mean;`scan_overrides` 零命中 → 硬错;死 glob → 告警并点名归属 feature)。纪律一直存在,只是没有被提升为跨模块不变量,于是新代码没有默认继承它。
142+
143+
`diag::degraded``impact` 参数是这条不变量的执行机制:它强制作者写出"用户会因此遭遇什么"。上述三个 bug 当年缺的恰好就是这句话。
144+
145+
### 6.3 code review checklist 增补
146+
147+
- 每个 `if (cond) return;` / `continue` / `catch` 静默吞掉的分支:用户看得见吗?
148+
- 每个 `std::expected` 返回值:被检查了吗?`(void)` 丢弃需要理由。
149+
- 新增一个"可条件化 / 可传播 / 可覆盖"的字段时:它进的是哪个**类型**?该类型是否已经表达了这个语义?

.agents/docs/2026-07-22-issue-triage-254-261.md

Lines changed: 326 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/2026-07-22-v0.0.102-batch-254-261-design.md

Lines changed: 533 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.102] — 2026-07-22
7+
8+
> 四条 issue 一个批次:#261 windows 命令行长度、#257 clang 依赖跟踪缺口、#258 条件段表达力、#254 host/target 双轴不自洽。贯穿主线是**同一决策不许两处推导**,以及本批次新立的第二条不变量**失败必须响,不许静默降级**。设计见 `.agents/docs/2026-07-22-v0.0.102-batch-254-261-design.md`,issue 裁决见 `.agents/docs/2026-07-22-issue-triage-254-261.md`
9+
10+
### 修复
11+
12+
- **windows scan-deps 撞 8191 字符上限**(#261):clang 扫描规则此前用 shell 重定向产出 P1689 JSON,ninja 在 windows 走 CreateProcess 不解释 `>`,于是整条命令被 `cmd /c` 包裹——而 cmd.exe 的命令行上限是 8191,只有 CreateProcess 的 1/4。改用 `clang-scan-deps -o`(LLVM 17+,两个自带工具链均实测可用),重定向与包裹一并消失,与 GCC 的 `-fdeps-file=`、MSVC 的 `/scanDependencies` 三条分支形态统一。`cmd /c` 自此在全仓 ninja 规则中绝迹。
13+
- **windows 编译/扫描命令行无长度兜底**(#261):`local_include_flags()` 每依赖一个 `-I`、无上界,而 `cxx_module`/`cxx_object`/`c_object`/`asm_object`/`cxx_scan` 全部内联该载荷;扫描命令包住编译命令,只是恰好先炸。把 #247 给链接规则的 response file 推广到所有携带无界载荷的规则(gcc/clang 驱动与 cl.exe 均展开 `@file`,clang-scan-deps 会把 `--` 之后的 `@file` 透传给驱动;NASM 因为拼写是 `-@ file` 而排除)。顺带把 `escape_flag_path` 改用 `generic_string()`——这是 #247 那个坑往下一层:response file 内容按 GNU 文法分词,反斜杠是转义符,windows 的 `-I` 路径一旦离开命令行就会丢分隔符。POSIX 逐字节不变。
14+
- **clang 路径 purview `#include` 不触发重建**(#257):编辑模块接口 purview 内 `#include` 的文件后,构建静默复用陈旧 BMI,导入方对着旧接口编译——最差的一类失效,错误结果看起来像一次正常的增量构建。根因是 0.0.97 把两个决策捆在一个谓词里:**是否发 depfile****是否剥离 GCC `-fmodules` 附加的 reversed make-rules**。实测两个自带工具链,clang 的 module-TU depfile 就是一条普通规则(`x.o: x.cppm ops.inc`),没有任何需要过滤的东西——当年那道闸在防一个不存在的形状,代价是把与它捆在一起的正确性契约一起关掉了。现拆为 `posixDepfile`(所有 POSIX 非 MSVC)与 `needsGnuModuleFilter`(仅 GCC)。同时补上同一处不对称的另一半:`c_object`/`asm_object` 此前在**任何**工具链上都没有 depfile。windows 非 MSVC(mingw gcc / 托管 clang)仍无 depfile(GCC 过滤器依赖 awk),但现在经 `diag::degraded` 明确上报影响,不再静默。
15+
- **xpkg per-OS 段按宿主而非目标拼接**(#254):per-OS 段的 sources/flags/deps 与 xpm 版本/资产表全部按 `xpkg_platform` 这个**编译期宿主常量**选取,而 `[target.'cfg(...)']`**解析后的目标**求值——同一决策两处推导。交叉编译时依赖包因此被拼进宿主那条腿。原生构建 host == target 恰好掩盖了它,所以三平台 CI 从未发现。
16+
17+
### 新增
18+
19+
- **`mcpp::diag` 统一降级/告警通道**:`degraded()` 强制填写 `impact`——作者必须回答"用户会因此遭遇什么",而这正是每一个静默降级 bug 当年缺的那句话。记录按整条载荷去重、报告即渲染(告警与 `ui::status` 的交织顺序、以及提前返回前已报内容都不变),`--strict` 提升策略收敛到单点(此前在 `prepare.cppm` 里 copy-paste 了 5 处)。告警内容首次可单测。`prepare.cppm` 的 8 处裸 `println(stderr, "warning: ...")` 已迁入;渲染仍走 `ui::warning`,输出逐字节不变。
20+
- **`[target.'cfg(...)'.build]` 支持 per-glob `flags``include_dirs`**(#258,xpkg `target_cfg` 同步):此前 `sources` 可按 OS 条件化而 `flags` 不能,于是一份覆盖三 OS 的 manifest 必须让每个 OS 都看到另外两个 OS 的 flag 条目,而它们按构造必然零命中。vendored-opencv 移植为此付出 **703 个 stub 文件**(唯一用途是给 windows TU 制造 OS-唯一路径好让全局 flag 表 key 上去)、32 条指向它们的 glob,以及每次构建 ~23 条结构性死 glob 告警。条件条目追加在 base 之后,GNU last-wins 使**撤销**可表达(windows 需要 `-UHAVE_UNISTD_H` 对抗 base 的 `-D`,而无条件覆盖层做不到——`-U` 反条目自身也需要按 OS 条件化,递归回同一缺口)。死 glob 告警由**结构**消除:不匹配当前 OS 的条目根本不进 `globFlags`,与 #253 在 feature 轴上的做法同构;issue 里提的 `optional = true` 逃生口**明确不做**,那会成为第二套抑制机制。
21+
- **`mcpp.platform.axis`:host / target 两轴成为不同类型**(#254):互不可转换,且都不能由裸字符串构造;API 声明自己要哪一轴,调用点必须**指名**自己给的是哪一轴。`synthesize_from_xpkg_lua` 的平台参数去掉宿主默认值改为必填——静默默认正是这个 bug 得以存活的方式。三段 triple 与 xpkg 的词汇差异("macos" vs "macosx")收进 `TargetPlatform::for_os`。分类:依赖包 manifest 合成与版本/资产选择走 **target**;工具链版本列举走 **host**(其注释本就写明了理由);`--all-os` lint 走刻意起得别扭的 `for_lint_of()`
22+
- **`BuildInputs` 类型**(#258 的架构面):cfg 轴与 feature 轴此前各自手挑可条件化的字段子集,**且挑得不一样**(cfg 拿 cflags/cxxflags/ldflags/sources,feature 拿 sources/defines/flags)。"哪些构建输入可被条件化"这一决策被提炼成一个**类型**而非一张要靠人记得更新的表。入选判据两条:合并语义是**追加**;消费点在条件合并**之后**。据此排除选型轴(`target` 用来*选定* triple,而谓词要靠该 triple 求值——构造性循环)与策略标量(需 last-wins,是另一场设计);`generatedFiles` 因为是写磁盘的**副作用动作**而非输入、`featureDefines` 因为是沿 Public 边**传播的接口贡献**而非私有构建输入,各自按范畴排除。`BuildConfig`**继承**方式承载(该字段在 ~150 处被读,嵌套只会是 150 次无收益的机械改动),`ConditionalConfig`**嵌套**——保证需要落在那里。合并收敛为单一 `append(BuildInputs&, const BuildInputs&)`
23+
- **#256 clang 模块运算符模板 canary + 文档**:mcpp 侧无缺陷(`--precompile` 成功,崩溃在 clang 前端的导入侧),但 mcpp 自带 LLVM,且该 hazard 正落在本项目推荐的模块包模式上。文档给出可操作规则(导出的运算符模板,所有模板参数应由**第一个实参**绑定;否则改为对整个推导操作数类型建模)与可直接套用的改写范式。e2e 150 是**状态** canary 而非通过/失败契约:期望表记录 LLVM 20-22 崩、18-19 正常,一旦现实与期望不符即失败——未来某次 clang bump **修好**它与**再弄坏**它同样是需要被看见的信号。
24+
25+
### 备注
26+
27+
- 新增不变量写入批次总账:**任何"因条件不满足而少做一步"的分支,必须要么返回错误,要么经 `diag::degraded` 上报;`log::debug`/`log::verbose` 不算用户可见;丢弃 `std::expected` 返回值视为缺陷。**
28+
- **#254 的覆盖是单测级**:per-OS 段按 target 选段、host==target 逐字节不变、轴类型互不可转,三条单测锁定;原计划的"cross 腿消费带 per-OS 段的 xpkg dep"e2e 未落地(需要 e2e 侧先有"复用宿主 registry + 本地索引 + 预置 payload"的夹具,临时 MCPP_HOME 会触发整套交叉工具链重装),记为已知缺口。
29+
- e2e 新增 148(64 个 include dir 的宽 include 表,POSIX 验内联形态、windows 验 response file)、149(条件段 per-glob flags 四象限:命中生效/覆盖 base 的 removal/非命中零告警/真死 glob 仍告警)、150(#256 canary);118 去掉 `# requires: gcc` 并加 clang 腿。
30+
- **未包含**:#259(根因在 xlings 侧的 dep 静默丢弃,调查结论已发 issue 评论;mcpp 侧另发现 sysroot 兜底是死代码——`resolve_xpkg_path("xim:glibc")` 缺版本必然失败且两处都丢弃返回值——一并另行安排);manifest 未知键策略五处不一致([#263](https://github.com/mcpp-community/mcpp/issues/263),有兼容性面,与 #258 无依赖)。
31+
632
## [0.0.101] — 2026-07-20
733

834
> #253:feature 模型两缺口收口——per-feature per-glob `flags` + per-OS `features` 语义锁定,解锁 compat.opencv `dnn` off-linux 腿并消除 feature-off 构建的死 glob 告警。设计见 `.agents/docs/2026-07-20-issue-253-feature-flags-and-per-os-features-design.md`

docs/03-toolchains.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,56 @@ When `MCPP_HOME` is not set explicitly, mcpp locates the sandbox automatically b
267267
## ABI Capability Enforcement
268268

269269
A dependency can declare an `abi:<name>` capability (for example, `compat.glfw` declares `abi:glibc`). When the resolved toolchain's ABI does not satisfy any dependency's abi requirement, the build **fails early** with a suggested fix (for example, a musl-static toolchain encountering an abi:glibc dependency), replacing deeper link/header errors. Inspect with: `mcpp why toolchain`.
270+
271+
## Known Toolchain Hazard: Operator Templates in Module Interfaces (Clang 20+)
272+
273+
A module that exports **replacement operator templates** can poison name
274+
lookup for that operator in every importer when compiled by Clang 20 or 22:
275+
any translation unit that `import`s the module and uses that operator — **on
276+
any type at all** — crashes the frontend (SIGSEGV). GCC 16 and Clang 18 are
277+
unaffected, so this is a regression somewhere between Clang 18 and 20.
278+
279+
This bites the module-package pattern directly. Wrapping an upstream header
280+
whose operators are `static inline` templates, and mirroring their signatures
281+
with a trivially-true constraint (the standard mixed-TU subsumption recipe),
282+
is exactly how you hit it.
283+
284+
**The rule of thumb:** every template parameter should be pinned by the
285+
**first** function argument. Shapes that break this are the poisonous ones:
286+
287+
```cpp
288+
// Poisonous — `n` and `l` are not determined by argument 1
289+
template<typename T, int m, int n, int l>
290+
Matx<T, m, n> operator*(const Matx<T, m, l>& a, const Matx<T, l, n>& b);
291+
292+
// Poisonous — second typename appears only in argument 2
293+
template<typename T1, typename T2, int n>
294+
Vec<T1, n>& operator+=(Vec<T1, n>& a, const Vec<T2, n>& b);
295+
296+
// Fine — every parameter is pinned by argument 1
297+
template<typename T, int m, int n>
298+
Matx<T, m, n> operator+(const Matx<T, m, n>& a, const Matx<T, m, n>& b);
299+
```
300+
301+
The crash is **name-keyed**: one poisoned `operator*` declaration makes every
302+
`x * y` in every importer crash, for entirely unrelated types. The function
303+
body is irrelevant.
304+
305+
**The workaround** is to deduce whole operand types and constrain them,
306+
rather than destructuring them in the parameter list. It stays
307+
call-compatible, and mixed-TU semantics survive because the upstream
308+
exact-pattern `static inline` remains more specialized and still wins there:
309+
310+
```cpp
311+
template<typename MA, typename MB>
312+
requires pick<typename MA::value_type>
313+
&& __is_same(MA, typename MA::mat_type)
314+
&& __is_same(MB, Matx<typename MB::value_type,
315+
(int)MA::rows, (int)MA::cols>)
316+
inline MA& operator+=(MA& a, const MB& b);
317+
```
318+
319+
Tracked as [mcpp#256](https://github.com/mcpp-community/mcpp/issues/256).
320+
`tests/e2e/150_clang_module_operator_template.sh` is a canary over the
321+
bundled LLVM toolchains, so a future Clang bump that fixes — or re-breaks —
322+
this becomes visible instead of silently changing what packages can express.

docs/05-mcpp-toml.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,12 +376,39 @@ when you need arch/env conditions or combinators.
376376
- **Keys**: `dependencies` / `dev-dependencies` / `build-dependencies`, and
377377
`build` with `cflags` / `cxxflags` / `ldflags` / `sources` (mcpp 0.0.95+ —
378378
conditional source globs, e.g. gating `src/x86/**/*.asm` behind
379-
`cfg(arch = "x86_64")`; `!`-exclusion globs work here too).
379+
`cfg(arch = "x86_64")`; `!`-exclusion globs work here too), plus `flags` and
380+
`include_dirs` / `include_dirs_after` (mcpp 0.0.102+).
381+
- **What `build` accepts is exactly the set of *additive build inputs*** — the
382+
things that combine by appending and are consumed after the predicate is
383+
evaluated. `linkage`, `target`, and the profile knobs are deliberately not
384+
among them: they are *inputs to* target selection (conditioning `target` on
385+
a predicate evaluated against `target` is circular), or they need
386+
override-rather-than-append semantics.
380387
- **Evaluated against the resolved target** — the `--target` triple for a cross
381388
build, otherwise the host. So a native Linux build never even *downloads* a
382389
`[target.windows]` dependency.
383390
- **Precedence**: an exact-triple table wins over a `cfg`/alias table; multiple
384-
matching predicate tables have their flags concatenated.
391+
matching predicate tables have their flags concatenated. Conditional entries
392+
are appended **after** the unconditional `[build]` ones, so under GNU
393+
"last flag wins" a conditional rule overrides a broader unconditional one.
394+
That is what makes a per-OS **removal** expressible:
395+
396+
```toml
397+
[build]
398+
flags = [{ glob = "third_party/zlib/**", defines = ["HAVE_UNISTD_H=1"] }]
399+
400+
# clang-MSVC has no <unistd.h>: undo the base define, add the windows one.
401+
[target.'cfg(windows)'.build]
402+
flags = [{ glob = "third_party/zlib/**",
403+
defines = ["NO_FSEEKO"], cflags = ["-UHAVE_UNISTD_H"] }]
404+
```
405+
406+
- **A conditional `flags` entry that does not match the current target does not
407+
exist at all**, so it cannot produce a "glob matched no source file" warning.
408+
One manifest can therefore carry all three OSes' flag tables without any of
409+
them generating noise on the other two — the same way an inactive feature's
410+
entries simply are not there. A zero-hit glob in the *unconditional* table
411+
still warns, because there it is a real defect.
385412
- **`toolchain` / `linkage` are exact-triple only** — they describe one specific
386413
cross target, so put them under `[target.<triple>]` (above), not under a bare
387414
alias or `cfg(...)`.

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.101"
3+
version = "0.0.102"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/execute.cppm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export module mcpp.build.execute;
1010

1111
import std;
1212
import mcpp.build.prepare;
13+
import mcpp.diag;
1314
import mcpp.build.plan;
1415
import mcpp.build.backend;
1516
import mcpp.build.ninja;
@@ -334,6 +335,14 @@ export int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache,
334335
std::move(runTargets), runEnvKey, runEnvValue);
335336
}
336337

338+
// The one place the --strict policy is settled. Degradations reported by
339+
// the backend (e.g. a toolchain/platform combination that cannot emit a
340+
// depfile, #257) are discovered during emission, so this has to come
341+
// after the build rather than at the end of prepare_build. Without this
342+
// call the whole diag channel would report and then be ignored — the
343+
// exact failure mode it exists to prevent.
344+
if (!mcpp::diag::flush(ctx.strict)) return 1;
345+
337346
mcpp::ui::finished("release", r->elapsed);
338347
return 0;
339348
}

0 commit comments

Comments
 (0)