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
439 changes: 439 additions & 0 deletions .agents/docs/2026-08-19-baremetal-phase3-usable-plan.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion .github/workflows/ci-linux-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ jobs:
# Invoked directly, a skip is visible: the script either prints its
# PASS line or it does not.
for t in tests/e2e/130_freestanding_riscv_build_and_run.sh \
tests/e2e/131_freestanding_bsp_supplies_everything.sh; do
tests/e2e/131_freestanding_bsp_supplies_everything.sh \
tests/e2e/132_freestanding_test_and_artifacts.sh; do
echo "=== $t ==="
bash "$t" 2>&1 | tee "$(basename "$t").log"
rc=${PIPESTATUS[0]}
Expand All @@ -188,6 +189,9 @@ jobs:
grep -q 'PASS: BSP supplies the sysroot' \
131_freestanding_bsp_supplies_everything.sh.log || {
echo "131 (ecosystem chain) skipped on the runner that must run it"; exit 1; }
grep -q 'PASS: bare-metal mcpp test names its failure' \
132_freestanding_test_and_artifacts.sh.log || {
echo "132 (test + artifacts) skipped on the runner that must run it"; exit 1; }

# ──────────────────────────────────────────────────────────────────
# Hermetic (no host toolchain): the ONLY environment class that
Expand Down
22 changes: 19 additions & 3 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -906,17 +906,33 @@ can produce them.

```bash
mcpp build --target riscv64-none-elf
mcpp run --target-triple riscv64-none-elf # via [target.<triple>].runner
mcpp run --target riscv64-none-elf # via [target.<triple>].runner
```

**Starting from a board package**

Almost nothing below has to be written by hand. A board-support package carries
the C library, the startup code, the memory layout and the emulator, so the
shortest path to a booting image is:

```bash
mcpp new blinky --template riscv-virt-rt
cd blinky && mcpp run
```

The generated manifest names no linker script, load address, libc or emulator —
it has no `[target.*]` section at all. The rest of this section describes what
such a package supplies, which is what to reach for when writing one for a board
that has none.

**What changes on a freestanding target**

| | |
|---|---|
| Link line | `-nostdlib -nostartfiles -static`, and nothing hosted — no crt files, no dynamic linker, no C++ runtime. The linker is addressed by **absolute path** (`-fuse-ld=<payload>/bin/ld.lld`), because `-fuse-ld=lld` resolves through `PATH` and finds GNU ld on any machine with binutils earlier on it. |
| ISA flags | `-march` / `-mabi` / `-mcmodel` come from the target table, so `--target <triple>` alone is enough to produce a correct object file. |
| `import std` | **Unavailable.** `std` is one module over the entire library — threads, filesystem and iostreams included — so there is no subset of it to build without an OS. The freestanding subset package replaces it, and mcpp's diagnostic names it. |
| Entry point | There is no `main`. Declare the target explicitly and point `main` at the file carrying `_start`. |
| `import std` | **Unavailable.** `std` is one module over the entire library — threads, filesystem and iostreams included — so there is no subset of it to build without an OS. What a firmware imports instead is the module its **board package** exports, which is where the target's C library is already wrapped. |
| Entry point | `int main()` works **as long as something supplies a `crt0`** — a board package normally does, and then a firmware's entry point is an ordinary `main` whose return value reaches the host through semihosting. Only a zero-libc board needs an explicit target whose `main` points at the file carrying `_start`. |

**A minimal firmware**

