Skip to content

Commit c0161e0

Browse files
committed
feat(target): the target owns its C library, so no package has to name one
Both bare-metal packages carried `[xlings] deps = ["xim:picolibc-riscv@1.8.12"]` and the standard-library subset also carried `xim:llvm`. That pinned a board package and an implementation-neutral library alike to one libc, one ISA, one toolchain and one version of each — none of which is a property of either. The cause was a gap in the target model rather than sloppy packaging. A hosted target has always had its C library resolved for it: musl rides inside its gcc payload and glibc arrives through PayloadPaths, which is why nobody writes `xim:glibc` in a manifest. A freestanding target pins a generic clang, which carries no target libc, and there was no axis for one — so the requirement leaked outward into every package. kKnownTargets now names the target's C library beside its compiler pin. It is installed through the same channel `[xlings] deps` already use, its headers go on every compile line and its directory on the link search path. Location is a target fact; selection stays a board fact — a board still chooses `-lcrt0-semihost` over a UART crt0, and still names its linker script. Two interfaces let a package ask instead of declare: `mcpp::toolchain_dir()` for headers that ship with the toolchain (libc++'s, for the freestanding subset) and `mcpp::sysroot_dir()` for a file inside the target's C library (a linker script). Both follow whatever `[toolchain]` and `--target` resolved. Three things this turned up: * `-L` appended to the ordinary ldflags is discarded — a freestanding link line is replaced wholesale, so it has to go through LinkInputs. Measured: the flag was built and then simply was not there. * `link-script` resolves a bare name against the PACKAGE root, so a board cannot rely on the linker's search path for it; it asks for the sysroot. * e2e/131's privacy assertion was testing the wrong thing. It proved a dependency's `include-dir` stays private by showing `#include <stdio.h>` fails — but with the libc owned by the target that now correctly SUCCEEDS, exactly as on a hosted build. Split into two: the target's C headers must reach the consumer, and a header the BOARD ships must not.
1 parent b7711fe commit c0161e0

15 files changed

Lines changed: 347 additions & 32 deletions

.agents/docs/2026-08-19-baremetal-phase3-usable-plan.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,3 +506,36 @@ manifest」的目录里,manifest 永远是最新的那个文件,快路径**永
506506
依赖缓存键有 triple,**没有 triple 隐含的那组 flag**。而**哪些 flag 由 triple 隐含是 mcpp 的决定**,
507507
会随版本变、triple 字符串不变 ⇒ 升级后复用了升级前的 BMI,硬失败,错误只点名一个 `.pcm`
508508
已加 `targetImpliedFlags` 轴(hosted 为空,不动任何现有键)。
509+
510+
### 11.6 ⭐ 用户指出的架构错误:目标的 libc 不该由包声明
511+
512+
我把 `xim:picolibc-riscv@1.8.12` 写进了 BSP 和 std 子集两个包的 `[xlings] deps`
513+
用户指出三条,**三条都对**:不该绑 libc、不该绑 riscv、不该绑编译器;而且 freestanding
514+
在普通宿主上一样能用(实测:`-ffreestanding` + 宿主 libc,同一份代码编得过)。
515+
516+
⚠️ **真因是引擎的一个结构性缺口**,不是包写得随意:
517+
518+
| | 编译器 | 目标 libc |
519+
|---|---|---|
520+
| hosted(`x86_64-linux-musl`) | 目标表 `pin` **自动** | musl 在 gcc 载荷里 / glibc 走 `PayloadPaths` —— **自动**,没人写过 `xim:glibc` |
521+
| freestanding | 目标表 `pin = llvm@22.1.8` **自动** | **没有任何一条轴** ⇒ 外溢到每个包 |
522+
523+
**`TargetInfo``sysroot`**(和 `pin` 并列),复用现有 `[xlings] deps` 物化通道
524+
安装,引擎把 `-isystem <sysroot>/include/<档位>``-L <sysroot>/lib/<档位>` 放上去。
525+
526+
**位置是目标的事实,选择是板级的事实**:引擎给位置,BSP 用**裸名**`-lcrt0-semihost`
527+
`-lc` `-lsemihost` 并指定链接脚本。
528+
529+
同时补两个「问引擎」的接口:`mcpp::toolchain_dir()``mcpp::sysroot_dir()`
530+
**两个包的 `[xlings] deps` 里 libc 与编译器全部消失**,std 子集变成与架构/libc/实现无关。
531+
532+
#### 实施中撞到的三条
533+
534+
1. ⚠️ **`-L` 加到 `f.ld` 上会被丢掉** —— freestanding **整条替换**链接线(否则载荷 cfg
535+
注入宿主 dynamic linker),所以必须放进 `LinkInputs`。实测:flag 拼出来了,然后不在。
536+
2. ⚠️ **`link-script` 的裸名会被按包根绝对化**`picolibcpp.ld` 指到包目录里。板级包要
537+
`sysroot_dir()` 自己拼 —— 它知道**要哪个脚本**,不知道**在哪**
538+
3. ⚠️ **e2e/131 的私有性断言判据失效了**:它用 `#include <stdio.h>` 编不过来证明依赖的
539+
`include-dir` 不到达消费者。libc 归目标之后 `<stdio.h>` **本来就该编得过**(和宿主一样)。
540+
⇒ 拆成两条:目标 C 头**必须**到达(正面断言),板级包**自己的****必须不**到达。
541+
**原来那条测的其实是「libc 从哪来」,不是「作用域对不对」。**

