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
33 changes: 33 additions & 0 deletions .agents/docs/2026-08-19-baremetal-phase3-usable-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,36 @@ mcpplibs.std.freestanding = "0.1"
⚠️ 一般化:**诊断里的每一条建议都是一个承诺**,而承诺是要被兑现的。写「加这一行」
之前必须先确认那一行今天能跑通 —— 这条和 [[issue427-absence-treated-as-contradiction]]
里那条错误建议是同一形状。

### 11.4 ⚠️ 2026.8.19.2 发出去就带着一个 bug:`mcpp build` 之后 `mcpp run` 直接执行裸机 ELF

**发布后**用发布的二进制做真实验证时才发现 —— 这正是「本地真实验证」这一步存在的理由。

```
$ mcpp new blinky --template riscv-virt-rt && cd blinky
$ mcpp run # ✅ 走 qemu,打印正常
$ mcpp build && mcpp run # ❌ Running `target/riscv64-none-elf/…/bin/blinky`
# 没有模拟器、没有输出、exit=1
```

**根因**:`try_fast_run` 直接 exec 缓存里的产物,它的注释写着
「`mcpp run` never takes a --target flag」所以只匹配 `targetTriple == ""` 的条目。
调用点确实挡住了 `--target` 旗标 —— 但**工程可以把目标写在 manifest 里**
(`[build] target`),而这个拼写**从来没进过缓存键**:交叉构建写下的条目 key 是 `""`,
读回来就被当成宿主构建。

⇒ 修法是把这个函数的**前置条件**写成代码:它 exec 产物本身,所以只在产物属于**本机**时
才成立。声明了默认目标就一律退回完整 prepare(runner 正是在那里解析的)。

#### ⚠️ 第一版回归测试是假绿 —— 而且我差点就信了

把 build-then-run 加进 e2e 131 的既有工程后,**关掉修复它照样通过**。
探针(给 `try_fast_run` 的 16 个 `return nullopt` 各打一个编号)指出是 **BAIL 11**:
`mcpp.toml` 比 `build.ninja` 新,快路径**本来就没被走到**。

原因很反直觉:**重建并不会重写内容未变的 `build.ninja`**,所以在一个「原地编辑过
manifest」的目录里,manifest 永远是最新的那个文件,快路径**永久关闭**。
⇒ 测试必须**新建一个干净工程**。改完后先确认它在关掉修复时**变红**,再确认修复后变绿。

**教训**:`mcpp run` 单独跑是对的,`mcpp build && mcpp run` 才错 —— **顺序本身就是被测
对象**。而 130/131/132 三个测试都恰好先 `run`,所以谁也看不见。
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.19.2"
version = "2026.8.19.3"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
28 changes: 27 additions & 1 deletion src/build/execute.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,9 @@ struct FastPathIdentity {
// extensions are module interfaces in THIS project, and this is already
// the only manifest read on the fast path.
mcpp::ExtensionTable extTable;
// `[build] target` — the project's DEFAULT cross target, and the reason
// try_fast_run cannot assume the artifact runs here. See its use.
std::string defaultTarget;
};

std::optional<FastPathIdentity>
Expand All @@ -813,6 +816,7 @@ fast_path_identity(const std::filesystem::path& projectRoot,
mcpp::build::resolve_cache_mode(*m, ""))),
m->resources.files,
mcpp::extension_table_for(m->buildConfig.moduleExtensions),
m->buildConfig.target,
};
}

Expand Down Expand Up @@ -916,7 +920,7 @@ export std::optional<int> try_fast_build(const std::filesystem::path& projectRoo

// mcpp#225 (E2): `mcpp run`'s fast path. Mirrors try_fast_build's
// fingerprint/freshness gate against the SAME cache entry `mcpp build`
// wrote (targetTriple == "" — `mcpp run` never takes a --target flag), then
// wrote (targetTriple == "" — a HOST build; see the precondition below), then
// on a hit runs ninja and execs the cached run-target directly — skipping
// prepare_build (toolchain resolution + full modgraph scan) entirely.
// Returns nullopt when there's no usable cache entry (build_run_target
Expand All @@ -928,6 +932,28 @@ std::optional<int> try_fast_run(const std::filesystem::path& projectRoot,
auto want = fast_path_identity(projectRoot);
if (!want) return std::nullopt;

// ⚠️ THE precondition of this whole function: it exec's the cached
// artifact itself, so it is only ever valid when that artifact is for THIS
// machine.
//
// The header above used to justify matching `targetTriple == ""` with
// "`mcpp run` never takes a --target flag". The caller does guard the
// flag — but a project can name its target in the MANIFEST instead, and
// that spelling never reaches the cache key, so a cross build's entry is
// written as "" and read back as if it were a host build.
//
// Measured on the shipped 2026.8.19.2, on the first two commands a
// bare-metal user runs after `mcpp new`:
//
// $ mcpp build && mcpp run
// Running `target/riscv64-none-elf/…/bin/blinky` ← no emulator
// exit=1
//
// `mcpp run` alone was correct; only build-then-run reached the cache. So
// the fast path is off whenever a default target is declared, and the full
// prepare — which is what resolves the runner — takes over.
if (!want->defaultTarget.empty()) return std::nullopt;

auto entries = read_build_cache(projectRoot);
const BuildCacheEntry* match = nullptr;
for (auto& e : entries) {
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.19.2";
inline constexpr std::string_view MCPP_VERSION = "2026.8.19.3";

} // namespace mcpp
47 changes: 47 additions & 0 deletions tests/e2e/131_freestanding_bsp_supplies_everything.sh
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,51 @@ if "$MCPP" build --target riscv64-none-elf > leak.log 2>&1; then
exit 1
fi

# ── ⚠️ build, THEN run: the sequence that shipped broken in 2026.8.19.2 ─────
#
# `mcpp run` on its own was correct; the SECOND invocation was not. With the
# target named in the MANIFEST rather than on the command line, `mcpp build`
# wrote a fast-path cache entry keyed "" (the CLI override, which was empty),
# and `mcpp run` matched that as a HOST build and exec'd the RISC-V ELF:
#
# Running `target/riscv64-none-elf/.../bin/firmware` ← no emulator
# exit=1
#
# ⚠️ A FRESH project, not the one above, and that is load-bearing. The run
# fast path also requires mcpp.toml to be older than build.ninja, and a
# rebuild does not rewrite build.ninja when its content is unchanged — so in a
# directory whose manifest has been edited in place the fast path is already
# off and this check silently tests nothing. Verified by disabling the fix and
# watching this go red.
cd "$TMP"
mkdir -p order/src
cd order
cat > mcpp.toml <<'EOF'
[package]
name = "order"
version = "0.1.0"

[build]
target = "riscv64-none-elf"

[dependencies]
board = { path = "../board" }
EOF
cat > src/main.cpp <<'EOF'
import board;
extern "C" int main() { board::print("ORDER-OK\n"); return 0; }
EOF

"$MCPP" build > buildfirst.log 2>&1 || {
cat buildfirst.log; echo "build with a manifest target failed"; exit 1; }
"$MCPP" run > runafter.log 2>&1 || true
grep -q 'ORDER-OK' runafter.log || {
cat runafter.log
echo "build-then-run lost the runner (the artifact was exec'd directly)"
exit 1; }
# Two-sided: assert the emulator is really in the command line, so a future
# change that happens to make the ELF runnable here cannot pass this silently.
grep -q 'qemu-system-riscv64' runafter.log || {
cat runafter.log; echo "the run did not go through the emulator"; exit 1; }

echo "PASS: BSP supplies the sysroot, linker script and runtime; consumer only depends on it"
Loading