Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
482 changes: 482 additions & 0 deletions .agents/docs/2026-08-27-issue516-windows-acp-glob-walk-fix.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions .agents/skills/mcpp-contributing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,40 @@ docs/ ← 用户文档
.agents/skills/ ← Agent 技能文档
```

## 路径窄化不变式(走查得到的 path 不得直接 `.string()`)

Windows 上 `std::filesystem::path::string()` 会把 native(宽)名经**进程 ANSI 代码页**
转换,遇到该代码页拼不出的字符就抛 `std::system_error`。**非 Windows 上同一个调用只是
一次拷贝,永不失败**——所以这个隐患在 Linux/macOS 上(包括它们的测试里)完全不可见。

它已经付过两次代价,每次戴着不同的面具:#230 抛出后逃到 `std::terminate`,git-bash
显示为**裸 exit 127**(看起来像"命令找不到");#516 逃到 `main()` 的 catch,显示为
`internal: unhandled exception`(看起来像下载器的**解压/编码缺陷**)。#231 加固了三处
调用点,漏掉了同一个 walk 循环里**早一行**执行的第四处。

规则(按用途选,不是三选一的风格问题):

| 用途 | 写法 |
|---|---|
| 与 ASCII 字面量比较 | **按 `path` 比**,根本不窄化 |
| 需要稳定身份(hash / key / digest) | `p.u8string()` —— 各平台都是 UTF-8,不碰代码页 |
| 需要交给编译器 / ninja / CDB | `mcpp::modgraph::try_narrow(p)`,并处理 `nullopt` |

`try_narrow` 返回 `nullopt` 表示"这个文件没法出现在任何交给工具链的字符串里"。
**跳过它,并且必须报出来**——`mcpp.diag` 的批次不变式对此已有规定:因为前提不满足而
少做事,必须走 `diag::degraded()` 并给出 `impact`。静默丢弃是这类缺陷藏身的地方。

`src/modgraph/` 与 `src/manifest/` 是 leaf 层(全仓没有一条到 `mcpp.ui` / `mcpp.diag`
的 import 边),所以它们**记录**(`note_unnarrowable_path`),由 CLI 层排空上报。

`.github/tools/check_narrow_conversions.sh` 是硬门,但它只扫 `src/modgraph`、
`src/scaffold`——**通过不等于已审计**。确有把握的站点用 `// NARROW-OK: <理由>` 标注,
理由必须写出"为什么这个输入不可能带这种名字"。

**测试只有跑在 Windows CI 上才有意义**,且必须自己检查 `GetACP()`:runner 镜像哪天默认
UTF-8 ACP(65001),这类用例会静默变成永远绿的装饰品。参见
`tests/unit/test_modgraph.cpp` 的 `Scanner.GlobWalkSurvivesNamesTheCodePageCannotSpell`。

## 注意事项

- C++23 模块项目,修改模块时注意 import 依赖顺序
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/bootstrap-mcpp/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ inputs:
# `package.name`, so one of the two was simply unreachable — and which one
# depended on the machine, which is why CI failed on `compat:lua` on
# Windows and `mcpplibs.capi:lua` on Linux. Never pin below that.
default: '2026.8.17.2'
default: '2026.8.27.4'
cache-target:
description: also restore/save target/ (build artifacts + BMIs)
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/setup-macos-llvm/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ inputs:
# Floor imposed by the index, not a routine bump — see
# .github/actions/bootstrap-mcpp/action.yml for why 0.4.69 is required
# (two packages named `lua` in one repo need openxlings/xlings#381).
default: '2026.8.17.2'
default: '2026.8.27.4'

