Skip to content

Commit 80ceb82

Browse files
committed
feat(build): 三个目标查询,解除板级包对编译器与 C 库的隐式耦合
⚠️ 这次解除的耦合**在任何 manifest 里都看不见**。`riscv-virt-rt` 自 #459 起既不 声明 LLVM 也不声明 picolibc,却依然服务不了第二种工具链或第二份 C 库 —— 因为它把 `clang_rt.builtins-riscv64`(compiler-rt 的事实,GCC 下是 `libgcc`)与 `rv64gc/lp64d`(picolibc 的 multilib 约定)写进了自己的 build.mcpp。 **声明出来的依赖可见可评审;写死的名字不可见,而且只在换东西时才失败** —— 恰好是 没人在看的时候。 ## 判据用的是既有的那条 位置是目标的事实,选择是板级的事实。据此: * 哪个 builtins 库存在,由**编译器**决定,而且没有板子会选择不要它 (rv64 上的触发者是 picolibc printf 的 128 位移位,ISA 无对应指令); * 某个 ISA 档位的库放在哪个子目录,是 **C 库的约定**,零板级输入。 两者引擎本来就知道 ⇒ `mcpp::target_builtins_lib()` / `mcpp::target_libc_profile()`。 第三个 `mcpp::target_libc()` **不消除耦合,而是让它显形**:crt0 的对象名在 picolibc 与 newlib 之间确实不同,而那确实是板级选择。显式分支可读可扩展, 藏在字面量里的假设两者都不是。 ## 单一读取点(又一次:它原本要变成两处) `bpEnv` 在两处构造(根工程 / 每个依赖)。四个值各推导两遍,会得到「作为根工程拿到 对的、作为依赖拿到旧的」的板级包 —— 而那只在**消费方**的构建里失败,是更难查的方向。 ⇒ 收敛为 `fill_target_build_env()`,两处同一次调用。 ## ⚠️ 一处契约不一致,由实测暴露 零 libc 档上 `profile` 仍返回 `rv64gc/lp64d`,而它按名字是 **C 库的**子目录。 没有 C 库时它不是任何东西的约定,发出去等于把一条不存在的路径交给内核,访问器的 名字就成了假话。⇒ 已收紧:**三个 libc 面的答案一起为空**,而 builtins 是编译器 事实,保留。 ## 验证 新增 e2e/134,七步全部**两侧钉**:目标行的 libc 可用 ↔ `sysroot = ""` 后 `<stdio.h>` 确实找不到;零 libc 仍产出镜像;三个查询在 rv64/rv32/零 libc 三种 配置下的值;裸名被解析期拒绝。 ⭐ **做了 revert-A 探针**:把 `effective_sysroot` 的覆盖分支注释掉重建后,e2e 精确 地死在第 4 步那条承重断言(`sysroot = "" did not remove the C library`)。不做这一步 不算写完 —— 这两个特性都属于「在已配置好的机器上,在与不在长得一模一样」那一类。
1 parent 32f5e53 commit 80ceb82

5 files changed

Lines changed: 289 additions & 14 deletions

File tree

