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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### 修复
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 = "2026.8.24.3"
version = "2026.8.24.4"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
21 changes: 20 additions & 1 deletion src/build/execute.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,26 @@ export int build_run_target(const std::optional<std::string>& 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
Expand Down
2 changes: 1 addition & 1 deletion src/version.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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
97 changes: 97 additions & 0 deletions tests/e2e/283_run_target_flag_owns_its_cache_slot.sh
Original file line number Diff line number Diff line change
@@ -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 <cstdio>
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"
Loading