Skip to content

Commit 8442434

Browse files
authored
fix(modgraph): a directory name the code page cannot spell must not end the build (2026.8.27.2) (#517)
Closes #516. #230 的同一处漏网:#231 加固了三个窄化站点,漏掉了同一个 walk 循环里 早一行执行的 `is_excluded_walk_dir`。 抛异常的不是解压,是 mcpp 自己 —— MSVC 的 `path::string()` 走 `WideCharToMultiByte(ACP)`,遇到当前代码页拼不出的字符就抛 `std::system_error`。 `include_dirs = { "*" }` 会从解压根无界递归遍历整棵上游源码树,cpp-httplib 带了 `test/www/<CJK>Dir/`,于是三个 httplib 测试在 Windows 上一起挂,Linux/macOS 全绿。 收敛成按用途分三档的一条规则:与 ASCII 字面量比较 → 按 path 比,不窄化; 稳定身份(hash/digest)→ u8string();交给编译器/ninja/CDB → try_narrow() 并处理 nullopt。跳过不再静默:按目录报告一次,走 mcpp.diag 的 degraded 通道。 顺带修掉 interface_set_digest 的一个真实跨平台不一致(Linux 打包 / Windows 校验 对非 ASCII 名字给出不同摘要)。加 .github/tools/check_narrow_conversions.sh 硬门。 红→绿闭环(同一 runner、同一测试、同一 ACP): ce86b41 不含修复 → ci-windows FAILED,报的是 #516 逐字相同的那句话; ci-linux 停在新门,点名 scanner.cppm:238 fade10f 含修复 → [ OK ] Scanner.GlobWalkSurvivesNamesTheCodePageCannotSpell 0059033 自审 6 处修正 → 36/36 checks success 内部依赖 xlings pin 2026.8.17.2 → 2026.8.27.4(17 个 pin 点)。
1 parent e187d3f commit 8442434

24 files changed

Lines changed: 1182 additions & 41 deletions

.agents/docs/2026-08-27-issue516-windows-acp-glob-walk-fix.md

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

.agents/skills/mcpp-contributing/SKILL.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,40 @@ docs/ ← 用户文档
278278
.agents/skills/ ← Agent 技能文档
279279
```
280280

281+
## 路径窄化不变式(走查得到的 path 不得直接 `.string()`
282+
283+
Windows 上 `std::filesystem::path::string()` 会把 native(宽)名经**进程 ANSI 代码页**
284+
转换,遇到该代码页拼不出的字符就抛 `std::system_error`**非 Windows 上同一个调用只是
285+
一次拷贝,永不失败**——所以这个隐患在 Linux/macOS 上(包括它们的测试里)完全不可见。
286+
287+
它已经付过两次代价,每次戴着不同的面具:#230 抛出后逃到 `std::terminate`,git-bash
288+
显示为**裸 exit 127**(看起来像"命令找不到");#516 逃到 `main()` 的 catch,显示为
289+
`internal: unhandled exception`(看起来像下载器的**解压/编码缺陷**)。#231 加固了三处
290+
调用点,漏掉了同一个 walk 循环里**早一行**执行的第四处。
291+
292+
规则(按用途选,不是三选一的风格问题):
293+
294+
| 用途 | 写法 |
295+
|---|---|
296+
| 与 ASCII 字面量比较 | **`path`**,根本不窄化 |
297+
| 需要稳定身份(hash / key / digest) | `p.u8string()` —— 各平台都是 UTF-8,不碰代码页 |
298+
| 需要交给编译器 / ninja / CDB | `mcpp::modgraph::try_narrow(p)`,并处理 `nullopt` |
299+
300+
`try_narrow` 返回 `nullopt` 表示"这个文件没法出现在任何交给工具链的字符串里"。
301+
**跳过它,并且必须报出来**——`mcpp.diag` 的批次不变式对此已有规定:因为前提不满足而
302+
少做事,必须走 `diag::degraded()` 并给出 `impact`。静默丢弃是这类缺陷藏身的地方。
303+
304+
`src/modgraph/``src/manifest/` 是 leaf 层(全仓没有一条到 `mcpp.ui` / `mcpp.diag`
305+
的 import 边),所以它们**记录**`note_unnarrowable_path`),由 CLI 层排空上报。
306+
307+
`.github/tools/check_narrow_conversions.sh` 是硬门,但它只扫 `src/modgraph`
308+
`src/scaffold`——**通过不等于已审计**。确有把握的站点用 `// NARROW-OK: <理由>` 标注,
309+
理由必须写出"为什么这个输入不可能带这种名字"。
310+
311+
**测试只有跑在 Windows CI 上才有意义**,且必须自己检查 `GetACP()`:runner 镜像哪天默认
312+
UTF-8 ACP(65001),这类用例会静默变成永远绿的装饰品。参见
313+
`tests/unit/test_modgraph.cpp``Scanner.GlobWalkSurvivesNamesTheCodePageCannotSpell`
314+
281315
## 注意事项
282316

283317
- C++23 模块项目,修改模块时注意 import 依赖顺序

.github/actions/bootstrap-mcpp/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ inputs:
2525
# `package.name`, so one of the two was simply unreachable — and which one
2626
# depended on the machine, which is why CI failed on `compat:lua` on
2727
# Windows and `mcpplibs.capi:lua` on Linux. Never pin below that.
28-
default: '2026.8.17.2'
28+
default: '2026.8.27.4'
2929
cache-target:
3030
description: also restore/save target/ (build artifacts + BMIs)
3131
required: false

.github/actions/setup-macos-llvm/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ inputs:
1515
# Floor imposed by the index, not a routine bump — see
1616
# .github/actions/bootstrap-mcpp/action.yml for why 0.4.69 is required
1717
# (two packages named `lua` in one repo need openxlings/xlings#381).
18-
default: '2026.8.17.2'
18+
default: '2026.8.27.4'
1919

2020
runs:
2121
using: composite
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Guard: a path that came out of a directory walk is never narrowed directly.
4+
#
5+
# WHY
6+
#
7+
# On Windows `std::filesystem::path::string()` converts the native (wide) name
8+
# through the process ANSI code page and THROWS std::system_error when a
9+
# character has no spelling there — "No mapping for the Unicode character
10+
# exists in the target multi-byte code page". Off Windows the same call is a
11+
# copy that cannot fail, so nothing on Linux or macOS — including their tests —
12+
# can see the hazard.
13+
#
14+
# It has cost two incidents, each wearing a different mask:
15+
#
16+
# #230 a walked index tree held a CJK-named issue template; the throw
17+
# escaped to std::terminate → __fastfail → git-bash reported a bare
18+
# exit 127, which reads as "command not found".
19+
# #516 cpp-httplib ships test/www/<CJK>Dir/ and the `include_dirs = { "*" }`
20+
# convention walks the whole extracted tarball; the throw escaped to
21+
# main()'s catch as `internal: unhandled exception`, which reads as an
22+
# extraction/encoding bug in the downloader.
23+
#
24+
# #231 hardened three call sites and missed a fourth — `is_excluded_walk_dir`,
25+
# which runs ONE LINE EARLIER in the same walk loop. A fifth site is what this
26+
# script exists to make expensive.
27+
#
28+
# THE RULE
29+
#
30+
# * Comparing against ASCII literals? Compare as `path`. Do not narrow.
31+
# * Need a stable identity (hash, key)? `u8string()` — UTF-8 everywhere,
32+
# never touches the code page.
33+
# * Need a build-facing string (compiler
34+
# argument, ninja file, CDB)? `mcpp::modgraph::try_narrow()`,
35+
# and handle the nullopt.
36+
#
37+
# WHAT THIS DOES AND DOES NOT CATCH
38+
#
39+
# It greps the leaf layers that walk trees mcpp does not control. It catches a
40+
# NEW direct narrowing written there. It does NOT catch a path narrowed after
41+
# being passed out to another layer — that is what the try_narrow convention is
42+
# for, and no grep can enforce it. Do not read a pass here as "audited".
43+
#
44+
# `.extension()` is deliberately NOT matched: an extension is ASCII in every
45+
# case that reaches these predicates, so matching it would produce only noise —
46+
# and noise is how a gate gets suppressed.
47+
#
48+
# Escape hatch: `// NARROW-OK: <reason>` on the line itself or within the two
49+
# lines above it. Use it when the input provably cannot carry an unspellable
50+
# name, and say why — a bare marker with no argument is worse than no gate,
51+
# because it reads as "someone checked".
52+
#
53+
# Usage: bash .github/tools/check_narrow_conversions.sh [repo_dir]
54+
55+
set -uo pipefail
56+
57+
REPO_DIR="${1:-$(pwd)}"
58+
cd "$REPO_DIR" || { echo "FAIL: cannot cd to $REPO_DIR" >&2; exit 1; }
59+
60+
# SCOPE, and why it is this narrow.
61+
#
62+
# The hazard needs a path from a tree MCPP DOES NOT CONTROL. Two directories
63+
# qualify: src/modgraph walks arbitrary package and project trees, and
64+
# src/scaffold enumerates third-party template providers.
65+
#
66+
# The first draft of this guard also covered src/pack and src/manifest and
67+
# produced 22 hits, ~20 of them false: src/pack narrows names MCPP ITSELF
68+
# produced (staging roots, built binaries, strip artifacts — all derived from
69+
# validated ASCII package/target names), and src/manifest only ever narrows an
70+
# `.extension()`. A gate with twenty false positives is a gate that gets
71+
# suppressed within a month, and the suppression then becomes the only record
72+
# that a rule existed. The real hazards in those two directories were fixed by
73+
# hand instead (pack/digest.cppm, which feeds on an unfiltered
74+
# recursive_directory_iterator over a published package).
75+
#
76+
# So: a pass here does NOT mean "the tree is audited". It means no NEW direct
77+
# narrowing was written where this class originates.
78+
SCAN_DIRS="src/modgraph src/scaffold"
79+
80+
PATTERN='\.(filename|stem)\(\)\.(generic_)?string\(\)'
81+
82+
fail=0
83+
found=0
84+
85+
for dir in $SCAN_DIRS; do
86+
[ -d "$dir" ] || { echo "FAIL: $dir does not exist — this guard has gone stale" >&2; exit 1; }
87+
while IFS= read -r file; do
88+
# Strip // line comments before matching: several of these files DESCRIBE
89+
# the forbidden call in prose (that is the point of the comments), and a
90+
# guard that trips on its own documentation gets deleted.
91+
while IFS=: read -r lineno text; do
92+
[ -n "${lineno:-}" ] || continue
93+
found=1
94+
# NARROW-OK on the line itself, or on either of the two lines above it.
95+
ctx=$(sed -n "$(( lineno > 2 ? lineno - 2 : 1 )),${lineno}p" "$file")
96+
case "$ctx" in
97+
*NARROW-OK:*) continue ;;
98+
esac
99+
echo "FAIL: $file:$lineno narrows a path directly:" >&2
100+
echo " ${text# }" >&2
101+
fail=1
102+
done < <(sed 's://.*::' "$file" | grep -nE "$PATTERN")
103+
done < <(find "$dir" -type f \( -name '*.cppm' -o -name '*.cpp' -o -name '*.hpp' \) | sort)
104+
done
105+
106+
if [ "$fail" = 1 ]; then
107+
cat >&2 <<'EOF'
108+
109+
Use one of:
110+
- compare as std::filesystem::path (ASCII literals; no narrowing)
111+
- p.u8string() (stable identity: hashes, keys)
112+
- mcpp::modgraph::try_narrow(p) (build-facing; handle nullopt)
113+
or annotate with `// NARROW-OK: <why this input cannot carry such a name>`.
114+
115+
Background: mcpp#516, mcpp#230, src/modgraph/glob.cppm.
116+
EOF
117+
exit 1
118+
fi
119+
120+
if [ "$found" = 0 ]; then
121+
echo "ok: no direct path narrowing in $SCAN_DIRS"
122+
else
123+
echo "ok: every direct narrowing in $SCAN_DIRS carries a NARROW-OK rationale"
124+
fi
125+
exit 0

