Skip to content

Commit 45a36db

Browse files
committed
feat(target): Cortex-M gets a C library, and its multilib key is the TRIPLE
`xim:picolibc-arm@1.8.12` carries seven multilibs — picolibc plus the compiler-rt builtins that link it, built together by one LLVM. A project opts in with one line: [target.thumbv7m-none-eabi] sysroot = "xim:picolibc-arm@1.8.12" and `libdir` is what makes that line find anything. ## ⚠️⚠️ The column is the TRIPLE here, not `<march>/<mabi>` `libdir` names the sub-directory a multilib C library uses. On riscv, `<march>/<mabi>` separates every profile because `mabi` there IS the float ABI — `lp64d` and `lp64` are different values. On ARM `mabi` names the PROCEDURE CALL STANDARD and is `aapcs` for both variants, while the float ABI lives in the triple's `eabi`/`eabihf` suffix. So `armv7e-m/aapcs` would name ONE directory for two incompatible libraries. Measured while building the payload: under the sibling convention the seven M-profile profiles collapsed into FIVE directories, and `armv7e-m/aapcs/libc.a` came out carrying `Tag_ABI_HardFP_use` and `FP_arch: VFPv4-D16` — the hard-float build, sitting exactly where a soft-float program would find it. Nothing failed at build time. The two ARMv7-A rows are filled for the same reason even though no package carries an A-profile multilib yet: the column is a CONVENTION about where a sysroot puts a profile, not a claim that one exists, and leaving it empty would leave the next payload free to choose the arrangement that just failed. ⭐ Filling it does NOT give these rows a C library by default. The column is read only once a sysroot has been resolved, and the target table still binds none: the zero-libc tier stays the default and the manifest line is the opt-in. ## The criteria are the ABI and the size, not the link * A unit test quantifies over every 32-bit ARM row and asserts that the soft and hard variants of ONE architecture name DIFFERENT directories — with a denominator, so the check cannot quietly range over nothing. * `tests/e2e/338` builds the SOFT-float row and asserts the image declares no hard-float ABI tag, then runs it and reads `$?`. * ⚠️ And it asserts the artefact is not EMPTY. Measured while writing it: with the board's crt0 selection missing, the link SUCCEEDED and mcpp reported `Size fw text 0 data 0` — a well-formed ELF containing nothing. Every check that looked only for "Finished" would have passed. 338 skips until the payload is published, and CI demands that it either passed or said why — which is what distinguishes a skip from a silent zero exit. 97/97 unit; e2e 130-131, 332, 336, 338 green.
1 parent b96b61c commit 45a36db

5 files changed

Lines changed: 281 additions & 13 deletions

File tree

.github/workflows/ci-linux-e2e.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ jobs:
189189
tests/e2e/132_freestanding_test_and_artifacts.sh \
190190
tests/e2e/133_freestanding_std_subset.sh \
191191
tests/e2e/332_cortex_m_builds_and_boots.sh \
192-
tests/e2e/336_armv7a_builds_and_boots.sh; do
192+
tests/e2e/336_armv7a_builds_and_boots.sh \
193+
tests/e2e/338_cortex_m_picolibc_sysroot.sh; do
193194
echo "=== $t ==="
194195
bash "$t" 2>&1 | tee "$(basename "$t").log"
195196
rc=${PIPESTATUS[0]}
@@ -231,6 +232,15 @@ jobs:
231232
a32=$(grep -c 'booted on virt' 336_armv7a_builds_and_boots.sh.log || true)
232233
[ "$a32" = "2" ] || {
233234
echo "336 booted $a32 rows, expected 2"; exit 1; }
235+
# ⚠️ 338 SKIPS UNTIL `xim:picolibc-arm` IS PUBLISHED, and a skip here
236+
# is legitimate rather than a defect — the payload is a separate
237+
# release. So its PASS line is NOT demanded; what IS demanded is that
238+
# the script either passed or said why, which is what distinguishes a
239+
# skip from a silent zero-exit.
240+
grep -qE 'PASS: a Cortex-M project opts into picolibc|SKIP: picolibc-arm is not installed' \
241+
338_cortex_m_picolibc_sysroot.sh.log || {
242+
cat 338_cortex_m_picolibc_sysroot.sh.log
243+
echo "338 neither passed nor reported why it did not run"; exit 1; }
234244
235245
# ──────────────────────────────────────────────────────────────────
236246
# Hermetic (no host toolchain): the ONLY environment class that

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@
55
66
## [2026.9.4.2] — 2026-09-04
77

