diff --git a/CHANGELOG.md b/CHANGELOG.md index 735e96f6..b73b20ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,37 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.8.24.4] — 2026-08-24 + +### 修复 + +- **⚠️ `mcpp run --target X` 把 X 的构建记进了宿主的槽。** + + 损害不在这条命令 —— 它构建得完全正确 —— 而在下一条: + + ``` + $ mcpp run --target riscv64-none-elf # 正确,经 qemu 跑起来 + $ mcpp run + Running `target/riscv64-none-elf/…/bin/openkal-same-source` + exit=1 + ``` + + 缓存文件里逐字可见:一次 riscv 交叉构建写下的键是 `[target=]`。空键的 + 含义是「为这台机器构建」,而这正是快路径允许自己**直接 exec 缓存产物** + 的唯一依据。裸的 `mcpp run` 命中它,跳过整个 prepare_build —— 因此既没有 + 构建宿主目标,也没有解析 runner —— 把另一个目标的二进制在本机执行了。 + + 真因是一个丢掉的实参:`build_run_target` 收到 `target_triple`、用它建好 + overrides 并正确交叉构建,唯独没把它传给写缓存的那一步。`mcpp build` 的 + 两个调用点都传了。 + + ⭐ 同一个缺陷有**两条到达路径**:目标写在清单的 `[build] target` 里(已守), + 以及目标来自 `--target` flag(本次)。修好一条不会暴露另一条。 + + ⚠️ 回归测试的判据是**路径而不是退出码**。异架构产物 exec 会失败,所以 + 只看退出码的测试在那里因错误的理由通过,**而在同架构上完全测不到** —— + 后者更危险:用户不会看到崩溃,只会拿到一个 musl 产物冒充宿主产物。 + ## [2026.8.24.3] — 2026-08-24 ### 修复 diff --git a/mcpp.toml b/mcpp.toml index 295e1aa5..c667c5c0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.8.24.3" +version = "2026.8.24.4" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 70670699..58dae30a 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -1115,7 +1115,26 @@ export int build_run_target(const std::optional& targetName, auto ctx = prepare_build(/*print_fp=*/false, /*includeDevDeps=*/false, /*extraTargets=*/{}, ov); if (!ctx) { std::println(stderr, "error: {}", ctx.error()); return 2; } - if (auto rc = run_build_plan(*ctx, /*verbose=*/false, no_cache); rc != 0) + // ⚠️ `target_triple` IS PASSED, AND OMITTING IT WROTE A CROSS BUILD INTO + // THE HOST'S CACHE SLOT. + // + // The last argument becomes the cache entry's `[target=]` key. `cmd_build` + // supplies it at both of its call sites; this one did not, so + // `mcpp run --target X` built X correctly and then recorded the result as a + // HOST build. `try_fast_run` matches on `targetTriple.empty()`, so the very + // next bare `mcpp run` took that entry and exec'd the cross artifact + // directly — no build of the host target, and no runner: + // + // $ mcpp run --target riscv64-none-elf # correct, under qemu + // $ mcpp run + // Running `target/riscv64-none-elf/…/bin/openkal-same-source` + // exit=1 + // + // The comment on `try_fast_run` records the same defect reached through the + // MANIFEST's default target, and guards that door alone. This is the other + // door: the flag. Measured on 2026.8.24.3, from a clean `target/`. + if (auto rc = run_build_plan(*ctx, /*verbose=*/false, no_cache, target_triple); + rc != 0) return rc; // Find binary target diff --git a/src/version.cppm b/src/version.cppm index 277558ea..4be1e591 100644 --- a/src/version.cppm +++ b/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.8.24.3"; +inline constexpr std::string_view MCPP_VERSION = "2026.8.24.4"; } // namespace mcpp diff --git a/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh b/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh new file mode 100755 index 00000000..ad631323 --- /dev/null +++ b/tests/e2e/283_run_target_flag_owns_its_cache_slot.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# requires: gcc +# `mcpp run --target X` must not write X's build into the HOST's cache slot. +# +# ⚠️ WHAT MAKES THIS FAIL SILENTLY RATHER THAN LOUDLY. +# +# The build cache is keyed on the target triple, and `mcpp run`'s fast path +# matches the entry whose key is EMPTY — empty means "built for this machine", +# which is the only case where exec'ing a cached artifact directly is correct. +# `build_run_target` received the `--target` value, used it to prepare a correct +# cross build, and then did not pass it to the function that writes the cache. +# The cross build was recorded under the host's key. +# +# Nothing about that build is wrong and nothing reports it. The damage lands on +# the NEXT command: a bare `mcpp run` finds an entry claiming to be a host +# build, skips prepare_build entirely — so no host artifact is produced and no +# runner is resolved — and exec's the foreign binary on this machine. +# +# Measured on 2026.8.24.3 with a bare-metal target, from a clean `target/`: +# +# $ mcpp run --target riscv64-none-elf # correct, ran under qemu +# $ mcpp run +# Running `target/riscv64-none-elf/…/bin/openkal-same-source` +# exit=1 +# +# ⭐ THE ASSERTION IS ON THE PATH, NOT ON THE EXIT CODE. A foreign-architecture +# artifact fails to exec, so a test checking only the status would pass for the +# wrong reason there and MISS the defect wherever the foreign binary happens to +# run — which is what a same-architecture cross build does. What is wrong is +# which directory the program came from. +# +# The comment on `try_fast_run` records this same defect reached through the +# manifest's `[build] target` and guards that door. This is the other door. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +cd "$work" + +mkdir -p app/src +cat > app/mcpp.toml <<'TOML' +[package] +name = "runslot" +version = "0.1.0" +TOML + +cat > app/src/main.cpp <<'CPP' +#include +int main() { std::printf("host-artifact\n"); } +CPP + +cd app + +# A second target this machine can build AND run: the same architecture and OS, +# spelled with the other C library. A bare-metal target would expose the defect +# through a crash, which is the weaker signal — see the note above. +cross=x86_64-linux-musl + +if ! "$MCPP" build --target "$cross" >/dev/null 2>&1; then + echo "SKIP: this machine cannot build $cross"; exit 0 +fi + +rm -rf target +if ! "$MCPP" run --target "$cross" >/dev/null 2>&1; then + echo "SKIP: $cross builds but does not run here"; exit 0 +fi + +# ── The command under test ────────────────────────────────────────────────── +if ! out="$("$MCPP" run 2>&1)"; then + echo "FAIL: a bare \`mcpp run\` after a cross run exited non-zero" + printf '%s\n' "$out" | tail -5 + exit 1 +fi + +# ⭐ The load-bearing line. `Running `…`` names the artifact's path, and the +# path names the target it was built for. +ran="$(printf '%s\n' "$out" | sed -n 's/.*Running `\([^`]*\)`.*/\1/p' | head -1)" +if [ -z "$ran" ]; then + echo "FAIL: no \`Running\` line in the output"; printf '%s\n' "$out"; exit 1 +fi + +case "$ran" in + *"/$cross/"*) + echo "FAIL: a bare \`mcpp run\` exec'd the $cross artifact" + echo " $ran" + echo " The cross build was recorded under the host's cache key." + exit 1 ;; +esac + +if ! printf '%s\n' "$out" | grep -q 'host-artifact'; then + echo "FAIL: the host program's own output is missing" + printf '%s\n' "$out" + exit 1 +fi + +echo "OK: \`mcpp run --target $cross\` left the host's cache slot alone"