.github/workflows/bootstrap-macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
# Dormant (workflow_dispatch only), but kept in step with the rest —
1818
# check_version_pins.sh holds it there. Floor: 0.4.69, below which the
1919
# index cannot resolve two packages that share a short name.
20-
XLINGS_VERSION: '2026.8.17.2'
20+
XLINGS_VERSION: '2026.8.27.4'
2121
steps:
2222
- uses: actions/checkout@v4
2323

.github/workflows/ci-fresh-install.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ jobs:
152152
env:
153153
XLINGS_NON_INTERACTIVE: '1'
154154
run: |
155-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.17.2
155+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.27.4
156156
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
157157
158158
- name: Install mcpp and config mirror
@@ -293,7 +293,7 @@ jobs:
293293

294294
- name: Install xlings + mcpp
295295
run: |
296-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.17.2
296+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.27.4
297297
# Deliberately NOT writing to $GITHUB_PATH here. On container
298298
# images that declare no PATH in their config (opensuse/
299299
# tumbleweed), appending a single dir to GITHUB_PATH makes the
@@ -364,7 +364,7 @@ jobs:
364364
# (older ones carry minos=15 and refuse to start).
365365
# v0.4.51+: in-process sha256 — this image has no sha256sum
366366
# binary, so pinned fetches failed before it.
367-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.17.2
367+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.27.4
368368
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
369369
370370
- name: Install mcpp and config mirror

