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
12 changes: 11 additions & 1 deletion .github/workflows/ci-linux-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ jobs:
tests/e2e/132_freestanding_test_and_artifacts.sh \
tests/e2e/133_freestanding_std_subset.sh \
tests/e2e/332_cortex_m_builds_and_boots.sh \
tests/e2e/336_armv7a_builds_and_boots.sh; do
tests/e2e/336_armv7a_builds_and_boots.sh \
tests/e2e/338_cortex_m_picolibc_sysroot.sh; do
echo "=== $t ==="
bash "$t" 2>&1 | tee "$(basename "$t").log"
rc=${PIPESTATUS[0]}
Expand Down Expand Up @@ -231,6 +232,15 @@ jobs:
a32=$(grep -c 'booted on virt' 336_armv7a_builds_and_boots.sh.log || true)
[ "$a32" = "2" ] || {
echo "336 booted $a32 rows, expected 2"; exit 1; }
# ⚠️ 338 SKIPS UNTIL `xim:picolibc-arm` IS PUBLISHED, and a skip here
# is legitimate rather than a defect — the payload is a separate
# release. So its PASS line is NOT demanded; what IS demanded is that
# the script either passed or said why, which is what distinguishes a
# skip from a silent zero-exit.
grep -qE 'PASS: a Cortex-M project opts into picolibc|SKIP: picolibc-arm is not installed' \
338_cortex_m_picolibc_sysroot.sh.log || {
cat 338_cortex_m_picolibc_sysroot.sh.log
echo "338 neither passed nor reported why it did not run"; exit 1; }

# ──────────────────────────────────────────────────────────────────
# Hermetic (no host toolchain): the ONLY environment class that
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@

## [2026.9.4.2] — 2026-09-04

### ⭐⭐ Cortex-M 有 C 库了:`libdir` 填上,而它的键是**三元组**

`xim:picolibc-arm@1.8.12` 带七个多库(picolibc + compiler-rt builtins,同一个
LLVM 一起构建)。工程一行选入:

```toml
[target.thumbv7m-none-eabi]
sysroot = "xim:picolibc-arm@1.8.12"
```

⚠️⚠️ **`libdir` 这一列在 ARM 上是三元组,不是 `<march>/<mabi>`,而差别是一次
无人报告的 ABI 替换。** riscv 的 `mabi` **就是**浮点 ABI(`lp64d` 与 `lp64` 是
两个值);ARM 的 `mabi` 是过程调用标准,两个变体都是 `aapcs`,浮点 ABI 在三元组的
`eabi`/`eabihf` 后缀里。于是 `armv7e-m/aapcs` 会给两份互不兼容的库命名同一个目录。

实测(构建 `xim:picolibc-arm` 时):按 `<march>/<mabi>`,七个档位塌成五个目录,
`armv7e-m/aapcs/libc.a` 带着 `Tag_ABI_HardFP_use` —— 硬浮点的构建,坐在软浮点
程序会去找它的位置上。**构建期什么都没报。**

⭐ 填这一列**不**给这些行默认配 C 库:只有解析出 sysroot 之后才会读它,目标表仍然
不绑定任何一个。零 libc 仍是默认,这只是让选入这件事能成立。

判据是浮点 ABI 而不是「链接通过」:`tests/e2e/338` 构建**软浮点**那一行,断言产物
里没有硬浮点 ABI 标记,然后启动它并读退出码。⚠️ 并且断言产物**不为空** —— 实测:
缺了 crt0 的链接会成功并报 `Size fw text 0 data 0`,一个格式良好、什么都没有的
ELF,任何只看 `Finished` 的检查都会放行。

runner 有了名字,工具有了档位,`--locked` 成为断言,`mcpp emit sbom`。