Expand Down
63 changes: 60 additions & 3 deletions docs/07-build-mcpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ is ignored, so diagnostics may be logged freely.
| `mcpp:source=<path>` *(0.0.100+)* | select a **pre-existing** source file into the build (absolute, or relative to the package root). Same downstream effect as `generated=`; use it for files the program *chose* (payload/vendored tree) rather than wrote — e.g. a per-target source selection over a large tarball |
| `mcpp:include-dir=<dir>` *(0.0.100+)* | add a **private** include directory (`-I`) for this package's own TUs (absolute, or relative to the package root; normalized). Replaces the `cxxflag=-I` + `cflag=-I` double emission |
| `mcpp:include-dir-after=<dir>` *(0.0.100+)* | like `include-dir`, but searched **after** the system directories (`-idirafter`) — for payload trees that shadow system headers |
| `mcpp:runner=<token>` *(2026.8.19.2+)* | one argv token of the command that EXECUTES this build's artifact, when the host cannot. Emitted once per token, in order; the artifact path is appended (or substituted for `{}`). Reaches the **consumer**. ⚠️ Emit the executable as an ABSOLUTE path, and only **one** dependency may supply it |
| `mcpp:link-script=<path>` *(2026.8.19+)* | link with this **linker script** (`-T`; relative resolves against the package root, and the emitted path is absolute because the link runs in the build directory). Reaches the **consumer**, unlike `include-dir` — a board's memory layout is the one thing a consumer cannot write for itself |
| `mcpp:rerun-if-changed=<path>` | re-run `build.mcpp` when this file changes |
| `mcpp:rerun-if-env-changed=<VAR>` | re-run `build.mcpp` when this env var changes |
Expand Down Expand Up @@ -101,9 +102,37 @@ int main() {
| `mcpp::rerun_if_changed_glob(pat)` *(2026.8.6.2+)* | `mcpp:rerun-if-changed-glob=` — re-run when the **set** of files matching `pat` changes (see below) |
| `mcpp::dep_bin(pkg, tool)` *(2026.8.5.1+)* | reads `MCPP_DEP_<PKG>_BIN_<TOOL>` — the absolute path of a **host tool** built by a dependency (see below) |
| `mcpp::link_script(p)` *(2026.8.19+)* | `mcpp:link-script=` |
| `mcpp::runner(tok)` *(2026.8.19.2+)* | `mcpp:runner=` — see below |
| `mcpp::xpkg_dir(ns, name)` / `mcpp::xpkg_dir(name)` *(2026.8.19+)* | the payload directory of a package this manifest declared in `[xlings] deps`; `""` when it was not declared or is not installed (see below) |
| `mcpp::action{…}.submit()` *(2026.8.5.1+)* | `mcpp:action=` — declares a **build-graph node** instead of doing the work here (see below) |

### `runner` — how the artifact is executed (2026.8.19.2+)

A board-support package knows the emulator, its machine model and its firmware
mode. It also knows where the emulator IS, which a static manifest cannot: the
payload path carries a home and a version.

```cpp
const char* qemu = mcpp::xpkg_dir("xim", "qemu-riscv");
mcpp::runner(std::format("{}/bin/qemu-system-riscv64", qemu).c_str());
for (auto a : {"-machine","virt","-nographic","-no-reboot","-kernel"})
mcpp::runner(a);
```

The consumer then needs no `[target.<triple>]` section at all. If it writes one
anyway, **it wins** — swapping `-bios default` for `-bios none -semihosting`
while debugging is a legitimate thing to want — and mcpp says which dependency
it overrode.

⚠️ **Emit the executable as an absolute path.** A bare name resolves through
`PATH` to a shim that dispatches against its own owner home, which is not
necessarily the home this build uses.

⚠️ **Exactly one dependency may supply a runner.** Two board-support packages
both claiming to know how to run the artifact is a configuration error, and
mcpp reports it naming both rather than merging them into an argv that is
neither one's.

### Finding an `[xlings] deps` payload: `xpkg_dir` (2026.8.19+)

`dep_dir` answers for **mcpp** dependencies. An xlings package is a different
Expand Down Expand Up @@ -297,16 +326,44 @@ write it yourself). mcpp uses that two ways:
with an upgrade hint. Continuing would silently drop directives the build
depends on — and "the build succeeded but the flag never arrived" is the
worst class of build bug.
- Because the two sides then provably agree, an **unrecognized directive is an
error** rather than a warning: within one protocol version it can only be a
typo.
- An **unrecognized directive is an error** rather than a warning, and the error
names *both* possible causes. It cannot name one: the protocol number is
stamped by whichever mcpp **compiled** the program, not carried by the
package, so a package written for a newer mcpp arrives at an older one
wearing the older engine's number. Two matching numbers therefore say nothing
about whether the key came from the future.

A `printf`-style program announces nothing, so it keeps the historical
warn-and-ignore behaviour. That surface is **frozen at the eleven directives in
the table above** — it still works and will keep working, but new capabilities
land only in the typed API. Prefer `import mcpp;` for anything intended to
maintain.

#### A package that needs a newer mcpp

When a published package calls a typed function this mcpp does not have, the
compile error naming it is followed by:

```
The `mcpp` build module this engine bundles does not have that name.
Either the package was written for a newer mcpp (try `mcpp self update`;
this is mcpp 2026.8.19.2), or the name is misspelled …
```

The package cannot handle this itself, and it is worth knowing why — the
obvious guard does not compile:

```cpp
if constexpr (requires { mcpp::runner("qemu"); }) // ✗ hard error when absent
mcpp::runner("qemu");
```

A `requires`-expression over a **qualified name that does not exist** is
ill-formed, not `false`. So there is no in-language feature probe, and a
package that adopts a new directive states its floor in prose (its README) and
relies on the diagnostic above. Such a package should name the mcpp version it
requires.

### `import std;` (mcpp 2026.8.2.1+)

