Skip to content

Commit df985df

Browse files
fix(scanner): prune .mcpp from glob walks; never throw on unnarrowable names (#230) (#231)
* fix(scanner): prune .mcpp from glob walks; never throw on unnarrowable names (#230) Root cause of the windows-latest 'exit 127' (really 0xC0000409): 0.0.95's scanner walks globs with follow_directory_symlink, and the project-local `.mcpp/.xlings/data/<index>` entry is a symlink back to the index root — so the walk escapes into the entire index checkout. In mcpp-index CI that tree contains a vendored xim-pkgindex whose issue template has a CJK filename; path_matches_glob spelled it narrow via generic_string(), which on MSVC under a non-CJK ANSI codepage throws std::system_error. The exception escaped main uncaught -> std::terminate -> __fastfail (0xC0000409), which git-bash reports as a bare exit 127. Three layers, all fixed: - expand_glob/expand_dir_glob prune directories named .mcpp — mcpp's own metadata dir is never a source dir, and pruning it severs the symlink escape at its origin (also stops walking the whole index per member). - path_matches_glob treats a name with no narrow spelling as 'no match' instead of letting the conversion tear down the build. - main() gains a last-resort catch: an escaped exception now names itself and exits 70 instead of a silent fastfail. Evidence: full-memory WER dump from the CI runner shows the throw chain scan_one_into -> expand_glob -> path_matches_glob -> _Convert_wide_to_narrow with the wide string '.mcpp\.xlings\data\compat\mcpp-0.0.95-windows-x86_64\registry\data\ xim-pkgindex\.github\ISSUE_TEMPLATE\bug-report---<CJK>.md'. Behavioral control on linux: release 0.0.95 compiles an out-of-tree extra.cpp through the .mcpp symlink (duplicate main at link); the fixed binary prunes it and builds clean. e2e 113 encodes both assertions. 35/35 unit tests pass. * release: 0.0.96 — windows scanner symlink-escape crash fix (#230) Version bump + CHANGELOG entry for the release notes extractor; also backfill a pointer entry for 0.0.95 (released without one). * release: bump MCPP_VERSION constant to 0.0.96 (match mcpp.toml) * release: keep .xlings.json bootstrap pin at 0.0.95 (bumped post-release once 0.0.96 is indexed) --------- Co-authored-by: Sunrisepeak <x.d2learn.org@gmail.com>
1 parent c77aca1 commit df985df

6 files changed

Lines changed: 125 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,40 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.96] — 2026-07-18
7+
8+
### 修复
9+
10+
- **[windows] `mcpp test --workspace` 静默崩溃(裸 exit 127,实为 0xC0000409)**
11+
(#230,修复 #231)。三层根因:
12+
1. 0.0.95 扫描器的 glob walk 新增 `follow_directory_symlink`,而项目本地
13+
`.mcpp/.xlings/data/<index>` 是指回索引根的**符号链接** → walk 逃逸出成员
14+
目录、扫遍整个索引 checkout(CI 里含 vendored xim-pkgindex);
15+
2. `path_matches_glob``generic_string()` 拼窄串,MSVC 在非 CJK ANSI 代码页
16+
(runner ACP=1252)下遇到中文文件名 `bug-report---问题反馈.md`
17+
`std::system_error`;
18+
3. 异常逃出 `main` 未捕获 → `std::terminate``__fastfail`(0xC0000409),
19+
git-bash 显示为无任何输出的 exit 127。
20+
- 修复:`expand_glob`/`expand_dir_glob` **按名剪枝 `.mcpp` 目录**(mcpp 自身
21+
元数据目录永远不是源码目录,从源头切断符号链接逃逸,顺带避免每个成员把
22+
整个索引树白走一遍);`path_matches_glob` 对无法窄化的文件名按"不匹配"
23+
跳过而不是摧毁构建;`main()` 增加最后防线 catch,逃逸异常打印真实错误并
24+
以 70 退出,不再静默 fastfail。
25+
- 取证:runner 开 WER 全内存 dump,崩溃栈+被转换字符串逐帧还原(记录见
26+
mcpplibs/mcpp-index `debug/mcpp230-windows-repro` 分支及 #230)。
27+
- 回归测试:`tests/e2e/113_scanner_mcpp_dir_prune.sh`(`.mcpp` 符号链接逃逸
28+
必须被剪枝;CJK 文件名不得致命)。linux 行为对照:0.0.95 会顺着该符号链接
29+
把项目外源码编进来(链接期 duplicate `main`),修复后构建干净。
30+
31+
## [0.0.95] — 2026-07-17
32+
33+
- 见 GitHub Release v0.0.95:声明式清单能力(features.sources / generated_files /
34+
cfg 条件 sources / per-glob flags,#223)、汇编源一等公民(.S/.s/.asm 进
35+
sources,NASM 按目标推导 `-f`,#220)、build.mcpp 补全(环境契约 + cross 下
36+
运行 + 依赖包执行,#222)。
37+
已知问题:#230(本版 windows workspace 崩溃,0.0.96 修复)、#232(冷环境
38+
`xim:nasm` 自举产出空载荷,待修)。
39+
640
## [0.0.94] — 2026-07-15
741

842
### 修复

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.95"
3+
version = "0.0.96"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/main.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,20 @@ import std;
55
import mcpp.cli;
66

77
int main(int argc, char* argv[]) {
8-
int rc = mcpp::cli::run(argc, argv);
8+
int rc;
9+
try {
10+
rc = mcpp::cli::run(argc, argv);
11+
} catch (const std::exception& e) {
12+
// Last-resort boundary: without it an escaped exception is
13+
// std::terminate — on Windows a silent 0xC0000409 that git-bash
14+
// reports as a bare exit 127 (mcpp#230 wore that mask). Name the
15+
// real error and exit with a recognizable internal-error code.
16+
std::println(std::cerr, "error: internal: unhandled exception: {}", e.what());
17+
rc = 70; // EX_SOFTWARE
18+
} catch (...) {
19+
std::println(std::cerr, "error: internal: unhandled non-standard exception");
20+
rc = 70;
21+
}
922
#ifdef __APPLE__
1023
// With statically linked libc++ (the macOS release linkage since
1124
// 0.0.50), static destruction can SIGABRT on exit — same issue xlings

src/modgraph/scanner.cppm

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,18 @@ bool path_matches_glob(const std::filesystem::path& candidate,
110110
// through a directory symlink back to its real location and break the
111111
// match (globs are about where a file appears in the tree, not where
112112
// its bits live).
113-
auto rel = candidate.lexically_relative(root).generic_string();
113+
std::string rel;
114+
try {
115+
rel = candidate.lexically_relative(root).generic_string();
116+
} catch (const std::exception&) {
117+
// MSVC's narrow conversion throws std::system_error when the
118+
// native (wide) name has no spelling in the ANSI codepage (e.g. a
119+
// CJK filename on an en-US host — mcpp#230 hit this on an issue
120+
// template inside a walked index tree). Such a name can never be
121+
// spelled in a glob or a compile command either: not a match, and
122+
// never a reason to tear down the whole build.
123+
return false;
124+
}
114125

115126
auto match = [](std::string_view s, std::string_view p) -> bool {
116127
// Simple recursive matcher.
@@ -242,6 +253,15 @@ std::vector<std::filesystem::path> expand_glob(const std::filesystem::path& root
242253
for (fs::recursive_directory_iterator end; !ec && it != end; it.increment(ec)) {
243254
auto& e = *it;
244255
if (e.is_directory(eec) && !eec) {
256+
// `.mcpp` is mcpp's own project metadata dir. Its xlings data
257+
// tree holds a SYMLINK back to each path-dep index root, so
258+
// following it walks that entire checkout (mcpp#230: the CI
259+
// workspace walked into a vendored xim-pkgindex and died on a
260+
// CJK filename). Never a source dir — prune by name.
261+
if (e.path().filename() == ".mcpp") {
262+
it.disable_recursion_pending();
263+
continue;
264+
}
245265
auto depth = static_cast<std::size_t>(it.depth());
246266
chain.resize(std::min(chain.size(), depth + 1));
247267
auto c = fs::canonical(e.path(), eec);
@@ -294,6 +314,11 @@ std::vector<std::filesystem::path> expand_dir_glob(const std::filesystem::path&
294314
!ec && it != end; it.increment(ec)) {
295315
auto& e = *it;
296316
if (!e.is_directory(eec) || eec) continue;
317+
// Same `.mcpp` prune as expand_glob (see comment there / mcpp#230).
318+
if (e.path().filename() == ".mcpp") {
319+
it.disable_recursion_pending();
320+
continue;
321+
}
297322
auto depth = static_cast<std::size_t>(it.depth());
298323
chain.resize(std::min(chain.size(), depth + 1));
299324
auto c = std::filesystem::canonical(e.path(), eec);

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.95";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.96";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc
3+
# mcpp#230: the source scanner follows directory symlinks (0.0.95), and the
4+
# project-local `.mcpp/.xlings/data/<index>` entry is a SYMLINK back to the
5+
# index root — following it walks that entire checkout. In CI that tree held
6+
# a CJK-named file, whose wide→narrow spelling throws on MSVC (bare
7+
# 0xC0000409 / exit 127). Assert both fixes:
8+
# 1. `.mcpp` is pruned from glob walks — an out-of-tree file reachable only
9+
# through the symlink must NOT be compiled (it double-defines main).
10+
# 2. A file whose name cannot be narrowed never aborts the build (covered
11+
# on MSVC by path_matches_glob's catch; here it just rides along).
12+
set -e
13+
14+
TMP=$(mktemp -d)
15+
trap "rm -rf $TMP" EXIT
16+
17+
cd "$TMP"
18+
mkdir -p outside/sub proj/src
19+
20+
cat > outside/sub/extra.cpp <<'EOF'
21+
int main() { return 9; } // double-defines main if the walk escapes
22+
EOF
23+
# CJK filename — on MSVC hosts with a non-CJK ANSI codepage this name has no
24+
# narrow spelling; the scanner must skip it, not tear down the build.
25+
cat > "outside/sub/问题反馈.cpp" <<'EOF'
26+
int cjk_never_compiled() { return 2; }
27+
EOF
28+
29+
cat > proj/src/main.cpp <<'EOF'
30+
int main() { return 0; }
31+
EOF
32+
cat > proj/mcpp.toml <<'EOF'
33+
[package]
34+
name = "prunetest"
35+
version = "0.1.0"
36+
37+
[build]
38+
sources = ["**/*.cpp"]
39+
EOF
40+
41+
# Simulate the path-dep index link mcpp creates under the project data dir.
42+
mkdir -p proj/.mcpp/.xlings/data
43+
ln -s "$TMP/outside" proj/.mcpp/.xlings/data/compat
44+
45+
cd proj
46+
"$MCPP" build
47+
"$MCPP" run
48+
49+
echo "PASS: .mcpp symlink escape pruned; build unaffected by unmatchable names"

0 commit comments

Comments
 (0)