### ⭐⭐ 一条命令,加具名的例外
Expand Down
52 changes: 40 additions & 12 deletions src/freestanding/target.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -251,20 +251,38 @@ inline constexpr Spec kTable[] = {
// the x86_64 row's problem does not recur here. Verified end to end: a
// freestanding thumb object links under `ld.lld` and boots under QEMU.
//
// `libdir` is EMPTY on every row because `sysroot` is (see the target
// table): the column is read only when a sysroot has been resolved, and a
// value here could never be checked.
// ⚠️⚠️ `libdir` IS THE TRIPLE HERE, NOT `<march>/<mabi>` LIKE EVERY OTHER
// ROW, AND THE DIFFERENCE IS AN ABI SUBSTITUTION THAT NOTHING REPORTS.
//
// The column names the sub-directory a multilib C library uses, and 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 this triple's `eabi`/`eabihf` suffix. So
// `armv7e-m/aapcs` would name ONE directory for two incompatible libraries.
//
// Measured while building `xim:picolibc-arm`: under the sibling convention
// the seven profiles collapsed into five directories and
// `armv7e-m/aapcs/libc.a` came out carrying `Tag_ABI_HardFP_use` — the
// hard-float build, sitting exactly where a soft-float program would find
// it. Nothing failed at build time.
//
// ⚠️ Filling this 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
// `[target.<triple>] sysroot = "xim:picolibc-arm@1.8.12"` is the opt-in.
// This makes the opt-in work; it does not take the opt-out away.
//
// ⚠️ `-mabi=aapcs`, matching the aarch64 row and for the same reason: on
// ARM `-mabi` names a procedure call standard, not a data model.
// triple march mabi mcmodel libdir extra
{ "thumbv6m-none-eabi", "armv6-m", "aapcs", "", "", kThumbSoftExtra },
{ "thumbv7m-none-eabi", "armv7-m", "aapcs", "", "", kThumbSoftExtra },
{ "thumbv7em-none-eabi", "armv7e-m", "aapcs", "", "", kThumbSoftExtra },
{ "thumbv7em-none-eabihf", "armv7e-m", "aapcs", "", "" },
{ "thumbv8m.base-none-eabi", "armv8-m.base","aapcs","", "", kThumbSoftExtra },
{ "thumbv8m.main-none-eabi", "armv8-m.main","aapcs","", "", kThumbSoftExtra },
{ "thumbv8m.main-none-eabihf","armv8-m.main","aapcs","", "" },
{ "thumbv6m-none-eabi", "armv6-m", "aapcs", "", "thumbv6m-none-eabi", kThumbSoftExtra },
{ "thumbv7m-none-eabi", "armv7-m", "aapcs", "", "thumbv7m-none-eabi", kThumbSoftExtra },
{ "thumbv7em-none-eabi", "armv7e-m", "aapcs", "", "thumbv7em-none-eabi", kThumbSoftExtra },
{ "thumbv7em-none-eabihf", "armv7e-m", "aapcs", "", "thumbv7em-none-eabihf" },
{ "thumbv8m.base-none-eabi", "armv8-m.base","aapcs","", "thumbv8m.base-none-eabi", kThumbSoftExtra },
{ "thumbv8m.main-none-eabi", "armv8-m.main","aapcs","", "thumbv8m.main-none-eabi", kThumbSoftExtra },
{ "thumbv8m.main-none-eabihf","armv8-m.main","aapcs","", "thumbv8m.main-none-eabihf" },
// ── ARMv7-A (Cortex-A, 32-bit) ──────────────────────────────────────────
//
// ⭐ THE FIRST 32-BIT MACHINE WITH A MEMORY MANAGEMENT UNIT, AND THAT IS
Expand Down Expand Up @@ -295,8 +313,18 @@ inline constexpr Spec kTable[] = {
// the WRONG exit status — measured: a program exiting 0 reported 1. That is
// a board fact rather than a target fact, recorded here because it is where
// the next person to write such a board will look.
{ "armv7a-none-eabi", "armv7-a", "aapcs", "", "", kArmSoftExtra },
{ "armv7a-none-eabihf", "armv7-a", "aapcs", "", "" },
//
// ⚠️ `libdir` IS THE TRIPLE HERE TOO, AND FOR THE SAME REASON AS THE
// M ROWS — 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: it is read only after a sysroot has been resolved,
// and an absent directory is skipped. Leaving it empty would leave the next
// A-profile payload free to choose `<march>/<mabi>` — which maps these two
// incompatible libraries onto one directory, exactly as it did for
// `armv7e-m` when `xim:picolibc-arm` was first built.
{ "armv7a-none-eabi", "armv7-a", "aapcs", "", "armv7a-none-eabi", kArmSoftExtra },
{ "armv7a-none-eabihf", "armv7-a", "aapcs", "", "armv7a-none-eabihf" },
};

// The single read point. Returns nullopt for anything that is not a known
Expand Down
36 changes: 27 additions & 9 deletions tests/e2e/337_run_takes_features_and_profile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,37 @@ want "$again" "dev" "a plain run inherited the previous --release"
#
# Measured before the fix: three builds of one project printed
# `quiet`, `LOUD`, `LOUD`.
artifact() { find target -type f -name featrun -newermt '-1 day' | head -1; }
# ⚠️⚠️ THE ASSERTION IS ON WHAT THE CACHE ENTRY RECORDS, NOT ON WHICH FILE A
# `find` HAPPENS TO RETURN FIRST.
#
# The first version of this block ran `find target -name featrun | head -1`.
# Two output directories exist by then — one per feature set — and which one
# `find` walks first is filesystem order. It passed locally and failed on CI,
# which is the signature of an assertion that depends on something nobody
# chose.
#
# What the fix actually changed is the ENTRY: it now records the feature set its
# artefacts were built with, and both fast paths compare it. Entries are written
# most-recently-used first, so the first `features=` line in the cache belongs
# to the build that just ran — and reading it is exact.
mru_features() { awk -F= '/^features=/{print $2; exit}' target/.build_cache; }

rm -rf target
"$MCPP" build >/dev/null 2>&1
first="$(./"$(artifact)")"
[ -z "$(mru_features)" ] || {
echo "FAIL: a plain build recorded features '$(mru_features)'"; exit 1; }

"$MCPP" build --features loud >/dev/null 2>&1
[ "$(mru_features)" = "loud" ] || {
echo "FAIL: --features loud recorded '$(mru_features)', expected loud"; exit 1; }

# ⚠️ THE ONE THAT CAUGHT THE PRE-EXISTING DEFECT. Before the entry carried a
# feature set, this plain build matched the entry `--features loud` had written,
# reported success in 0.00s and left the loud artefact in place. Measured: three
# consecutive builds of one project printed `quiet`, `LOUD`, `LOUD`.
"$MCPP" build >/dev/null 2>&1
third="$(./"$(artifact)")"
case "$first" in *quiet*) ;; *) echo "FAIL: the first build was not plain"; exit 1 ;; esac
case "$third" in
*quiet*) ;;
*) echo "FAIL: a plain build after --features served the featured artefact"
echo " first=$first third=$third"; exit 1 ;;
esac
[ -z "$(mru_features)" ] || {
echo "FAIL: a plain build after --features matched an entry recorded as '$(mru_features)'"
exit 1; }