8+
### ⭐⭐ Cortex-M 有 C 库了:`libdir` 填上,而它的键是**三元组**
9+
10+
`xim:picolibc-arm@1.8.12` 带七个多库(picolibc + compiler-rt builtins,同一个
11+
LLVM 一起构建)。工程一行选入:
12+
13+
```toml
14+
[target.thumbv7m-none-eabi]
15+
sysroot = "xim:picolibc-arm@1.8.12"
16+
```
17+
18+
⚠️⚠️ **`libdir` 这一列在 ARM 上是三元组,不是 `<march>/<mabi>`,而差别是一次
19+
无人报告的 ABI 替换。** riscv 的 `mabi` **就是**浮点 ABI(`lp64d``lp64`
20+
两个值);ARM 的 `mabi` 是过程调用标准,两个变体都是 `aapcs`,浮点 ABI 在三元组的
21+
`eabi`/`eabihf` 后缀里。于是 `armv7e-m/aapcs` 会给两份互不兼容的库命名同一个目录。
22+
23+
实测(构建 `xim:picolibc-arm` 时):按 `<march>/<mabi>`,七个档位塌成五个目录,
24+
`armv7e-m/aapcs/libc.a` 带着 `Tag_ABI_HardFP_use` —— 硬浮点的构建,坐在软浮点
25+
程序会去找它的位置上。**构建期什么都没报。**
26+
27+
⭐ 填这一列****给这些行默认配 C 库:只有解析出 sysroot 之后才会读它,目标表仍然
28+
不绑定任何一个。零 libc 仍是默认,这只是让选入这件事能成立。
29+
30+
判据是浮点 ABI 而不是「链接通过」:`tests/e2e/338` 构建**软浮点**那一行,断言产物
31+
里没有硬浮点 ABI 标记,然后启动它并读退出码。⚠️ 并且断言产物**不为空** —— 实测:
32+
缺了 crt0 的链接会成功并报 `Size fw text 0 data 0`,一个格式良好、什么都没有的
33+
ELF,任何只看 `Finished` 的检查都会放行。
34+
835
runner 有了名字,工具有了档位,`--locked` 成为断言,`mcpp emit sbom`
936

1037
### ⭐⭐ 一条命令,加具名的例外