docs/05-mcpp-toml.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,7 @@ that has none.
931931
|---|---|
932932
| 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. |
933933
| ISA flags | `-march` / `-mabi` / `-mcmodel` come from the target table, so `--target <triple>` alone is enough to produce a correct object file. |
934+
| C library | **The target's**, resolved by mcpp from the target's own row exactly as the compiler is — a bare-metal project declares no libc, just as a hosted one declares no glibc. Its headers reach every translation unit and its directory is on the link search path, so a board package selects out of it by bare name (`-lc`, `-lcrt0-semihost`). *Which* objects and *which* linker script remain board decisions. |
934935
| Exceptions and RTTI | **Off**, on every translation unit including a dependency's. There is no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would otherwise pull in `__cxa_throw` and three more undefined symbols. It belongs to the target rather than to a project's `cxxflags` because a BMI records it — a dependency compiled with exceptions cannot be imported by a unit without them. |
935936
| `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. Two ordinary dependencies replace it: the **board package** wraps the target's C library, and **`std-freestanding`** carries the parts of the standard library that need no OS (103 of libc++'s 110 headers, measured). |
936937
| 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`. |

docs/07-build-mcpp.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,28 @@ both claiming to know how to run the artifact is a configuration error, and
133133
mcpp reports it naming both rather than merging them into an argv that is
134134
neither one's.
135135
136+
### Asking instead of declaring: `toolchain_dir` / `sysroot_dir` (2026.8.19.4+)
137+
138+
```cpp
139+
const char* tc = mcpp::toolchain_dir(); // the resolved toolchain's payload root
140+
const char* sr = mcpp::sysroot_dir(); // the TARGET's C library root, or ""
141+
```
142+
143+
A package that needs headers shipped by the toolchain — libc++'s, for a
144+
freestanding standard-library subset — or a file inside the target's C library
145+
— a linker script, for a board-support package — asks for the directory rather
146+
than declaring a dependency on the thing that provides it.
147+
148+
⚠️ The difference is not cosmetic. Declaring `xim:llvm` pins a package to one
149+
standard-library implementation; declaring `xim:picolibc-riscv@1.8.12` pins it
150+
to one C library, one architecture and one version. Neither is a property of a
151+
package whose content is implementation-neutral. Asking follows whatever
152+
`[toolchain]` and `--target` actually resolved.
153+
154+
`sysroot_dir()` is empty on a hosted target: there the C library arrives with
155+
the compiler payload or through the runtime binding, and nothing has to look
156+
for it.
157+
136158
### Finding an `[xlings] deps` payload: `xpkg_dir` (2026.8.19+)
137159

138160
`dep_dir` answers for **mcpp** dependencies. An xlings package is a different

