Skip to content

Commit 8ebbbc8

Browse files
committed
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.
1 parent c77aca1 commit 8ebbbc8

3 files changed

Lines changed: 89 additions & 2 deletions

File tree

‎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);
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)