Skip to content

Commit a826c44

Browse files
authored
fix: route explicit ninja goals through the manifest, not the command line (0.0.108) (#285)
`mcpp test` names every shared prerequisite as a ninja goal so a broken package source fails once, as a package error, instead of N times as identical per-test compile failures. For a large package that is thousands of object paths: FFmpeg's 2281 translation units produced a 50,781-character argv. Windows joins argv into a single command string for cmd.exe, which caps at 8191 characters, so the command never ran at all — the 127 came back from cmd.exe, before ninja or mcpp could print anything. That silence is what made it hard to place. mcpp-index's Windows job has failed on the ffmpeg member since 0.0.104, and the log's last line was an unrelated download progress bar: no "build failed" diagnostic, no ninja output, and target/.build_cache never written. ffmpeg is the only member large enough to trip the limit, and `mcpp build` / `mcpp run` drive `default` with no goal arguments, so both stayed green throughout. Emit the goal set as a phony edge in build.ninja and pass its one-word name. The manifest has no length limit, and ninja still builds the whole set in a single invocation, so parallelism is unchanged. Same origin as the soname-alias regression fixed in 0.0.107: both come from the explicit-goal-targets change in #274 — one design change, two symptoms that only appear under conditions the test suite did not cover. e2e 164 asserts the mechanism (goals live in the manifest) rather than the 8191 limit: reproducing the limit needs a package far too slow to build here, and the limit does not exist on Linux or macOS. It is red on 0.0.107 and green here. Verified: unit 37/37; e2e 155 passed / 1 failed / 5 skipped, the one failure (22_doctor_cache_publish) reproducing identically on the released binary under the same MCPP_HOME. Found by publishing an instrumented Windows build to a throwaway prerelease and pinning it from a stripped-down mcpp-index probe PR — four rounds of black-box probing could not see inside the process.
1 parent 3901c1a commit a826c44

5 files changed

Lines changed: 120 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.108] — 2026-07-26
7+
8+
### 修复
9+
10+
- **Windows:显式 ninja 目标集不再撑爆命令行(0.0.104 起的回归)。** `mcpp test` 会把每一个共享前置(除测试自身 main 外的全部编译单元 + 非测试链接产物)列为 ninja 目标,好让包内源码出错时报**一次**包级错误,而不是 N 次雷同的逐测试编译失败。对大包来说这就是几千个对象路径 —— FFmpeg 的 2281 个编译单元拼出 **50,781 字符**的 argv。Windows 把 argv 合并成单条命令串交给 cmd.exe,而后者上限 **8191 字符**,于是命令**根本没有执行**:返回的是 cmd.exe 的裸 127,ninja 和 mcpp 都没有机会打印任何东西。
11+
12+
症状因此极难归因:mcpp-index 的 Windows CI 自 0.0.104 起在 `ffmpeg` 成员上失败,日志里最后一行是无关的下载进度条,既没有 `build failed` 诊断,`target/.build_cache` 也没写出。它是全仓唯一大到能触发的成员,`mcpp build` / `mcpp run``default`(不带目标参数)则始终正常。
13+
14+
修法是把目标集合写进 build.ninja 的一条 phony 聚合边,命令行只留一个词。清单文件没有长度限制,ninja 仍在**一次调用**内构建整个集合,并行度不变。e2e 164 锁住该机制。
15+
16+
与 0.0.107 修掉的 soname 别名回归**同源**:都来自 0.0.104 引入的"显式 ninja 目标"([#274](https://github.com/mcpp-community/mcpp/pull/274))—— 一处设计改动,两个只在特定条件下现形的症状。
17+
618
## [0.0.107] — 2026-07-25
719

820
### 修复

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

src/build/ninja_backend.cppm

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,38 @@ std::string emit_ninja_string(const BuildPlan& plan) {
11011101
return out;
11021102
}
11031103

1104+
// Name of the phony edge that aggregates an explicit goal set. Not a path —
1105+
// ninja resolves it in the build dir, and no rule produces a file by this name.
1106+
constexpr std::string_view kGoalPhony = "mcpp-requested-goals";
1107+
1108+
// Explicit goals go into the MANIFEST, not onto ninja's command line.
1109+
//
1110+
// `mcpp test` names every shared prerequisite as a goal so a broken package
1111+
// source fails once, as a package error, rather than N times as identical
1112+
// per-test compile failures (see execute.cppm phase A). For a large package
1113+
// that is thousands of object paths: FFmpeg's 2281 translation units produced
1114+
// a 50,781-character argv. Windows joins argv into a single command string for
1115+
// cmd.exe, which truncates at 8191 characters — the command never ran, and the
1116+
// bare 127 came back with no output at all, from cmd.exe rather than from
1117+
// ninja or mcpp.
1118+
//
1119+
// A phony edge expresses exactly the same goal set with a one-word command
1120+
// line. The manifest has no length limit, and ninja still builds the whole set
1121+
// in one invocation, so parallelism is unchanged.
1122+
//
1123+
// Returns the goal to put on the command line, or empty for "build default".
1124+
std::string append_goal_phony(std::string& manifest,
1125+
const std::vector<std::string>& goals) {
1126+
if (goals.empty()) return {};
1127+
manifest += std::format("\nbuild {} : phony", kGoalPhony);
1128+
for (auto const& g : goals) {
1129+
manifest += ' ';
1130+
manifest += escape_ninja_path(g);
1131+
}
1132+
manifest += '\n';
1133+
return std::string(kGoalPhony);
1134+
}
1135+
11041136
std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan,
11051137
const BuildOptions& opts) {
11061138
auto t0 = std::chrono::steady_clock::now();
@@ -1113,7 +1145,9 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
11131145
plan.outputDir});
11141146