echo "PASS: mcpp run takes --features and --profile, and the fast path honours both"
158 changes: 158 additions & 0 deletions tests/e2e/338_cortex_m_picolibc_sysroot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# requires: llvm unix-shell qemu-arm
# A Cortex-M project opts into a C library, and gets the RIGHT multilib.
#
# ⭐⭐ THE ZERO-LIBC TIER IS THE DEFAULT AND THIS IS THE OPT-IN. Every
# `thumb*-none-eabi*` row carries an empty C-library column, so a project
# targeting one begins with no libc unless it says otherwise. One line says
# otherwise, and `libdir` is what makes that line find anything.
#
# ⚠️⚠️ AND THE ASSERTION IS THE FLOAT ABI, NOT THAT IT LINKS.
#
# `libdir` names the sub-directory a multilib C library uses. On riscv
# `<march>/<mabi>` separates every profile because `mabi` there IS the float
# ABI; 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 `xim:picolibc-arm`, where the soft-float row silently
# received a library carrying `Tag_ABI_HardFP_use`.
#
# A test that only linked would pass either way. This one builds the SOFT-float
# row and asserts the produced image declares no hard-float ABI.
set -e

MCPP="${MCPP:-mcpp}"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT

qemu_arm() {
local d c
for d in "${MCPP_HOME:-$HOME/.mcpp}/registry" "$HOME/.xlings"; do
c=$(ls "$d"/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm 2>/dev/null | sort -V | tail -1)
[ -n "$c" ] && [ -x "$c" ] && { echo "$c"; return 0; }
done
command -v qemu-system-arm 2>/dev/null && return 0
return 1
}
QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; }