A `build.mcpp` may `import std;` (and `import std.compat;`), alone or together
Expand Down
20 changes: 17 additions & 3 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -804,17 +804,31 @@ cxxflags = ["-march=x86-64-v2"]

```bash
mcpp build --target riscv64-none-elf
mcpp run --target-triple riscv64-none-elf # 经 [target.<triple>].runner
mcpp run --target riscv64-none-elf # 经 [target.<triple>].runner
```

**从板级支持包起步**

下面这些几乎都不需要手写。板级支持包(BSP)自带 C 库、启动代码、内存布局和模拟器,
所以跑起一个镜像的最短路径是:

```bash
mcpp new blinky --template riscv-virt-rt
cd blinky && mcpp run
```

生成的 manifest 里没有链接脚本、没有加载地址、没有 libc、没有模拟器 —— 连
`[target.*]` 段都没有。本节余下的内容讲的是**这样一个包提供了什么**,也就是要给
一块还没有 BSP 的板子写一个时该照着做什么。

**freestanding target 上有什么不同**

| | |
|---|---|
| 链接线 | `-nostdlib -nostartfiles -static`,且不带任何 hosted 的东西 —— 没有 crt 文件、没有动态链接器、没有 C++ 运行时。链接器用**绝对路径**寻址(`-fuse-ld=<载荷>/bin/ld.lld`),因为 `-fuse-ld=lld` 走 `PATH` 解析,在任何 binutils 排前面的机器上都会找到 GNU ld。 |
| ISA flag | `-march` / `-mabi` / `-mcmodel` 来自 target 表,所以只写 `--target <triple>` 就足以产出正确的目标文件。 |
| `import std` | **不可用。** `std` 是覆盖整个库的一个模块 —— 线程、文件系统、iostreams 全在内 —— 没有 OS 就没有它的子集可编。freestanding 子集包取代它,mcpp 的诊断会点名。 |
| 入口点 | 没有 `main`。显式声明 target,并把 `main` 指向携带 `_start` 的那个文件。 |
| `import std` | **不可用。** `std` 是覆盖整个库的一个模块 —— 线程、文件系统、iostreams 全在内 —— 没有 OS 就没有它的子集可编。固件真正 import 的是**板级包导出的模块**,目标的 C 库已经在那里包好了。 |
| 入口点 | **只要有人提供 `crt0`,`int main()` 就能用** —— 板级支持包通常就提供它,于是固件的入口就是普通的 `main`,它的返回值经 semihosting 传回宿主。**只有零 libc 的板子**才需要显式声明 target 并把 `main` 指向携带 `_start` 的那个文件。 |

**一个最小固件**