docs/zh/05-mcpp-toml.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,7 @@ cd blinky && mcpp run
827827
|---|---|
828828
| 链接线 | `-nostdlib -nostartfiles -static`,且不带任何 hosted 的东西 —— 没有 crt 文件、没有动态链接器、没有 C++ 运行时。链接器用**绝对路径**寻址(`-fuse-ld=<载荷>/bin/ld.lld`),因为 `-fuse-ld=lld``PATH` 解析,在任何 binutils 排前面的机器上都会找到 GNU ld。 |
829829
| ISA flag | `-march` / `-mabi` / `-mcmodel` 来自 target 表,所以只写 `--target <triple>` 就足以产出正确的目标文件。 |
830+
| C 库 | **属于 target**,由 mcpp 从目标自己那一行解析,和解析编译器同理 —— 裸机工程不声明 libc,正如宿主工程不声明 glibc。它的头进入每一个翻译单元,它的目录进入链接搜索路径,所以板级包用**裸名**选库(`-lc``-lcrt0-semihost`)。**选哪个**启动对象、**用哪份**链接脚本仍然是板级决定。 |
830831
| 异常与 RTTI | **关闭**,作用于每一个翻译单元,依赖的也不例外。没有 unwinder、没有 `libc++abi`,谁都抛不了;否则光是 `std::optional::value()` 就会拉进 `__cxa_throw` 等四个未定义符号。它属于 **target** 而不是工程的 `cxxflags`,因为 **BMI 会记录这个配置** —— 带异常编出来的依赖,不带异常的单元 import 不进来。 |
831832
| `import std` | **不可用。** `std` 是覆盖整个库的一个模块 —— 线程、文件系统、iostreams 全在内 —— 没有 OS 就没有它的子集可编。取代它的是两个普通依赖:**板级包**包住目标的 C 库,**`std-freestanding`** 提供标准库里不需要 OS 的那部分(实测 libc++ 110 个头里的 103 个)。 |
832833
| 入口点 | **只要有人提供 `crt0`,`int main()` 就能用** —— 板级支持包通常就提供它,于是固件的入口就是普通的 `main`,它的返回值经 semihosting 传回宿主。**只有零 libc 的板子**才需要显式声明 target 并把 `main` 指向携带 `_start` 的那个文件。 |

docs/zh/07-build-mcpp.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@ for (auto a : {"-machine","virt","-nographic","-no-reboot","-kernel"})
121121
⚠️ **只能有一个依赖提供 runner。** 两个板级支持包都声称知道怎么跑这个产物是配置
122122
错误;mcpp 会**同时点名两个**并报错,而不是把它们并成一个谁也不是的 argv。
123123
124+
### 问,而不是声明:`toolchain_dir` / `sysroot_dir`(2026.8.19.4+)
125+
126+
```cpp
127+
const char* tc = mcpp::toolchain_dir(); // 已解析工具链的载荷根目录
128+
const char* sr = mcpp::sysroot_dir(); // 目标的 C 库根目录,没有则为 ""
129+
```
130+
131+
一个包需要工具链自带的头(比如 freestanding 标准库子集要的 libc++ 头),或者需要
132+
目标 C 库里的某个**文件**(比如板级支持包要的链接脚本)时,应当**问这个目录在哪**,
133+
而不是去声明一个依赖来把它拽进来。
134+
135+
⚠️ 这不是写法差异。声明 `xim:llvm` 会把包**钉死在一个标准库实现**上;声明
136+
`xim:picolibc-riscv@1.8.12` 会把它钉死在**一个 C 库、一种架构、一个版本**上。而这些
137+
都不是一个内容全是标准规定的名字的包的属性。****则会跟随 `[toolchain]`
138+
`--target` 真正解析到的结果。
139+
140+
宿主目标上 `sysroot_dir()` 为空:那里 C 库随编译器载荷或运行时绑定而来,没人需要找它。
141+
124142
### 找到 `[xlings] deps` 的载荷:`xpkg_dir`(2026.8.19+)
125143

126144
`dep_dir` 回答的是 **mcpp** 依赖。xlings 包是另一个命名空间、另一套 store 布局,