llvm_tool() {
local c
c=$(ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-llvm/*/bin/"$1" 2>/dev/null | sort -V | tail -1)
[ -n "$c" ] && { echo "$c"; return 0; }
command -v "$1" 2>/dev/null
}
READELF="$(llvm_tool llvm-readelf)" || { echo "SKIP: llvm-readelf not found"; exit 0; }

# ⚠️ The payload has to be present. Installing it here rather than skipping
# would make the test about the install; skipping when it is absent keeps the
# criterion about the ENGINE, which is what this suite tests.
# ⚠️ `xim-x-picolibc-arm` AND NOT `*-x-picolibc-arm`. The engine resolves the
# name `xim:picolibc-arm`, so a copy installed under any other namespace — a
# LOCAL index entry, say — satisfies a glob and not the engine. Measured: with
# the loose pattern this test proceeded against a `local-x-` payload and failed
# on `'stdio.h' file not found`, having asserted nothing about the engine.
sysroot_dir() {
local d c
for d in "${MCPP_HOME:-$HOME/.mcpp}/registry" "$HOME/.xlings"; do
for c in "$d"/data/xpkgs/xim-x-picolibc-arm/*/; do
[ -d "$c/lib/thumbv7m-none-eabi" ] && { echo "$c"; return 0; }
done
done
return 1
}
SYSROOT="$(sysroot_dir)" || { echo "SKIP: picolibc-arm is not installed"; exit 0; }

mkdir -p "$work/fw/src"
cd "$work/fw"
cat > mcpp.toml <<'TOML'
[package]
name = "fw"
version = "0.1.0"

[build]
target = "thumbv7m-none-eabi"
sources = ["src/main.c"]

[targets.fw]
kind = "bin"
main = "src/main.c"

# The opt-in, and the whole of it.
[target.thumbv7m-none-eabi]
sysroot = "xim:picolibc-arm@1.8.12"
TOML

# ⭐ LOCATION IS A TARGET FACT; SELECTION IS A BOARD FACT. The engine resolved
# WHERE the C library is and which multilib profile applies. WHICH startup
# object, WHICH linker script and WHICH libraries are decisions a board makes,
# and this fixture stands in for a board package.
#
# ⚠️ `target_libc_profile()` IS THE `libdir` COLUMN, AND WITHOUT IT NONE OF THIS
# CAN BE WRITTEN. It is empty when the column is, and the board is then reduced
# to guessing a directory name — which is the thing that goes wrong silently.
cat > build.mcpp <<'BUILD'
import mcpp;
import std;
int main() {
const std::string root = mcpp::sysroot_dir() ? mcpp::sysroot_dir() : "";
const std::string prof = mcpp::target_libc_profile()
? mcpp::target_libc_profile() : "";
if (prof.empty()) {
std::cerr << "the target row carries no libc profile; a board cannot "
"name a file inside the sysroot\n";
return 1;
}
const std::string lib = root + "/lib/" + prof;
mcpp::link_script((lib + "/picolibc.ld").c_str());
mcpp::link_search(lib.c_str());
mcpp::link_lib("crt0-semihost"); // pulled from the archive by ENTRY
mcpp::link_lib("c");
mcpp::link_lib("semihost");
mcpp::link_lib(mcpp::target_builtins_lib() ? mcpp::target_builtins_lib() : "");
return 0;
}
BUILD

cat > src/main.c <<'C'
#include <stdio.h>
/* `volatile`, so the multiply survives constant folding and picolibc's float
formatting is really exercised — the path that needs the builtins. */
volatile float fa = 3.0f, fb = 4.0f;
int main(void) { printf("picolibc: %.2f\n", (double)(fa * fb + 1.0f)); return 0; }
C

# Twice, the first allowed to fail: the target world is installed during a build.
"$MCPP" build > /dev/null 2>&1 || true
"$MCPP" build 2>&1 | tail -20 || { echo "FAIL: the opt-in build did not succeed"; exit 1; }

elf=$(find target -type f -name fw | head -1)
[ -n "$elf" ] || { echo "FAIL: no artefact"; exit 1; }

# ⚠️⚠️ AN EMPTY IMAGE IS NOT A PASS, AND IT IS WHAT A MISSING BOARD SELECTION
# PRODUCES. Measured while writing this: without the crt0 the link SUCCEEDED and
# mcpp reported `Size fw text 0 data 0` — a well-formed ELF containing
# nothing. Every check that only looked for "Finished" would have passed.
size=$(wc -c < "$elf")
[ "$size" -gt 4096 ] || { echo "FAIL: the artefact is $size bytes — an empty link"; exit 1; }

# ⭐ THE ASSERTION THAT CATCHES THE WRONG MULTILIB. A hard-float libc linked
# into a soft-float image leaves `Tag_ABI_HardFP_use` in the attributes.
hard=$("$READELF" -A "$elf" 2>/dev/null | grep -c 'ABI_HardFP_use' || true)
[ "$hard" = "0" ] || {
echo "FAIL: a soft-float image carries $hard hard-float ABI tags — the wrong multilib was linked"
exit 1; }
echo " ok the soft-float row linked a soft-float C library"

# And it runs: the C library is not merely present, its printf works.
set +e
timeout 30 "$QEMU" -machine mps2-an385 -cpu cortex-m3 -nographic -semihosting \
-no-reboot -kernel "$elf" > run.log 2>&1
rc=$?
set -e
cat run.log
grep -q 'picolibc: 13.00' run.log \
|| { echo "FAIL: printf did not produce the expected output"; exit 1; }
[ "$rc" = "0" ] || { echo "FAIL: the image ran but exited $rc"; exit 1; }

echo "PASS: a Cortex-M project opts into picolibc and gets the right multilib"
45 changes: 45 additions & 0 deletions tests/unit/test_freestanding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,48 @@ TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) {
}
EXPECT_GT(rows, 0);
}