Expand Down
51 changes: 49 additions & 2 deletions docs/zh/07-build-mcpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mcpp build # 编译 + 运行 build.mcpp,然后构建工程
| `mcpp:source=<path>` *(0.0.100+)* | 把一份**既有**源文件选入构建(绝对路径,或相对包根)。下游效果与 `generated=` 相同;语义区别在于文件是程序*选中*的(tarball payload / vendored 源树)而非程序写出的——例如对大型源码包做 per-target 源选择 |
| `mcpp:include-dir=<dir>` *(0.0.100+)* | 为本包自身 TU 增加一个**私有** include 目录(`-I`;绝对路径或相对包根,自动规范化)。取代过去 `cxxflag=-I` + `cflag=-I` 的双重裸发 |
| `mcpp:include-dir-after=<dir>` *(0.0.100+)* | 同 `include-dir`,但排在系统目录**之后**搜索(`-idirafter`)——用于会遮蔽系统头的 payload 源树 |
| `mcpp:runner=<token>` *(2026.8.19.2+)* | 执行本次构建产物的命令的**一个 argv token**(宿主跑不了它时)。一个 token 一次调用、按顺序;产物路径会被追加(或替换 `{}`)。**到达消费者**。⚠️ 可执行文件要发**绝对路径**,且**只能有一个**依赖提供它 |
| `mcpp:link-script=<path>` *(2026.8.19+)* | 用这个**链接脚本**链接(`-T`;相对路径按包根解析,发出的是绝对路径,因为链接是在构建目录里跑的)。与 `include-dir` 不同,它**到达消费者** —— 板子的内存布局恰恰是消费者写不出来的那一项 |
| `mcpp:rerun-if-changed=<path>` | 该文件变化时重跑 `build.mcpp` |
| `mcpp:rerun-if-env-changed=<VAR>` | 该环境变量变化时重跑 `build.mcpp` |
Expand Down Expand Up @@ -94,9 +95,32 @@ int main() {
| `mcpp::rerun_if_changed_glob(pat)` *(2026.8.6.2+)* | `mcpp:rerun-if-changed-glob=` —— 匹配 `pat` 的文件**集合**发生变化时重跑(见下) |
| `mcpp::dep_bin(pkg, tool)` *(2026.8.5.1+)* | 读 `MCPP_DEP_<PKG>_BIN_<TOOL>` —— 依赖构建出的 **host 工具**的绝对路径(见下) |
| `mcpp::link_script(p)` *(2026.8.19+)* | `mcpp:link-script=` |
| `mcpp::runner(tok)` *(2026.8.19.2+)* | `mcpp:runner=` —— 见下 |
| `mcpp::xpkg_dir(ns, name)` / `mcpp::xpkg_dir(name)` *(2026.8.19+)* | 本 manifest 在 `[xlings] deps` 里声明的包的载荷目录;没声明或没安装时返回 `""`(见下) |
| `mcpp::action{…}.submit()` *(2026.8.5.1+)* | `mcpp:action=` —— **声明一个构建图节点**,而不是在这里把活干了(见下) |

### `runner` —— 产物的执行方式(2026.8.19.2+)

板级支持包知道模拟器、机器型号和固件模式,也知道模拟器**在哪** —— 而静态 manifest
写不出来:载荷路径里带着 home 和版本号。

```cpp
const char* qemu = mcpp::xpkg_dir("xim", "qemu-riscv");
mcpp::runner(std::format("{}/bin/qemu-system-riscv64", qemu).c_str());
for (auto a : {"-machine","virt","-nographic","-no-reboot","-kernel"})
mcpp::runner(a);
```

这样消费者**完全不需要 `[target.<triple>]` 段**。它若还是写了,**以它为准** ——
调试时把 `-bios default` 换成 `-bios none -semihosting` 是正当需求 —— 且 mcpp 会说明
它覆盖了哪个依赖。

⚠️ **可执行文件要发绝对路径。** 裸名会经 `PATH` 解析到一个 shim,而 shim 按**拥有它
的 home** 派发,那未必是本次构建用的 home。

⚠️ **只能有一个依赖提供 runner。** 两个板级支持包都声称知道怎么跑这个产物是配置
错误;mcpp 会**同时点名两个**并报错,而不是把它们并成一个谁也不是的 argv。

### 找到 `[xlings] deps` 的载荷:`xpkg_dir`(2026.8.19+)

`dep_dir` 回答的是 **mcpp** 依赖。xlings 包是另一个命名空间、另一套 store 布局,
Expand Down Expand Up @@ -266,13 +290,36 @@ mcpp 会播下一个带着该声明的占位文件,使 prepare 期的扫描与

- 程序声明的协议**高于** mcpp 所理解的 → **拒绝执行**,并给出升级提示。继续跑会
静默丢掉构建依赖的指令,而「构建成功了但那个 flag 根本没到」是最难查的一类问题。
- 既然双方已被证明一致,**未知指令就是错误**而不是警告:在同一个协议版本内,
它只可能是拼写错误。
- **未知指令是错误**而不是警告,而且这条错误会把**两种可能的原因都说出来**。
它没法只说一种:协议号是由**编译**该程序的那个 mcpp 现场打上的,并不由包本身携带
—— 于是一个写给新 mcpp 的包到了老 mcpp 手里,身上戴的是老引擎的号。
**两个号一致因此完全不能说明这个键是不是来自未来。**

`printf` 风格的程序什么都不声明,因此保留历史上的「警告并忽略」行为。这一面
**冻结在上表的 11 条指令**上——它仍然能用、也会继续能用,但新能力只在类型化 API 里
落地。**要长期维护的程序请用 `import mcpp;`。**

#### 一个需要更新 mcpp 的包

当已发布的包调用了当前 mcpp 没有的类型化函数时,点名的编译错误之后会跟着:

```
The `mcpp` build module this engine bundles does not have that name.
Either the package was written for a newer mcpp (try `mcpp self update`;
this is mcpp 2026.8.19.2), or the name is misspelled …
```

**包自己处理不了这件事**,而原因值得知道 —— 最直觉的那道防护编译不过:

```cpp
if constexpr (requires { mcpp::runner("qemu"); }) // ✗ 名字不存在时是硬错误
mcpp::runner("qemu");
```

`requires` 表达式作用在一个**不存在的限定名**上时是 ill-formed,而**不是求值为
`false`**。所以语言内没有特性探测这条路:采用了新指令的包只能在自己的 README 里
用文字写明版本下限,并依赖上面那条诊断。**这类包应当写清楚它需要哪个版本的 mcpp。**

### `import std;`(mcpp 2026.8.2.1+)

`build.mcpp` 可以 `import std;`(以及 `import std.compat;`),单用或与
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.19.1"
version = "2026.8.19.2"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
Loading
Loading