From 8ebbbc80a0f62948b309d876b94c9757d6e08ae3 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 18 Jul 2026 06:36:23 +0800 Subject: [PATCH 1/4] fix(scanner): prune .mcpp from glob walks; never throw on unnarrowable names (#230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/` 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---.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. --- src/main.cpp | 15 +++++++- src/modgraph/scanner.cppm | 27 +++++++++++++- tests/e2e/113_scanner_mcpp_dir_prune.sh | 49 +++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100755 tests/e2e/113_scanner_mcpp_dir_prune.sh diff --git a/src/main.cpp b/src/main.cpp index b62c9b3f..b249748e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,20 @@ import std; import mcpp.cli; int main(int argc, char* argv[]) { - int rc = mcpp::cli::run(argc, argv); + int rc; + try { + rc = mcpp::cli::run(argc, argv); + } catch (const std::exception& e) { + // Last-resort boundary: without it an escaped exception is + // std::terminate — on Windows a silent 0xC0000409 that git-bash + // reports as a bare exit 127 (mcpp#230 wore that mask). Name the + // real error and exit with a recognizable internal-error code. + std::println(std::cerr, "error: internal: unhandled exception: {}", e.what()); + rc = 70; // EX_SOFTWARE + } catch (...) { + std::println(std::cerr, "error: internal: unhandled non-standard exception"); + rc = 70; + } #ifdef __APPLE__ // With statically linked libc++ (the macOS release linkage since // 0.0.50), static destruction can SIGABRT on exit — same issue xlings diff --git a/src/modgraph/scanner.cppm b/src/modgraph/scanner.cppm index b4b43d14..04988964 100644 --- a/src/modgraph/scanner.cppm +++ b/src/modgraph/scanner.cppm @@ -110,7 +110,18 @@ bool path_matches_glob(const std::filesystem::path& candidate, // through a directory symlink back to its real location and break the // match (globs are about where a file appears in the tree, not where // its bits live). - auto rel = candidate.lexically_relative(root).generic_string(); + std::string rel; + try { + rel = candidate.lexically_relative(root).generic_string(); + } catch (const std::exception&) { + // MSVC's narrow conversion throws std::system_error when the + // native (wide) name has no spelling in the ANSI codepage (e.g. a + // CJK filename on an en-US host — mcpp#230 hit this on an issue + // template inside a walked index tree). Such a name can never be + // spelled in a glob or a compile command either: not a match, and + // never a reason to tear down the whole build. + return false; + } auto match = [](std::string_view s, std::string_view p) -> bool { // Simple recursive matcher. @@ -242,6 +253,15 @@ std::vector expand_glob(const std::filesystem::path& root for (fs::recursive_directory_iterator end; !ec && it != end; it.increment(ec)) { auto& e = *it; if (e.is_directory(eec) && !eec) { + // `.mcpp` is mcpp's own project metadata dir. Its xlings data + // tree holds a SYMLINK back to each path-dep index root, so + // following it walks that entire checkout (mcpp#230: the CI + // workspace walked into a vendored xim-pkgindex and died on a + // CJK filename). Never a source dir — prune by name. + if (e.path().filename() == ".mcpp") { + it.disable_recursion_pending(); + continue; + } auto depth = static_cast(it.depth()); chain.resize(std::min(chain.size(), depth + 1)); auto c = fs::canonical(e.path(), eec); @@ -294,6 +314,11 @@ std::vector expand_dir_glob(const std::filesystem::path& !ec && it != end; it.increment(ec)) { auto& e = *it; if (!e.is_directory(eec) || eec) continue; + // Same `.mcpp` prune as expand_glob (see comment there / mcpp#230). + if (e.path().filename() == ".mcpp") { + it.disable_recursion_pending(); + continue; + } auto depth = static_cast(it.depth()); chain.resize(std::min(chain.size(), depth + 1)); auto c = std::filesystem::canonical(e.path(), eec); diff --git a/tests/e2e/113_scanner_mcpp_dir_prune.sh b/tests/e2e/113_scanner_mcpp_dir_prune.sh new file mode 100755 index 00000000..3a8af3f1 --- /dev/null +++ b/tests/e2e/113_scanner_mcpp_dir_prune.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# requires: gcc +# mcpp#230: the source scanner follows directory symlinks (0.0.95), and the +# project-local `.mcpp/.xlings/data/` entry is a SYMLINK back to the +# index root — following it walks that entire checkout. In CI that tree held +# a CJK-named file, whose wide→narrow spelling throws on MSVC (bare +# 0xC0000409 / exit 127). Assert both fixes: +# 1. `.mcpp` is pruned from glob walks — an out-of-tree file reachable only +# through the symlink must NOT be compiled (it double-defines main). +# 2. A file whose name cannot be narrowed never aborts the build (covered +# on MSVC by path_matches_glob's catch; here it just rides along). +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +mkdir -p outside/sub proj/src + +cat > outside/sub/extra.cpp <<'EOF' +int main() { return 9; } // double-defines main if the walk escapes +EOF +# CJK filename — on MSVC hosts with a non-CJK ANSI codepage this name has no +# narrow spelling; the scanner must skip it, not tear down the build. +cat > "outside/sub/问题反馈.cpp" <<'EOF' +int cjk_never_compiled() { return 2; } +EOF + +cat > proj/src/main.cpp <<'EOF' +int main() { return 0; } +EOF +cat > proj/mcpp.toml <<'EOF' +[package] +name = "prunetest" +version = "0.1.0" + +[build] +sources = ["**/*.cpp"] +EOF + +# Simulate the path-dep index link mcpp creates under the project data dir. +mkdir -p proj/.mcpp/.xlings/data +ln -s "$TMP/outside" proj/.mcpp/.xlings/data/compat + +cd proj +"$MCPP" build +"$MCPP" run + +echo "PASS: .mcpp symlink escape pruned; build unaffected by unmatchable names" From 2dbd1be19189ecbe6466683aa81aa5497ea94a0f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 18 Jul 2026 08:41:39 +0800 Subject: [PATCH 2/4] =?UTF-8?q?release:=200.0.96=20=E2=80=94=20windows=20s?= =?UTF-8?q?canner=20symlink-escape=20crash=20fix=20(#230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version bump + CHANGELOG entry for the release notes extractor; also backfill a pointer entry for 0.0.95 (released without one). --- .xlings.json | 2 +- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ mcpp.toml | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.xlings.json b/.xlings.json index 625440d6..998893d7 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "0.0.95" + "mcpp": "0.0.96" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f621850..71f69ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,40 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.96] — 2026-07-18 + +### 修复 + +- **[windows] `mcpp test --workspace` 静默崩溃(裸 exit 127,实为 0xC0000409)** + (#230,修复 #231)。三层根因: + 1. 0.0.95 扫描器的 glob walk 新增 `follow_directory_symlink`,而项目本地 + `.mcpp/.xlings/data/` 是指回索引根的**符号链接** → walk 逃逸出成员 + 目录、扫遍整个索引 checkout(CI 里含 vendored xim-pkgindex); + 2. `path_matches_glob` 用 `generic_string()` 拼窄串,MSVC 在非 CJK ANSI 代码页 + (runner ACP=1252)下遇到中文文件名 `bug-report---问题反馈.md` 抛 + `std::system_error`; + 3. 异常逃出 `main` 未捕获 → `std::terminate` → `__fastfail`(0xC0000409), + git-bash 显示为无任何输出的 exit 127。 + - 修复:`expand_glob`/`expand_dir_glob` **按名剪枝 `.mcpp` 目录**(mcpp 自身 + 元数据目录永远不是源码目录,从源头切断符号链接逃逸,顺带避免每个成员把 + 整个索引树白走一遍);`path_matches_glob` 对无法窄化的文件名按"不匹配" + 跳过而不是摧毁构建;`main()` 增加最后防线 catch,逃逸异常打印真实错误并 + 以 70 退出,不再静默 fastfail。 + - 取证:runner 开 WER 全内存 dump,崩溃栈+被转换字符串逐帧还原(记录见 + mcpplibs/mcpp-index `debug/mcpp230-windows-repro` 分支及 #230)。 + - 回归测试:`tests/e2e/113_scanner_mcpp_dir_prune.sh`(`.mcpp` 符号链接逃逸 + 必须被剪枝;CJK 文件名不得致命)。linux 行为对照:0.0.95 会顺着该符号链接 + 把项目外源码编进来(链接期 duplicate `main`),修复后构建干净。 + +## [0.0.95] — 2026-07-17 + +- 见 GitHub Release v0.0.95:声明式清单能力(features.sources / generated_files / + cfg 条件 sources / per-glob flags,#223)、汇编源一等公民(.S/.s/.asm 进 + sources,NASM 按目标推导 `-f`,#220)、build.mcpp 补全(环境契约 + cross 下 + 运行 + 依赖包执行,#222)。 + 已知问题:#230(本版 windows workspace 崩溃,0.0.96 修复)、#232(冷环境 + `xim:nasm` 自举产出空载荷,待修)。 + ## [0.0.94] — 2026-07-15 ### 修复 diff --git a/mcpp.toml b/mcpp.toml index a0ffae65..777d868d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.95" +version = "0.0.96" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] From 9bcd819a24804654b2dccedc8bd170519685f370 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 18 Jul 2026 08:56:03 +0800 Subject: [PATCH 3/4] release: bump MCPP_VERSION constant to 0.0.96 (match mcpp.toml) --- src/toolchain/fingerprint.cppm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index adec22b7..015e69d8 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.95"; +inline constexpr std::string_view MCPP_VERSION = "0.0.96"; struct FingerprintInputs { Toolchain toolchain; From 62e6394091270d78219c9ac37df266e069109f0a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 18 Jul 2026 09:11:56 +0800 Subject: [PATCH 4/4] release: keep .xlings.json bootstrap pin at 0.0.95 (bumped post-release once 0.0.96 is indexed) --- .xlings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.xlings.json b/.xlings.json index 998893d7..625440d6 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "0.0.96" + "mcpp": "0.0.95" } }