runs:
using: composite
Expand Down
125 changes: 125 additions & 0 deletions .github/tools/check_narrow_conversions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
#
# Guard: a path that came out of a directory walk is never narrowed directly.
#
# WHY
#
# On Windows `std::filesystem::path::string()` converts the native (wide) name
# through the process ANSI code page and THROWS std::system_error when a
# character has no spelling there — "No mapping for the Unicode character
# exists in the target multi-byte code page". Off Windows the same call is a
# copy that cannot fail, so nothing on Linux or macOS — including their tests —
# can see the hazard.
#
# It has cost two incidents, each wearing a different mask:
#
# #230 a walked index tree held a CJK-named issue template; the throw
# escaped to std::terminate → __fastfail → git-bash reported a bare
# exit 127, which reads as "command not found".
# #516 cpp-httplib ships test/www/<CJK>Dir/ and the `include_dirs = { "*" }`
# convention walks the whole extracted tarball; the throw escaped to
# main()'s catch as `internal: unhandled exception`, which reads as an
# extraction/encoding bug in the downloader.
#
# #231 hardened three call sites and missed a fourth — `is_excluded_walk_dir`,
# which runs ONE LINE EARLIER in the same walk loop. A fifth site is what this
# script exists to make expensive.
#
# THE RULE
#
# * Comparing against ASCII literals? Compare as `path`. Do not narrow.
# * Need a stable identity (hash, key)? `u8string()` — UTF-8 everywhere,
# never touches the code page.
# * Need a build-facing string (compiler
# argument, ninja file, CDB)? `mcpp::modgraph::try_narrow()`,
# and handle the nullopt.
#
# WHAT THIS DOES AND DOES NOT CATCH
#
# It greps the leaf layers that walk trees mcpp does not control. It catches a
# NEW direct narrowing written there. It does NOT catch a path narrowed after
# being passed out to another layer — that is what the try_narrow convention is
# for, and no grep can enforce it. Do not read a pass here as "audited".
#
# `.extension()` is deliberately NOT matched: an extension is ASCII in every
# case that reaches these predicates, so matching it would produce only noise —
# and noise is how a gate gets suppressed.
#
# Escape hatch: `// NARROW-OK: <reason>` on the line itself or within the two
# lines above it. Use it when the input provably cannot carry an unspellable
# name, and say why — a bare marker with no argument is worse than no gate,
# because it reads as "someone checked".
#
# Usage: bash .github/tools/check_narrow_conversions.sh [repo_dir]

set -uo pipefail

REPO_DIR="${1:-$(pwd)}"
cd "$REPO_DIR" || { echo "FAIL: cannot cd to $REPO_DIR" >&2; exit 1; }

# SCOPE, and why it is this narrow.
#
# The hazard needs a path from a tree MCPP DOES NOT CONTROL. Two directories
# qualify: src/modgraph walks arbitrary package and project trees, and
# src/scaffold enumerates third-party template providers.
#
# The first draft of this guard also covered src/pack and src/manifest and
# produced 22 hits, ~20 of them false: src/pack narrows names MCPP ITSELF
# produced (staging roots, built binaries, strip artifacts — all derived from
# validated ASCII package/target names), and src/manifest only ever narrows an
# `.extension()`. A gate with twenty false positives is a gate that gets
# suppressed within a month, and the suppression then becomes the only record
# that a rule existed. The real hazards in those two directories were fixed by
# hand instead (pack/digest.cppm, which feeds on an unfiltered
# recursive_directory_iterator over a published package).
#
# So: a pass here does NOT mean "the tree is audited". It means no NEW direct
# narrowing was written where this class originates.
SCAN_DIRS="src/modgraph src/scaffold"

PATTERN='\.(filename|stem)\(\)\.(generic_)?string\(\)'

fail=0
found=0

for dir in $SCAN_DIRS; do
[ -d "$dir" ] || { echo "FAIL: $dir does not exist — this guard has gone stale" >&2; exit 1; }
while IFS= read -r file; do
# Strip // line comments before matching: several of these files DESCRIBE
# the forbidden call in prose (that is the point of the comments), and a
# guard that trips on its own documentation gets deleted.
while IFS=: read -r lineno text; do
[ -n "${lineno:-}" ] || continue
found=1
# NARROW-OK on the line itself, or on either of the two lines above it.
ctx=$(sed -n "$(( lineno > 2 ? lineno - 2 : 1 )),${lineno}p" "$file")
case "$ctx" in
*NARROW-OK:*) continue ;;
esac
echo "FAIL: $file:$lineno narrows a path directly:" >&2
echo " ${text# }" >&2
fail=1
done < <(sed 's://.*::' "$file" | grep -nE "$PATTERN")
done < <(find "$dir" -type f \( -name '*.cppm' -o -name '*.cpp' -o -name '*.hpp' \) | sort)
done

if [ "$fail" = 1 ]; then
cat >&2 <<'EOF'

Use one of:
- compare as std::filesystem::path (ASCII literals; no narrowing)
- p.u8string() (stable identity: hashes, keys)
- mcpp::modgraph::try_narrow(p) (build-facing; handle nullopt)
or annotate with `// NARROW-OK: <why this input cannot carry such a name>`.

Background: mcpp#516, mcpp#230, src/modgraph/glob.cppm.
EOF
exit 1
fi

if [ "$found" = 0 ]; then
echo "ok: no direct path narrowing in $SCAN_DIRS"
else
echo "ok: every direct narrowing in $SCAN_DIRS carries a NARROW-OK rationale"
fi
exit 0
2 changes: 1 addition & 1 deletion .github/workflows/bootstrap-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
# Dormant (workflow_dispatch only), but kept in step with the rest —
# check_version_pins.sh holds it there. Floor: 0.4.69, below which the
# index cannot resolve two packages that share a short name.
XLINGS_VERSION: '2026.8.17.2'
XLINGS_VERSION: '2026.8.27.4'
steps:
- uses: actions/checkout@v4

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-fresh-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
env:
XLINGS_NON_INTERACTIVE: '1'
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.17.2
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.27.4
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install mcpp and config mirror
Expand Down Expand Up @@ -293,7 +293,7 @@ jobs:

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

