From c6ae234d21345c27a5d6fbf3b94a9b9eea06713f Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 26 Jul 2026 02:43:22 +0800 Subject: [PATCH] fix: route explicit ninja goals through the manifest, not the command line (0.0.108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- CHANGELOG.md | 12 ++++ mcpp.toml | 2 +- src/build/ninja_backend.cppm | 44 +++++++++++++-- src/toolchain/fingerprint.cppm | 2 +- tests/e2e/164_ninja_goal_command_length.sh | 66 ++++++++++++++++++++++ 5 files changed, 120 insertions(+), 6 deletions(-) create mode 100755 tests/e2e/164_ninja_goal_command_length.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0d7a9..8c7b312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.108] — 2026-07-26 + +### 修复 + +- **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 都没有机会打印任何东西。 + + 症状因此极难归因:mcpp-index 的 Windows CI 自 0.0.104 起在 `ffmpeg` 成员上失败,日志里最后一行是无关的下载进度条,既没有 `build failed` 诊断,`target/.build_cache` 也没写出。它是全仓唯一大到能触发的成员,`mcpp build` / `mcpp run` 走 `default`(不带目标参数)则始终正常。 + + 修法是把目标集合写进 build.ninja 的一条 phony 聚合边,命令行只留一个词。清单文件没有长度限制,ninja 仍在**一次调用**内构建整个集合,并行度不变。e2e 164 锁住该机制。 + + 与 0.0.107 修掉的 soname 别名回归**同源**:都来自 0.0.104 引入的"显式 ninja 目标"([#274](https://github.com/mcpp-community/mcpp/pull/274))—— 一处设计改动,两个只在特定条件下现形的症状。 + ## [0.0.107] — 2026-07-25 ### 修复 diff --git a/mcpp.toml b/mcpp.toml index c1a6370..ea908d6 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.107" +version = "0.0.108" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 7b5b276..28bd095 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -1101,6 +1101,38 @@ std::string emit_ninja_string(const BuildPlan& plan) { return out; } +// Name of the phony edge that aggregates an explicit goal set. Not a path — +// ninja resolves it in the build dir, and no rule produces a file by this name. +constexpr std::string_view kGoalPhony = "mcpp-requested-goals"; + +// Explicit goals go into the MANIFEST, not onto ninja's command line. +// +// `mcpp test` names every shared prerequisite as a goal so a broken package +// source fails once, as a package error, rather than N times as identical +// per-test compile failures (see execute.cppm phase A). 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 truncates at 8191 characters — the command never ran, and the +// bare 127 came back with no output at all, from cmd.exe rather than from +// ninja or mcpp. +// +// A phony edge expresses exactly the same goal set with a one-word command +// line. The manifest has no length limit, and ninja still builds the whole set +// in one invocation, so parallelism is unchanged. +// +// Returns the goal to put on the command line, or empty for "build default". +std::string append_goal_phony(std::string& manifest, + const std::vector& goals) { + if (goals.empty()) return {}; + manifest += std::format("\nbuild {} : phony", kGoalPhony); + for (auto const& g : goals) { + manifest += ' '; + manifest += escape_ninja_path(g); + } + manifest += '\n'; + return std::string(kGoalPhony); +} + std::expected NinjaBackend::build(const BuildPlan& plan, const BuildOptions& opts) { auto t0 = std::chrono::steady_clock::now(); @@ -1113,7 +1145,9 @@ std::expected NinjaBackend::build(const BuildPlan& plan plan.outputDir}); auto ninja_path = plan.outputDir / "build.ninja"; - write_file(ninja_path, emit_ninja_string(plan)); + auto manifest = emit_ninja_string(plan); + auto goalArg = append_goal_phony(manifest, opts.ninjaTargets); + write_file(ninja_path, manifest); // compile_commands.json — via the dedicated module. auto flags = compute_flags(plan); @@ -1201,9 +1235,11 @@ std::expected NinjaBackend::build(const BuildPlan& plan } // Explicit goal targets: ninja builds only these outputs (and their - // prerequisites). Used by `mcpp test` to isolate per-test compiles. - for (auto& t : opts.ninjaTargets) - nargv.push_back(t); + // prerequisites). Used by `mcpp test` to isolate per-test compiles. The set + // travels through the manifest as a phony edge (append_goal_phony), so the + // command line stays one word no matter how many goals there are. + if (!goalArg.empty()) + nargv.push_back(goalArg); // Real env pairs for THIS run (the "@env" cache encoding above is only // for the fast path's later re-creation of the same environment). diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index be8b2c7..1b31aa4 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.107"; +inline constexpr std::string_view MCPP_VERSION = "0.0.108"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/164_ninja_goal_command_length.sh b/tests/e2e/164_ninja_goal_command_length.sh new file mode 100755 index 0000000..edf2c55 --- /dev/null +++ b/tests/e2e/164_ninja_goal_command_length.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# requires: +# 164_ninja_goal_command_length.sh — an explicit ninja goal set must travel +# through the manifest, never on the command line. +# +# `mcpp test` names every shared prerequisite as a goal so that 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 one command string for cmd.exe, which caps at 8191 characters +# — the command never ran and a bare 127 came back, from cmd.exe, with no output +# from either ninja or mcpp. mcpp-index's Windows job failed this way on the +# ffmpeg member from 0.0.104 (which introduced explicit goals) onward. +# +# The fix routes the goal set into build.ninja as a phony edge and puts one word +# on the command line. This test asserts that mechanism rather than the 8191 +# limit itself: reproducing the limit needs a package big enough to be far too +# slow here, and the limit does not exist on Linux/macOS at all. What is +# portable — and what actually regressed — is "goals go in the manifest". +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +mkdir -p proj/src proj/tests +cat > proj/mcpp.toml <<'EOF' +[package] +name = "goalset" +version = "0.1.0" +EOF + +# Enough units that a regression would be visible as a long argv, while still +# building in a couple of seconds. +for i in $(seq 1 30); do + echo "int unit_$i() { return $i; }" > "proj/src/unit_$i.cpp" +done +echo 'int main() { return 0; }' > proj/src/main.cpp +cat > proj/tests/sanity.cpp <<'EOF' +extern int unit_1(); +int main() { return unit_1() == 1 ? 0 : 1; } +EOF + +cd proj +"$MCPP" test > test.log 2>&1 || { cat test.log; echo "FAIL: mcpp test"; exit 1; } +grep -q "sanity ... ok" test.log || { cat test.log; exit 1; } + +NINJA=$(find target -name build.ninja | head -1) +[ -n "$NINJA" ] || { echo "FAIL: no build.ninja"; exit 1; } + +# The phony edge is how the goal set reaches ninja. +grep -q "^build mcpp-requested-goals : phony " "$NINJA" || { + echo "FAIL: explicit goals must be aggregated into a phony edge" + grep -n "phony" "$NINJA" | head; exit 1; } + +# And the goals themselves must not have been left on a command line. mcpp +# records the ninja program it ran in target/.build_cache; the goal set never +# belongs there either. +if [ -f target/.build_cache ]; then + if grep -qE 'obj/.*obj/.*obj/' target/.build_cache; then + echo "FAIL: goal paths leaked into the recorded ninja invocation" + cat target/.build_cache; exit 1 + fi +fi + +echo "PASS 164_ninja_goal_command_length"