// ⚠️⚠️ THE MULTILIB KEY IS NOT `<march>/<mabi>` ON ARM, AND ASSUMING IT WAS
// PRODUCES AN ABI SUBSTITUTION THAT NOTHING REPORTS.
//
// The column 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.
//
// Measured while building `xim:picolibc-arm`: 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` — the
// hard-float build, where a soft-float program would find it. Nothing failed.
//
// So the assertion is not "libdir is non-empty": it is that the soft and hard
// variants of ONE architecture name DIFFERENT directories, which is the
// property the layout exists to preserve.
TEST(FreestandingTarget, Arm32SoftAndHardVariantsNameDifferentLibdirs) {
std::map<std::string, std::vector<std::string>> byMarch;
int rows = 0;
for (const auto& spec : mcpp::freestanding::known()) {
if (!is_arm32(spec.triple)) continue;
++rows;
EXPECT_FALSE(spec.libdir.empty())
<< spec.triple << " has no libdir, so an opt-in sysroot cannot be found";
byMarch[std::string(spec.march)].push_back(std::string(spec.libdir));
}
EXPECT_GT(rows, 0);

int pairs = 0;
for (auto const& [march, dirs] : byMarch) {
if (dirs.size() < 2) continue;
++pairs;
std::set<std::string> distinct(dirs.begin(), dirs.end());
EXPECT_EQ(distinct.size(), dirs.size())
<< march << " maps " << dirs.size() << " rows onto "
<< distinct.size() << " directories: a soft-float program would "
"link a hard-float library, silently";
}
// ⚠️ A denominator. If no architecture had two rows the loop above would
// assert nothing, and the property would be untested rather than held.
EXPECT_GT(pairs, 0) << "no architecture carries both float ABIs; the check "
"above quantified over nothing";
}
Loading