src/build/build_program.cppm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ export namespace mcpp::build {
3838
// target/profile/feature change re-runs the program.
3939
struct BuildProgramEnv {
4040
std::string targetTriple; // resolved canonical triple; "" = host
41+
// The resolved toolchain's payload root and the target's own C library
42+
// root. Both exist so a package can ASK instead of DECLARE — see
43+
// hostprogram::toolchain_dir / sysroot_dir for why declaring was wrong.
44+
std::string toolchainDir;
45+
std::string targetSysroot;
4146
std::string profile; // effective profile name (dev/release/…)
4247
std::vector<std::string> features; // active feature closure of the package
4348
// Artifact home (bin/cache/out). Empty → <root>/target/.build-mcpp (the
@@ -295,6 +300,12 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv
295300
e.emplace_back("MCPP_TARGET_ENV", t.env);
296301
}
297302
e.emplace_back("MCPP_HOST", hostT);
303+
// Always emitted, empty when they do not apply: a build program reads
304+
// these through `env_or`, which cannot tell "absent" from "empty", and an
305+
// absent variable would make the answer depend on whatever the parent
306+
// process happened to export.
307+
e.emplace_back("MCPP_TOOLCHAIN_DIR", env.toolchainDir);
308+
e.emplace_back("MCPP_TARGET_SYSROOT", env.targetSysroot);
298309
e.emplace_back("MCPP_PROFILE", env.profile);
299310
e.emplace_back("MCPP_OUT_DIR", outDir.string());
300311
e.emplace_back("MCPP_MANIFEST_DIR", root.string());

src/build/flags.cppm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,12 +1285,38 @@ CompileFlags compute_flags(const BuildPlan& plan) {
12851285
f.cc += prefix;
12861286
f.as += mcpp::freestanding::assemble_prefix(*spec);
12871287

1288+
// ── The target's C library, from the TARGET, not from a package ──
1289+
//
1290+
// A hosted target never made anyone write this down: musl rides
1291+
// inside its gcc payload and glibc arrives through PayloadPaths.
1292+
// Bare metal pins a generic clang, which carries no target libc,
1293+
// and until this the gap leaked outward — every board-support
1294+
// package and every library had to declare
1295+
// `[xlings] deps = ["xim:picolibc-riscv@1.8.12"]`, which bound
1296+
// each of them to one libc, one ISA and one version of both.
1297+
//
1298+
// What the engine supplies is exactly the libc's LOCATION. Which
1299+
// objects to start with, which linker script, which libraries —
1300+
// `-lcrt0-semihost` versus a UART crt0 — remain board decisions
1301+
// and stay in the board package. Location is a target fact;
1302+
// selection is a board fact.
1303+
// Compile side: the target's C headers. Link side is NOT here —
1304+
// it goes into LinkInputs below, because the freestanding link
1305+
// line is replaced wholesale and anything added to f.ld here is
1306+
// discarded.
1307+
if (!plan.toolchain.targetSysrootInclude.empty()) {
1308+
const auto flag =
1309+
" -isystem " + ninjaEsc(plan.toolchain.targetSysrootInclude);
1310+
f.cxx += flag; f.cc += flag; f.as += flag;
1311+
}
1312+
12881313
mcpp::freestanding::LinkInputs in;
12891314
// Absolute path, not `-fuse-ld=lld`: the name resolves through
12901315
// PATH and finds GNU ld on any machine with binutils earlier on
12911316
// it, which then dies with 'unrecognised emulation mode'.
12921317
// Reproduced on this toolchain 2026-08-19.
12931318
in.lld = mcpp::freestanding::resolve_lld(plan.toolchain.binaryPath);
1319+
in.sysrootLib = plan.toolchain.targetSysrootLib;
12941320
// ⚠️ `--no-default-config` FIRST, and it is not hygiene.
12951321
//
12961322
// The llvm payload ships bin/clang++.cfg with an unconditional

src/build/hostprogram.cppm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,27 @@ inline const char* target_env() { return env_or("MCPP_TARGET_E
182182
inline const char* host() { return env_or("MCPP_HOST"); }
183183
inline const char* profile() { return env_or("MCPP_PROFILE"); }
184184
inline const char* out_dir() { return env_or("MCPP_OUT_DIR"); }
185+
186+
// Where the TOOLCHAIN mcpp resolved for this build lives — the payload root,
187+
// the directory whose `bin/` holds the driver.
188+
//
189+
// ⚠️ This exists so a package never has to DECLARE a toolchain. A package that
190+
// needs headers the toolchain ships (libc++'s, for a freestanding standard
191+
// library subset) previously had to put `xim:llvm` in `[xlings] deps`, which
192+
// pinned it to one implementation — and the measured fact is that the same
193+
// subset works over libstdc++'s freestanding mode too, so pinning was not
194+
// merely inelegant, it closed a road. Asking here follows whatever
195+
// `[toolchain]` actually resolved.
196+
inline const char* toolchain_dir() { return env_or("MCPP_TOOLCHAIN_DIR"); }
197+
198+
// Where the TARGET's C library lives, for targets that have one of their own
199+
// (today: bare metal). Same argument one line up: the libc is a property of
200+
// the target, mcpp resolves it from the target's own row, and a package that
201+
// needs to name a FILE inside it (a linker script) asks rather than declares.
202+
//
203+
// Empty on a hosted target — there the libc comes with the compiler payload or
204+
// through the runtime binding, and nothing has to look for it.
205+
inline const char* sysroot_dir() { return env_or("MCPP_TARGET_SYSROOT"); }
185206
inline const char* manifest_dir() { return env_or("MCPP_MANIFEST_DIR"); }
186207
inline bool has_feature(const char* name) {
187208
char buf[256] = "MCPP_FEATURE_";

0 commit comments

Comments
 (0)