- name: Install mcpp and config mirror
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-linux-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ jobs:

- name: Bootstrap xlings + released mcpp
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.17.2
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.27.4
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
xlings update
xlings install mcpp -y -g
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ jobs:
- name: Check version / xlings pin consistency
run: bash .github/tools/check_version_pins.sh

# Same placement, same reason: pure text, no toolchain, under a second.
#
# This one is a HARD gate (unlike lint-ci-assertions.sh below) because it
# has no false positives left — its scope was cut to the two directories
# that walk trees mcpp does not control, and the one legitimate site
# carries a NARROW-OK rationale. See the script's header for why the
# scope is that narrow, and mcpp#516 for what it costs when it is missed.
#
# It runs on LINUX on purpose even though the bug it guards is
# Windows-only: it is text analysis, and putting it where the fast leg is
# means a violation is reported in seconds rather than after a Windows
# bootstrap.
- name: Check no walk-derived path is narrowed directly
run: bash .github/tools/check_narrow_conversions.sh

# Same placement, same reason: pure text, no toolchain.
#
# ⚠️ IT PRINTS AND DOES NOT FAIL, DELIBERATELY. The three rules it carries
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/cross-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ jobs:
# release assets were uploaded in a broken state (records present,
# blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX
# half is handled by the marker-clear below.
XLINGS_VERSION: '2026.8.17.2'
XLINGS_VERSION: '2026.8.27.4'
run: |
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \
Expand Down Expand Up @@ -263,7 +263,7 @@ jobs:
- name: Bootstrap mcpp via xlings
env:
XLINGS_NON_INTERACTIVE: '1'
XLINGS_VERSION: '2026.8.17.2'
XLINGS_VERSION: '2026.8.27.4'
run: |
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
# Pin xlings to a known-good version. The upstream install
# script always grabs `latest` (no version override), so we
# download + self-install manually to avoid broken releases.
XLINGS_VERSION: '2026.8.17.2'
XLINGS_VERSION: '2026.8.27.4'
run: |
if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
Expand Down Expand Up @@ -289,7 +289,7 @@ jobs:
- name: Bootstrap mcpp via xlings
env:
XLINGS_NON_INTERACTIVE: '1'
XLINGS_VERSION: '2026.8.17.2'
XLINGS_VERSION: '2026.8.27.4'
run: |
tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz"
bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \
Expand Down Expand Up @@ -360,7 +360,7 @@ jobs:
# below are pinned to the same version as XLINGS_VERSION; they are
# NOT interpolated from it, so check_version_pins.sh scans for them
# explicitly (they were absent from the old lock-step comment).
XLA="xlings-2026.8.17.2-linux-aarch64.tar.gz"
XLA="xlings-2026.8.27.4-linux-aarch64.tar.gz"
# NOT fetch_release.sh: this asset is OPTIONAL and the `if` is the
# point — an arch with no prebuilt xlings must fall through quietly,
# while the helper retries a 404 five times before giving up. The one
Expand All @@ -369,9 +369,9 @@ jobs:
# cover it.
if curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors \
--connect-timeout 20 --max-time 600 -o "/tmp/$XLA" \
"https://github.com/openxlings/xlings/releases/download/v2026.8.17.2/$XLA"; then
"https://github.com/openxlings/xlings/releases/download/v2026.8.27.4/$XLA"; then
tar -xzf "/tmp/$XLA" -C /tmp
XLBIN=$(find /tmp/xlings-2026.8.17.2-linux-aarch64 -path '*/bin/xlings' -type f | head -1)
XLBIN=$(find /tmp/xlings-2026.8.27.4-linux-aarch64 -path '*/bin/xlings' -type f | head -1)
if [ -n "$XLBIN" ]; then
mkdir -p "$STAGING/$WRAPPER/registry/bin"
cp "$XLBIN" "$STAGING/$WRAPPER/registry/bin/xlings"
Expand Down Expand Up @@ -449,7 +449,7 @@ jobs:
- name: Bootstrap mcpp via xlings
env:
XLINGS_NON_INTERACTIVE: '1'
XLINGS_VERSION: '2026.8.17.2'
XLINGS_VERSION: '2026.8.27.4'
run: |
if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then
WORK=$(mktemp -d)
Expand Down Expand Up @@ -632,7 +632,7 @@ jobs:
shell: bash
env:
XLINGS_NON_INTERACTIVE: '1'
XLINGS_VERSION: '2026.8.17.2'
XLINGS_VERSION: '2026.8.27.4'
run: |
# Captured before the `cd` below, in POSIX form: this step never
# returns to the workspace, and GITHUB_WORKSPACE is a backslash
Expand Down
Loading
Loading