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"