src/freestanding/target.cppm

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -251,20 +251,38 @@ inline constexpr Spec kTable[] = {
251251
// the x86_64 row's problem does not recur here. Verified end to end: a
252252
// freestanding thumb object links under `ld.lld` and boots under QEMU.
253253
//
254-
// `libdir` is EMPTY on every row because `sysroot` is (see the target
255-
// table): the column is read only when a sysroot has been resolved, and a
256-
// value here could never be checked.
254+
// ⚠️⚠️ `libdir` IS THE TRIPLE HERE, NOT `<march>/<mabi>` LIKE EVERY OTHER
255+
// ROW, AND THE DIFFERENCE IS AN ABI SUBSTITUTION THAT NOTHING REPORTS.
256+
//
257+
// The column names the sub-directory a multilib C library uses, and on
258+
// riscv `<march>/<mabi>` separates every profile because `mabi` there IS
259+
// the float ABI — `lp64d` and `lp64` are different values. On ARM `mabi`
260+
// names the PROCEDURE CALL STANDARD and is `aapcs` for both variants, while
261+
// the float ABI lives in this triple's `eabi`/`eabihf` suffix. So
262+
// `armv7e-m/aapcs` would name ONE directory for two incompatible libraries.
263+
//
264+
// Measured while building `xim:picolibc-arm`: under the sibling convention
265+
// the seven profiles collapsed into five directories and
266+
// `armv7e-m/aapcs/libc.a` came out carrying `Tag_ABI_HardFP_use` — the
267+
// hard-float build, sitting exactly where a soft-float program would find
268+
// it. Nothing failed at build time.
269+
//
270+
// ⚠️ Filling this does NOT give these rows a C library by default: the
271+
// column is read only once a sysroot has been resolved, and the target
272+
// table still binds none. The zero-libc tier stays the default and
273+
// `[target.<triple>] sysroot = "xim:picolibc-arm@1.8.12"` is the opt-in.
274+
// This makes the opt-in work; it does not take the opt-out away.
257275
//
258276
// ⚠️ `-mabi=aapcs`, matching the aarch64 row and for the same reason: on
259277
// ARM `-mabi` names a procedure call standard, not a data model.
260278
// triple march mabi mcmodel libdir extra
261-
{ "thumbv6m-none-eabi", "armv6-m", "aapcs", "", "", kThumbSoftExtra },
262-
{ "thumbv7m-none-eabi", "armv7-m", "aapcs", "", "", kThumbSoftExtra },
263-
{ "thumbv7em-none-eabi", "armv7e-m", "aapcs", "", "", kThumbSoftExtra },
264-
{ "thumbv7em-none-eabihf", "armv7e-m", "aapcs", "", "" },
265-
{ "thumbv8m.base-none-eabi", "armv8-m.base","aapcs","", "", kThumbSoftExtra },
266-
{ "thumbv8m.main-none-eabi", "armv8-m.main","aapcs","", "", kThumbSoftExtra },
267-
{ "thumbv8m.main-none-eabihf","armv8-m.main","aapcs","", "" },
279+
{ "thumbv6m-none-eabi", "armv6-m", "aapcs", "", "thumbv6m-none-eabi", kThumbSoftExtra },
280+
{ "thumbv7m-none-eabi", "armv7-m", "aapcs", "", "thumbv7m-none-eabi", kThumbSoftExtra },
281+
{ "thumbv7em-none-eabi", "armv7e-m", "aapcs", "", "thumbv7em-none-eabi", kThumbSoftExtra },
282+
{ "thumbv7em-none-eabihf", "armv7e-m", "aapcs", "", "thumbv7em-none-eabihf" },
283+
{ "thumbv8m.base-none-eabi", "armv8-m.base","aapcs","", "thumbv8m.base-none-eabi", kThumbSoftExtra },
284+
{ "thumbv8m.main-none-eabi", "armv8-m.main","aapcs","", "thumbv8m.main-none-eabi", kThumbSoftExtra },
285+
{ "thumbv8m.main-none-eabihf","armv8-m.main","aapcs","", "thumbv8m.main-none-eabihf" },
268286
// ── ARMv7-A (Cortex-A, 32-bit) ──────────────────────────────────────────
269287
//
270288
// ⭐ THE FIRST 32-BIT MACHINE WITH A MEMORY MANAGEMENT UNIT, AND THAT IS
@@ -295,8 +313,18 @@ inline constexpr Spec kTable[] = {
295313
// the WRONG exit status — measured: a program exiting 0 reported 1. That is
296314
// a board fact rather than a target fact, recorded here because it is where
297315
// the next person to write such a board will look.
298-
{ "armv7a-none-eabi", "armv7-a", "aapcs", "", "", kArmSoftExtra },
299-
{ "armv7a-none-eabihf", "armv7-a", "aapcs", "", "" },
316+
//
317+
// ⚠️ `libdir` IS THE TRIPLE HERE TOO, AND FOR THE SAME REASON AS THE
318+
// M ROWS — even though no package carries an A-profile multilib yet.
319+
//
320+
// The column is a CONVENTION about where a sysroot puts a profile, not a
321+
// claim that one exists: it is read only after a sysroot has been resolved,
322+
// and an absent directory is skipped. Leaving it empty would leave the next
323+
// A-profile payload free to choose `<march>/<mabi>` — which maps these two
324+
// incompatible libraries onto one directory, exactly as it did for
325+
// `armv7e-m` when `xim:picolibc-arm` was first built.
326+
{ "armv7a-none-eabi", "armv7-a", "aapcs", "", "armv7a-none-eabi", kArmSoftExtra },
327+
{ "armv7a-none-eabihf", "armv7-a", "aapcs", "", "armv7a-none-eabihf" },
300328
};
301329

302330
// The single read point. Returns nullopt for anything that is not a known
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env bash
2+
# requires: llvm unix-shell qemu-arm
3+
# A Cortex-M project opts into a C library, and gets the RIGHT multilib.
4+
#
5+
# ⭐⭐ THE ZERO-LIBC TIER IS THE DEFAULT AND THIS IS THE OPT-IN. Every
6+
# `thumb*-none-eabi*` row carries an empty C-library column, so a project
7+
# targeting one begins with no libc unless it says otherwise. One line says
8+
# otherwise, and `libdir` is what makes that line find anything.
9+
#
10+
# ⚠️⚠️ AND THE ASSERTION IS THE FLOAT ABI, NOT THAT IT LINKS.
11+
#
12+
# `libdir` names the sub-directory a multilib C library uses. On riscv
13+
# `<march>/<mabi>` separates every profile because `mabi` there IS the float
14+
# ABI; on ARM `mabi` names the procedure call standard and is `aapcs` for both
15+
# variants, while the float ABI lives in the triple's `eabi`/`eabihf` suffix. So
16+
# `armv7e-m/aapcs` would name ONE directory for two incompatible libraries —
17+
# measured while building `xim:picolibc-arm`, where the soft-float row silently
18+
# received a library carrying `Tag_ABI_HardFP_use`.
19+
#
20+
# A test that only linked would pass either way. This one builds the SOFT-float
21+
# row and asserts the produced image declares no hard-float ABI.
22+
set -e
23+
24+
MCPP="${MCPP:-mcpp}"
25+
work="$(mktemp -d)"
26+
trap 'rm -rf "$work"' EXIT
27+
28+
qemu_arm() {
29+
local d c
30+
for d in "${MCPP_HOME:-$HOME/.mcpp}/registry" "$HOME/.xlings"; do
31+
c=$(ls "$d"/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm 2>/dev/null | sort -V | tail -1)
32+
[ -n "$c" ] && [ -x "$c" ] && { echo "$c"; return 0; }
33+
done
34+
command -v qemu-system-arm 2>/dev/null && return 0
35+
return 1
36+
}
37+
QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; }
38+
39+
llvm_tool() {
40+
local c
41+
c=$(ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-llvm/*/bin/"$1" 2>/dev/null | sort -V | tail -1)
42+
[ -n "$c" ] && { echo "$c"; return 0; }
43+
command -v "$1" 2>/dev/null
44+
}
45+
READELF="$(llvm_tool llvm-readelf)" || { echo "SKIP: llvm-readelf not found"; exit 0; }
46+
47+
# ⚠️ The payload has to be present. Installing it here rather than skipping
48+
# would make the test about the install; skipping when it is absent keeps the
49+
# criterion about the ENGINE, which is what this suite tests.
50+
# ⚠️ `xim-x-picolibc-arm` AND NOT `*-x-picolibc-arm`. The engine resolves the
51+
# name `xim:picolibc-arm`, so a copy installed under any other namespace — a
52+
# LOCAL index entry, say — satisfies a glob and not the engine. Measured: with
53+
# the loose pattern this test proceeded against a `local-x-` payload and failed
54+
# on `'stdio.h' file not found`, having asserted nothing about the engine.
55+
sysroot_dir() {
56+
local d c
57+
for d in "${MCPP_HOME:-$HOME/.mcpp}/registry" "$HOME/.xlings"; do
58+
for c in "$d"/data/xpkgs/xim-x-picolibc-arm/*/; do
59+
[ -d "$c/lib/thumbv7m-none-eabi" ] && { echo "$c"; return 0; }
60+
done
61+
done
62+
return 1
63+
}
64+
SYSROOT="$(sysroot_dir)" || { echo "SKIP: picolibc-arm is not installed"; exit 0; }
65+
66+
mkdir -p "$work/fw/src"
67+
cd "$work/fw"
68+
cat > mcpp.toml <<'TOML'
69+
[package]
70+
name = "fw"
71+
version = "0.1.0"
72+
73+
[build]
74+
target = "thumbv7m-none-eabi"
75+
sources = ["src/main.c"]
76+
77+
[targets.fw]
78+
kind = "bin"
79+
main = "src/main.c"
80+
81+
# The opt-in, and the whole of it.
82+
[target.thumbv7m-none-eabi]
83+
sysroot = "xim:picolibc-arm@1.8.12"
84+
TOML
85+
86+
# ⭐ LOCATION IS A TARGET FACT; SELECTION IS A BOARD FACT. The engine resolved
87+
# WHERE the C library is and which multilib profile applies. WHICH startup
88+
# object, WHICH linker script and WHICH libraries are decisions a board makes,
89+
# and this fixture stands in for a board package.
90+
#
91+
# ⚠️ `target_libc_profile()` IS THE `libdir` COLUMN, AND WITHOUT IT NONE OF THIS
92+
# CAN BE WRITTEN. It is empty when the column is, and the board is then reduced
93+
# to guessing a directory name — which is the thing that goes wrong silently.
94+
cat > build.mcpp <<'BUILD'
95+
import mcpp;
96+
import std;
97+
int main() {
98+
const std::string root = mcpp::sysroot_dir() ? mcpp::sysroot_dir() : "";
99+
const std::string prof = mcpp::target_libc_profile()
100+
? mcpp::target_libc_profile() : "";
101+
if (prof.empty()) {
102+
std::cerr << "the target row carries no libc profile; a board cannot "
103+
"name a file inside the sysroot\n";
104+
return 1;
105+
}
106+
const std::string lib = root + "/lib/" + prof;
107+
mcpp::link_script((lib + "/picolibc.ld").c_str());
108+
mcpp::link_search(lib.c_str());
109+
mcpp::link_lib("crt0-semihost"); // pulled from the archive by ENTRY
110+
mcpp::link_lib("c");
111+
mcpp::link_lib("semihost");
112+
mcpp::link_lib(mcpp::target_builtins_lib() ? mcpp::target_builtins_lib() : "");
113+
return 0;
114+
}
115+
BUILD
116+
117+
cat > src/main.c <<'C'
118+
#include <stdio.h>
119+
/* `volatile`, so the multiply survives constant folding and picolibc's float
120+
formatting is really exercised — the path that needs the builtins. */
121+
volatile float fa = 3.0f, fb = 4.0f;
122+
int main(void) { printf("picolibc: %.2f\n", (double)(fa * fb + 1.0f)); return 0; }
123+
C
124+
125+
# Twice, the first allowed to fail: the target world is installed during a build.
126+
"$MCPP" build > /dev/null 2>&1 || true
127+
"$MCPP" build 2>&1 | tail -20 || { echo "FAIL: the opt-in build did not succeed"; exit 1; }
128+
129+
elf=$(find target -type f -name fw | head -1)
130+
[ -n "$elf" ] || { echo "FAIL: no artefact"; exit 1; }
131+
132+
# ⚠️⚠️ AN EMPTY IMAGE IS NOT A PASS, AND IT IS WHAT A MISSING BOARD SELECTION
133+
# PRODUCES. Measured while writing this: without the crt0 the link SUCCEEDED and
134+
# mcpp reported `Size fw text 0 data 0` — a well-formed ELF containing
135+
# nothing. Every check that only looked for "Finished" would have passed.
136+
size=$(wc -c < "$elf")
137+
[ "$size" -gt 4096 ] || { echo "FAIL: the artefact is $size bytes — an empty link"; exit 1; }
138+
139+
# ⭐ THE ASSERTION THAT CATCHES THE WRONG MULTILIB. A hard-float libc linked
140+
# into a soft-float image leaves `Tag_ABI_HardFP_use` in the attributes.
141+
hard=$("$READELF" -A "$elf" 2>/dev/null | grep -c 'ABI_HardFP_use' || true)
142+
[ "$hard" = "0" ] || {
143+
echo "FAIL: a soft-float image carries $hard hard-float ABI tags — the wrong multilib was linked"
144+
exit 1; }
145+
echo " ok the soft-float row linked a soft-float C library"
146+
147+
# And it runs: the C library is not merely present, its printf works.
148+
set +e
149+
timeout 30 "$QEMU" -machine mps2-an385 -cpu cortex-m3 -nographic -semihosting \
150+
-no-reboot -kernel "$elf" > run.log 2>&1
151+
rc=$?
152+
set -e
153+
cat run.log
154+
grep -q 'picolibc: 13.00' run.log \
155+
|| { echo "FAIL: printf did not produce the expected output"; exit 1; }
156+
[ "$rc" = "0" ] || { echo "FAIL: the image ran but exited $rc"; exit 1; }
157+
158+
echo "PASS: a Cortex-M project opts into picolibc and gets the right multilib"

tests/unit/test_freestanding.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,3 +559,48 @@ TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) {
559559
}
560560
EXPECT_GT(rows, 0);
561561
}
562+
563+
// ⚠️⚠️ THE MULTILIB KEY IS NOT `<march>/<mabi>` ON ARM, AND ASSUMING IT WAS
564+
// PRODUCES AN ABI SUBSTITUTION THAT NOTHING REPORTS.
565+
//
566+
// The column names the sub-directory a multilib C library uses. On riscv,
567+
// `<march>/<mabi>` separates every profile because `mabi` there IS the float
568+
// ABI — `lp64d` and `lp64` are different values. On ARM `mabi` names the
569+
// procedure call standard and is `aapcs` for both variants, while the float ABI
570+
// lives in the triple's `eabi`/`eabihf` suffix.
571+
//
572+
// Measured while building `xim:picolibc-arm`: under the sibling convention the
573+
// seven M-profile profiles collapsed into five directories, and
574+
// `armv7e-m/aapcs/libc.a` came out carrying `Tag_ABI_HardFP_use` — the
575+
// hard-float build, where a soft-float program would find it. Nothing failed.
576+
//
577+
// So the assertion is not "libdir is non-empty": it is that the soft and hard
578+
// variants of ONE architecture name DIFFERENT directories, which is the
579+
// property the layout exists to preserve.
580+
TEST(FreestandingTarget, Arm32SoftAndHardVariantsNameDifferentLibdirs) {
581+
std::map<std::string, std::vector<std::string>> byMarch;
582+
int rows = 0;
583+
for (const auto& spec : mcpp::freestanding::known()) {
584+
if (!is_arm32(spec.triple)) continue;
585+
++rows;
586+
EXPECT_FALSE(spec.libdir.empty())
587+
<< spec.triple << " has no libdir, so an opt-in sysroot cannot be found";
588+
byMarch[std::string(spec.march)].push_back(std::string(spec.libdir));
589+
}
590+
EXPECT_GT(rows, 0);
591+
592+
int pairs = 0;
593+
for (auto const& [march, dirs] : byMarch) {
594+
if (dirs.size() < 2) continue;
595+
++pairs;
596+
std::set<std::string> distinct(dirs.begin(), dirs.end());
597+
EXPECT_EQ(distinct.size(), dirs.size())
598+
<< march << " maps " << dirs.size() << " rows onto "
599+
<< distinct.size() << " directories: a soft-float program would "
600+
"link a hard-float library, silently";
601+
}
602+
// ⚠️ A denominator. If no architecture had two rows the loop above would
603+
// assert nothing, and the property would be untested rather than held.
604+
EXPECT_GT(pairs, 0) << "no architecture carries both float ABIs; the check "
605+
"above quantified over nothing";
606+
}

0 commit comments

Comments
 (0)