11151147
auto ninja_path = plan.outputDir / "build.ninja";
1116-
write_file(ninja_path, emit_ninja_string(plan));
1148+
auto manifest = emit_ninja_string(plan);
1149+
auto goalArg = append_goal_phony(manifest, opts.ninjaTargets);
1150+
write_file(ninja_path, manifest);
11171151

11181152
// compile_commands.json — via the dedicated module.
11191153
auto flags = compute_flags(plan);
@@ -1201,9 +1235,11 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
12011235
}
12021236

12031237
// Explicit goal targets: ninja builds only these outputs (and their
1204-
// prerequisites). Used by `mcpp test` to isolate per-test compiles.
1205-
for (auto& t : opts.ninjaTargets)
1206-
nargv.push_back(t);
1238+
// prerequisites). Used by `mcpp test` to isolate per-test compiles. The set
1239+
// travels through the manifest as a phony edge (append_goal_phony), so the
1240+
// command line stays one word no matter how many goals there are.
1241+
if (!goalArg.empty())
1242+
nargv.push_back(goalArg);
12071243

12081244
// Real env pairs for THIS run (the "@env" cache encoding above is only
12091245
// for the fast path's later re-creation of the same environment).

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.107";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.108";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env bash
2+
# requires:
3+
# 164_ninja_goal_command_length.sh — an explicit ninja goal set must travel
4+
# through the manifest, never on the command line.
5+
#
6+
# `mcpp test` names every shared prerequisite as a goal so that a broken package
7+
# source fails once as a package error instead of N times as identical per-test
8+
# compile failures. For a large package that is thousands of object paths:
9+
# FFmpeg's 2281 translation units produced a 50,781-character argv. Windows
10+
# joins argv into one command string for cmd.exe, which caps at 8191 characters
11+
# — the command never ran and a bare 127 came back, from cmd.exe, with no output
12+
# from either ninja or mcpp. mcpp-index's Windows job failed this way on the
13+
# ffmpeg member from 0.0.104 (which introduced explicit goals) onward.
14+
#
15+
# The fix routes the goal set into build.ninja as a phony edge and puts one word
16+
# on the command line. This test asserts that mechanism rather than the 8191
17+
# limit itself: reproducing the limit needs a package big enough to be far too
18+
# slow here, and the limit does not exist on Linux/macOS at all. What is
19+
# portable — and what actually regressed — is "goals go in the manifest".
20+
set -e
21+
22+
TMP=$(mktemp -d)
23+
trap "rm -rf $TMP" EXIT
24+
cd "$TMP"
25+
26+
mkdir -p proj/src proj/tests
27+
cat > proj/mcpp.toml <<'EOF'
28+
[package]
29+
name = "goalset"
30+
version = "0.1.0"
31+
EOF
32+
33+
# Enough units that a regression would be visible as a long argv, while still
34+
# building in a couple of seconds.
35+
for i in $(seq 1 30); do
36+
echo "int unit_$i() { return $i; }" > "proj/src/unit_$i.cpp"
37+
done
38+
echo 'int main() { return 0; }' > proj/src/main.cpp
39+
cat > proj/tests/sanity.cpp <<'EOF'
40+
extern int unit_1();
41+
int main() { return unit_1() == 1 ? 0 : 1; }
42+
EOF
43+
44+
cd proj
45+
"$MCPP" test > test.log 2>&1 || { cat test.log; echo "FAIL: mcpp test"; exit 1; }
46+
grep -q "sanity ... ok" test.log || { cat test.log; exit 1; }
47+
48+
NINJA=$(find target -name build.ninja | head -1)
49+
[ -n "$NINJA" ] || { echo "FAIL: no build.ninja"; exit 1; }
50+
51+
# The phony edge is how the goal set reaches ninja.
52+
grep -q "^build mcpp-requested-goals : phony " "$NINJA" || {
53+
echo "FAIL: explicit goals must be aggregated into a phony edge"
54+
grep -n "phony" "$NINJA" | head; exit 1; }
55+
56+
# And the goals themselves must not have been left on a command line. mcpp
57+
# records the ninja program it ran in target/.build_cache; the goal set never
58+
# belongs there either.
59+
if [ -f target/.build_cache ]; then
60+
if grep -qE 'obj/.*obj/.*obj/' target/.build_cache; then
61+
echo "FAIL: goal paths leaked into the recorded ninja invocation"
62+
cat target/.build_cache; exit 1
63+
fi
64+
fi
65+
66+
echo "PASS 164_ninja_goal_command_length"

0 commit comments

Comments
 (0)