src/build/build_program.cppm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ struct BuildProgramEnv {
4343
// hostprogram::toolchain_dir / sysroot_dir for why declaring was wrong.
4444
std::string toolchainDir;
4545
std::string targetSysroot;
46+
// Three more answers a board-support package would otherwise hardcode.
47+
//
48+
// ⚠️ THE COUPLING THESE REMOVE IS INVISIBLE IN A MANIFEST. `riscv-virt-rt`
49+
// declares no dependency on LLVM or on picolibc — #459 removed those — and
50+
// yet it named `clang_rt.builtins-riscv64` (a compiler-rt fact, `libgcc`
51+
// under GCC) and `rv64gc/lp64d` (picolibc's multilib convention). A
52+
// declared dependency is visible; a hardcoded name is not, and it fails
53+
// only when something is swapped.
54+
//
55+
// The division of labour is the same one the layering already uses:
56+
// location is a target fact, selection is a board fact. Which builtins
57+
// library exists is decided by the compiler, and no board chooses to go
58+
// without one; where a profile's libraries live is the C library's
59+
// convention. Both belong to the engine, which knows them already.
60+
std::string targetBuiltinsLib; // "clang_rt.builtins-riscv64" | "gcc" | ""
61+
std::string targetLibcProfile; // "rv64gc/lp64d" | ""
62+
std::string targetLibc; // "picolibc-riscv" | "" (zero-libc tier)
4663
std::string profile; // effective profile name (dev/release/…)
4764
std::vector<std::string> features; // active feature closure of the package
4865
// Artifact home (bin/cache/out). Empty → <root>/target/.build-mcpp (the
@@ -306,6 +323,9 @@ contract_env(const fs::path& root, const fs::path& outDir, const BuildProgramEnv
306323
// process happened to export.
307324
e.emplace_back("MCPP_TOOLCHAIN_DIR", env.toolchainDir);
308325
e.emplace_back("MCPP_TARGET_SYSROOT", env.targetSysroot);
326+
e.emplace_back("MCPP_TARGET_BUILTINS_LIB", env.targetBuiltinsLib);
327+
e.emplace_back("MCPP_TARGET_LIBC_PROFILE", env.targetLibcProfile);
328+
e.emplace_back("MCPP_TARGET_LIBC", env.targetLibc);
309329
e.emplace_back("MCPP_PROFILE", env.profile);
310330
e.emplace_back("MCPP_OUT_DIR", outDir.string());
311331
e.emplace_back("MCPP_MANIFEST_DIR", root.string());

src/build/hostprogram.cppm

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,48 @@ inline const char* toolchain_dir() { return env_or("MCPP_TOOLCHAI
203203
// Empty on a hosted target — there the libc comes with the compiler payload or
204204
// through the runtime binding, and nothing has to look for it.
205205
inline const char* sysroot_dir() { return env_or("MCPP_TARGET_SYSROOT"); }
206+
207+
// ── Three answers a board-support package would otherwise hardcode ───────────
208+
//
209+
// ⚠️ The coupling these remove does not appear in any manifest. A board package
210+
// can declare no dependency on LLVM and none on picolibc — and still be unable
211+
// to serve a second toolchain or a second C library, because it wrote their
212+
// names into its `build.mcpp`. A declared dependency is visible and reviewable;
213+
// a hardcoded name fails only when something is swapped, which is exactly when
214+
// nobody is looking for it.
215+
//
216+
// The rule that decides what belongs here is the one the layering already
217+
// uses: LOCATION IS A TARGET FACT, SELECTION IS A BOARD FACT.
218+
219+
// The compiler's builtins library, by bare name: `clang_rt.builtins-riscv64`
220+
// for an LLVM payload, `gcc` for a GCC one.
221+
//
222+
// A board does not choose whether to have builtins — every freestanding link
223+
// needs them, and on rv64 the trigger is picolibc's printf doing 128-bit
224+
// shifts, which the ISA has no instruction for. What varies is only which
225+
// implementation the resolved toolchain ships, and that is not a board fact.
226+
// Empty on a hosted target, where the driver links them without being asked.
227+
inline const char* target_builtins_lib() { return env_or("MCPP_TARGET_BUILTINS_LIB"); }
228+
229+
// The C library's sub-directory for this target's ISA profile, e.g.
230+
// `rv64gc/lp64d`. It is the multilib convention of whichever C library the
231+
// target resolved, with no board input at all — a board that wanted a
232+
// different layout would be using a different C library.
233+
//
234+
// Empty when the target has no C library of its own (the zero-libc tier, or a
235+
// hosted target).
236+
inline const char* target_libc_profile() { return env_or("MCPP_TARGET_LIBC_PROFILE"); }
237+
238+
// The C library's package name, e.g. `picolibc-riscv`; empty on the zero-libc
239+
// tier and on hosted targets.
240+
//
241+
// This one does NOT remove a coupling — it makes one visible. A board package
242+
// that genuinely must differ between picolibc and newlib (the crt0 object is
243+
// named differently, and that IS a board choice) can branch on this instead of
244+
// assuming. An explicit branch can be read and can be extended; an assumption
245+
// baked into a string literal can be neither.
246+
inline const char* target_libc() { return env_or("MCPP_TARGET_LIBC"); }
247+
206248
inline const char* manifest_dir() { return env_or("MCPP_MANIFEST_DIR"); }
207249
inline bool has_feature(const char* name) {
208250
char buf[256] = "MCPP_FEATURE_";

src/build/prepare.cppm

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,45 @@ sysroot_override(const mcpp::manifest::Manifest& m,
748748
return e ? e->sysroot : std::nullopt;
749749
}
750750

751+
// The target-facing answers a `build.mcpp` may ask the engine for.
752+
//
753+
// ONE function because there are TWO call sites — the root project and each
754+
// dependency — and four values derived independently in two places is the
755+
// shape this codebase keeps paying for. A board package that got the right
756+
// answer as a root project and a stale one as a dependency would fail only in
757+
// the consuming build, which is the harder direction to debug.
758+
void fill_target_build_env(mcpp::build::BuildProgramEnv& e,
759+
const mcpp::toolchain::Toolchain* tc)
760+
{
761+
e.toolchainDir = (tc && !tc->binaryPath.empty())
762+
? tc->binaryPath.parent_path().parent_path().string() : std::string{};
763+
e.targetSysroot = tc ? tc->targetSysrootRoot.string() : std::string{};
764+
e.targetLibc = tc ? tc->targetSysrootPkg : std::string{};
765+
if (!tc) return;
766+
767+
// The C LIBRARY's sub-directory for this ISA profile, from the freestanding
768+
// table — the same single read point the compile flags use.
769+
//
770+
// ⚠️ Gated on there being a C library at all, and the gate is the point: the
771+
// value is a multilib convention, so on the zero-libc tier there is nothing
772+
// for it to be a convention OF. Emitting `rv64gc/lp64d` there would hand a
773+
// kernel a path into a directory that does not exist, and the name of the
774+
// accessor would be a lie. All three libc-facing answers are empty together.
775+
if (!e.targetSysroot.empty())
776+
if (auto spec = mcpp::freestanding::resolve(tc->targetTriple))
777+
e.targetLibcProfile = std::string(spec->libdir);
778+
779+
// Which builtins library the RESOLVED toolchain ships. Freestanding only:
780+
// on a hosted target the driver links them without being asked, and
781+
// handing a package a name it must not use would invite it to.
782+
if (auto t = mcpp::toolchain::triple::parse(tc->targetTriple);
783+
t && t->is_freestanding()) {
784+
e.targetBuiltinsLib = mcpp::toolchain::is_clang(*tc)
785+
? "clang_rt.builtins-" + t->arch
786+
: std::string("gcc");
787+
}
788+
}
789+
751790
std::string with_index_cause(std::string msg) {
752791
if (auto hint = mcpp::pm::unusable_index_hint(); !hint.empty())
753792
msg += "\n" + hint;
@@ -1841,6 +1880,7 @@ prepare_build(bool print_fingerprint,
18411880
*dir / "lib" / std::string(spec->libdir);
18421881
std::error_code ec2;
18431882
tc->targetSysrootRoot = *dir;
1883+
tc->targetSysrootPkg = ref.name;
18441884
if (std::filesystem::is_directory(inc, ec2))
18451885
tc->targetSysrootInclude = inc;
18461886
if (std::filesystem::is_directory(lib, ec2))
@@ -5043,13 +5083,10 @@ prepare_build(bool print_fingerprint,
50435083
};
50445084
mcpp::build::BuildProgramEnv bpEnv;
50455085
bpEnv.targetTriple = resolvedTargetCanonical;
5046-
// The payload ROOT, not the driver: `<root>/bin/clang++` → `<root>`.
5047-
// A build program wants `<root>/include/c++/v1`, and deriving that
5048-
// from the driver path in every program would be the same expression
5049-
// copied into every package.
5050-
bpEnv.toolchainDir = (tc && !tc->binaryPath.empty())
5051-
? tc->binaryPath.parent_path().parent_path().string() : std::string{};
5052-
bpEnv.targetSysroot = tc ? tc->targetSysrootRoot.string() : std::string{};
5086+
// The payload ROOT (not the driver), the target's C library, and the
5087+
// three answers that keep a board package from hardcoding a toolchain
5088+
// or a libc. All four in one call — see fill_target_build_env.
5089+
fill_target_build_env(bpEnv, tc ? &*tc : nullptr);
50535090
bpEnv.profile = effectiveProfile;
50545091
bpEnv.features = feature_closure(pkg.manifest, req, depDefaultFeatures);
50555092
bpEnv.artifactsDir = workRoot / "target" / ".build-mcpp" / "deps"
@@ -5210,13 +5247,10 @@ prepare_build(bool print_fingerprint,
52105247
if (!host) return std::unexpected(host.error());
52115248
mcpp::build::BuildProgramEnv bpEnv;
52125249
bpEnv.targetTriple = resolvedTargetCanonical;
5213-
// The payload ROOT, not the driver: `<root>/bin/clang++` → `<root>`.
5214-
// A build program wants `<root>/include/c++/v1`, and deriving that
5215-
// from the driver path in every program would be the same expression
5216-
// copied into every package.
5217-
bpEnv.toolchainDir = (tc && !tc->binaryPath.empty())
5218-
? tc->binaryPath.parent_path().parent_path().string() : std::string{};
5219-
bpEnv.targetSysroot = tc ? tc->targetSysrootRoot.string() : std::string{};
5250+
// The payload ROOT (not the driver), the target's C library, and the
5251+
// three answers that keep a board package from hardcoding a toolchain
5252+
// or a libc. All four in one call — see fill_target_build_env.
5253+
fill_target_build_env(bpEnv, tc ? &*tc : nullptr);
52205254
bpEnv.profile = effectiveProfile;
52215255
// Set explicitly rather than relying on build_dir()'s root-relative
52225256
// default: under BuildOverrides::work_dir the package root is shared

src/toolchain/model.cppm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ struct Toolchain {
9494
// down for them.
9595
std::filesystem::path targetSysrootRoot;
9696
std::filesystem::path targetSysrootInclude;
97+
// The C library's package NAME (`picolibc-riscv`), recorded beside its
98+
// paths so a build program can be told which C library it is without
99+
// reverse-engineering the store layout from the path above. Empty on the
100+
// zero-libc tier and on hosted targets.
101+
std::string targetSysrootPkg;
97102
std::filesystem::path targetSysrootLib;
98103
std::vector<std::filesystem::path> compilerRuntimeDirs; // LD_LIBRARY_PATH for private tools
99104
std::vector<std::filesystem::path> linkRuntimeDirs; // -L/-rpath dirs for produced binaries
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env bash
2+
# requires: llvm unix-shell
3+
# `[target.<triple>].sysroot` and the three target queries a board package asks.
4+
#
5+
# ⚠️ WHY THIS TEST IS TWO-SIDED THROUGHOUT
6+
#
7+
# Both features here are of the kind whose presence and absence look identical
8+
# on a machine that is already configured. A sysroot override that silently did
9+
# nothing would still build, because the target row's picolibc is what would be
10+
# used either way; a query that returned a stale value would still compile,
11+
# because there is only one toolchain and one C library installed. So every
12+
# assertion below pins BOTH states — with the override and without it, with a C
13+
# library and without one.
14+
#
15+
# The queries exist to remove a coupling that no manifest shows. `riscv-virt-rt`
16+
# declares no dependency on LLVM and none on picolibc, and still could not serve
17+
# a second toolchain or a second C library, because it had written
18+
# `clang_rt.builtins-riscv64` and `rv64gc/lp64d` into its build program.
19+
#
20+
# No emulator is needed: everything here is decided at configure and link time,
21+
# which is why `requires:` asks only for llvm.
22+
set -e
23+
24+
TMP=$(mktemp -d)
25+
trap "rm -rf $TMP" EXIT
26+
cd "$TMP"
27+
28+
"$MCPP" new probe > /dev/null
29+
cd probe
30+
rm -f tests/*.cpp 2>/dev/null || true
31+
32+
# A build program that reports what the engine told it. It returns non-zero so
33+
# that mcpp surfaces the output: a build program's stderr is otherwise only
34+
# shown when it fails, and a probe nobody can read proves nothing.
35+
cat > build.mcpp <<'EOF'
36+
import mcpp;
37+
import std;
38+
int main() {
39+
std::cerr << "builtins=[" << (mcpp::target_builtins_lib() ?: "") << "]\n"
40+
<< "profile=[" << (mcpp::target_libc_profile() ?: "") << "]\n"
41+
<< "libc=[" << (mcpp::target_libc() ?: "") << "]\n";
42+
return 1;
43+
}
44+
EOF
45+
cat > src/main.cpp <<'EOF'
46+
extern "C" void _start() { for (;;) {} }
47+
EOF
48+
49+
manifest() {
50+
cat > mcpp.toml <<EOF
51+
[package]
52+
name = "probe"
53+
version = "0.1.0"
54+
55+
[build]
56+
target = "riscv64-none-elf"
57+
$1
58+
EOF
59+
}
60+
61+
# ── 1. The target row's C library, and the queries derived from it ───────────
62+
manifest ""
63+
"$MCPP" build > q64.log 2>&1 || true
64+
grep -q 'builtins=\[clang_rt.builtins-riscv64\]' q64.log || {
65+
cat q64.log; echo "rv64 builtins query wrong"; exit 1; }
66+
grep -q 'profile=\[rv64gc/lp64d\]' q64.log || {
67+
cat q64.log; echo "rv64 libc profile query wrong"; exit 1; }
68+
grep -q 'libc=\[picolibc-riscv\]' q64.log || {
69+
cat q64.log; echo "libc name query wrong"; exit 1; }
70+
71+
# ── 2. The SAME manifest at the other width ─────────────────────────────────
72+
# This is what makes the queries worth having: one board description, two ISA
73+
# profiles, and the varying parts come from the engine rather than from a
74+
# branch in the package.
75+
"$MCPP" build --target riscv32-none-elf > q32.log 2>&1 || true
76+
grep -q 'builtins=\[clang_rt.builtins-riscv32\]' q32.log || {
77+
cat q32.log; echo "rv32 builtins query did not follow the target"; exit 1; }
78+
grep -q 'profile=\[rv32imac/ilp32\]' q32.log || {
79+
cat q32.log; echo "rv32 libc profile query did not follow the target"; exit 1; }
80+
81+
# ── 3. The C library is really reachable without the override ───────────────
82+
# Establishes the control for step 4: `<stdio.h>` resolves here.
83+
cat > src/main.cpp <<'EOF'
84+
#include <stdio.h>
85+
extern "C" void _start() { (void)sizeof(FILE); for (;;) {} }
86+
EOF
87+
cat > build.mcpp <<'EOF'
88+
import mcpp;
89+
int main() { return 0; }
90+
EOF
91+
manifest ""
92+
"$MCPP" build > with_libc.log 2>&1 || {
93+
cat with_libc.log; echo "the target row's C library should have been usable"; exit 1; }
94+
95+
# ── 4. `sysroot = ""` really removes it ─────────────────────────────────────
96+
# ⚠️ THE LOAD-BEARING ASSERTION. Without it, an override that parsed and did
97+
# nothing would pass every other check in this file.
98+
manifest '
99+
[target.riscv64-none-elf]
100+
sysroot = ""'
101+
if "$MCPP" build > no_libc.log 2>&1; then
102+
cat no_libc.log
103+
echo "sysroot = \"\" did not remove the C library — <stdio.h> still resolved"
104+
exit 1
105+
fi
106+
grep -q "stdio.h" no_libc.log || {
107+
cat no_libc.log; echo "build failed, but not because the C library was gone"; exit 1; }
108+
109+
# ── 5. The zero-libc tier still produces an image ───────────────────────────
110+
# Removing the C library must leave a usable target, not a broken one.
111+
cat > src/main.cpp <<'EOF'
112+
static volatile unsigned char* const UART =
113+
reinterpret_cast<unsigned char*>(0x10000000);
114+
extern "C" void kmain() {
115+
for (const char* p = "zero-libc\n"; *p; ++p)
116+
*UART = static_cast<unsigned char>(*p);
117+
*reinterpret_cast<volatile unsigned int*>(0x100000) = 0x5555;
118+
for (;;) {}
119+
}
120+
asm(".section .text.entry\n.globl _start\n_start:\n"
121+
" la sp, __stack_top\n call kmain\n1: j 1b\n");
122+
EOF
123+
cat > link.ld <<'EOF'
124+
ENTRY(_start)
125+
SECTIONS {
126+
. = 0x80000000;
127+
.text : { *(.text.entry) *(.text*) }
128+
.rodata : { *(.rodata*) }
129+
.data : { *(.data*) }
130+
.bss : { *(.bss*) *(COMMON) }
131+
. = ALIGN(16); . = . + 0x1000; __stack_top = .;
132+
}
133+
EOF
134+
manifest "ldflags = [\"-T\", \"$PWD/link.ld\"]
135+
136+
[target.riscv64-none-elf]
137+
sysroot = \"\""
138+
"$MCPP" build > zero.log 2>&1 || {
139+
cat zero.log; echo "a self-contained zero-libc image should still build"; exit 1; }
140+
grep -q 'Size probe' zero.log || {
141+
cat zero.log; echo "no size summary — the freestanding link path was not taken"; exit 1; }
142+
143+
# ── 6. On the zero-libc tier the libc-facing queries are empty ──────────────
144+
# All three answers move together: a kernel must not be handed a path into a
145+
# C library that is not there, while `builtins` is a COMPILER fact and stays.
146+
cat > build.mcpp <<'EOF'
147+
import mcpp;
148+
import std;
149+
int main() {
150+
std::cerr << "builtins=[" << (mcpp::target_builtins_lib() ?: "") << "]\n"
151+
<< "profile=[" << (mcpp::target_libc_profile() ?: "") << "]\n"
152+
<< "libc=[" << (mcpp::target_libc() ?: "") << "]\n";
153+
return 1;
154+
}
155+
EOF
156+
"$MCPP" build > zeroq.log 2>&1 || true
157+
grep -q 'profile=\[\]' zeroq.log || {
158+
cat zeroq.log; echo "libc profile should be empty with no C library"; exit 1; }
159+
grep -q 'libc=\[\]' zeroq.log || {
160+
cat zeroq.log; echo "libc name should be empty with no C library"; exit 1; }
161+
grep -q 'builtins=\[clang_rt.builtins-riscv64\]' zeroq.log || {
162+
cat zeroq.log; echo "builtins is a compiler fact and must survive"; exit 1; }
163+
164+
# ── 7. A bare name is rejected at parse time ────────────────────────────────
165+
# Accepting it would install nothing and fail much later naming a missing libc.
166+
manifest '
167+
[target.riscv64-none-elf]
168+
sysroot = "newlib"'
169+
if "$MCPP" build > badref.log 2>&1; then
170+
cat badref.log; echo "a bare sysroot name should have been rejected"; exit 1; fi
171+
grep -q 'xpkg reference' badref.log || {
172+
cat badref.log; echo "rejection did not explain the expected form"; exit 1; }
173+
174+
echo "PASS: [target.X].sysroot override, zero-libc tier, and the three target queries"

0 commit comments

Comments
 (0)