.github/workflows/ci-linux-e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ jobs:
237237
238238
- name: Bootstrap xlings + released mcpp
239239
run: |
240-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.17.2
240+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.27.4
241241
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
242242
xlings update
243243
xlings install mcpp -y -g

.github/workflows/ci-linux.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ jobs:
5555
- name: Check version / xlings pin consistency
5656
run: bash .github/tools/check_version_pins.sh
5757

58+
# Same placement, same reason: pure text, no toolchain, under a second.
59+
#
60+
# This one is a HARD gate (unlike lint-ci-assertions.sh below) because it
61+
# has no false positives left — its scope was cut to the two directories
62+
# that walk trees mcpp does not control, and the one legitimate site
63+
# carries a NARROW-OK rationale. See the script's header for why the
64+
# scope is that narrow, and mcpp#516 for what it costs when it is missed.
65+
#
66+
# It runs on LINUX on purpose even though the bug it guards is
67+
# Windows-only: it is text analysis, and putting it where the fast leg is
68+
# means a violation is reported in seconds rather than after a Windows
69+
# bootstrap.
70+
- name: Check no walk-derived path is narrowed directly
71+
run: bash .github/tools/check_narrow_conversions.sh
72+
5873
# Same placement, same reason: pure text, no toolchain.
5974
#
6075
# ⚠️ IT PRINTS AND DOES NOT FAIL, DELIBERATELY. The three rules it carries

.github/workflows/cross-build-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ jobs:
122122
# release assets were uploaded in a broken state (records present,
123123
# blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX
124124
# half is handled by the marker-clear below.
125-
XLINGS_VERSION: '2026.8.17.2'
125+
XLINGS_VERSION: '2026.8.27.4'
126126
run: |
127127
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
128128
bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \
@@ -263,7 +263,7 @@ jobs:
263263
- name: Bootstrap mcpp via xlings
264264
env:
265265
XLINGS_NON_INTERACTIVE: '1'
266-
XLINGS_VERSION: '2026.8.17.2'
266+
XLINGS_VERSION: '2026.8.27.4'
267267
run: |
268268
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
269269
bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \

0 commit comments

Comments
 (0)