Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### 修复
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
44 changes: 40 additions & 4 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>& 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<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan,
const BuildOptions& opts) {
auto t0 = std::chrono::steady_clock::now();
Expand All @@ -1113,7 +1145,9 @@ std::expected<BuildResult, BuildError> 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);
Expand Down Expand Up @@ -1201,9 +1235,11 @@ std::expected<BuildResult, BuildError> 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).
Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
66 changes: 66 additions & 0 deletions tests/e2e/164_ninja_goal_command_length.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading