From c4300bb5cf369512c41a0ba13ff24caee9fa9700 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Fri, 16 Oct 2026 10:30:43 +0200 Subject: [PATCH 1/4] compiler-rt: Build builtins for bare-metal Arm Cortex-M MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standalone build only builds the runtimes for the host, so /usr/lib/clang/23/lib contains nothing but linux/, and linking for a bare-metal Arm target fails: $ clang --target=arm-none-eabi -mcpu=cortex-m4 -mfloat-abi=hard \ -fuse-ld=lld -nostartfiles -nolibc -Wl,-e,_start -o t.elf t.c ld.lld: error: cannot open /usr/lib/clang/23/lib/arm-unknown-none-eabi/libclang_rt.builtins.a: No such file or directory clang: error: linker command failed with exit code 1 (use -v to see invocation) Build the builtins a second time for six Cortex-M variants, named like those of Arm’s LLVM Embedded Toolchain: armv6m_soft_nofp Cortex-M0/M0+/M1/M23 armv7m_soft_nofp Cortex-M3, and any later core with -mfloat-abi=soft armv7em_soft_fpv4_sp_d16 Cortex-M4/M7, -mfloat-abi=softfp (default) armv7em_hard_fpv4_sp_d16 Cortex-M4/M7, -mfloat-abi=hard armv8m.main_soft_fp Cortex-M33/M35P, -mfloat-abi=softfp armv8m.main_hard_fp Cortex-M33/M35P, -mfloat-abi=hard They are installed into /usr/lib/clang-runtimes/arm-none-eabi/, clang’s default bare-metal sysroot, together with a multilib.yaml there. clang then picks the variant from -mcpu, -mfloat-abi and -mfpu, so --target=arm-none-eabi, armv7em-none-eabihf and thumbv7em-none-eabihf all work. With per-target directories in the resource directory instead, the --target spelling would have to match the directory name, and --target=arm-none-eabi -mfloat-abi=hard would silently link the soft-float library. The softfp variants are needed because clang defaults to -mfloat-abi=softfp for a core with FPU. That code uses the FPU, but the crt0 of a variant without FPU does not enable it, so with only the soft-float variants, picolibc’s hello world built with -mcpu=cortex-m33 faulted in QEMU on its first VFP instruction, a VLDR (encoding ed9f 0b17): ARM fault: hardfault LR: 0x100000b1 PC: 0x100001e2 The variants without FPU therefore require -mfpu=none, and a core without a matching variant, for example the Cortex-M55, gets clang’s -Wmissing-multilib warning instead of a library that crashes. compiler-rt appends the normalized target to the library directory, but clang only searches the variant’s lib directory, so move the archive up. The C library comes from to be added picolibc.be0, which uses the same variants. Assisted-by: claude-opus-5.5 (Claude Code) --- compiler-rt.be0 | 105 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/compiler-rt.be0 b/compiler-rt.be0 index ef3f17a0c..b4e8f94d7 100755 --- a/compiler-rt.be0 +++ b/compiler-rt.be0 @@ -1,6 +1,6 @@ #!/usr/bin/env beesh -# BEE_VERSION compiler-rt-23.1.3-0 +# BEE_VERSION compiler-rt-23.1.3-1 #SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/llvm-project-${PKGVERSION}.src.tar.xz" SRCURL[0]="https://beehive.molgen.mpg.de/d7e227fe471d80d5e2d411127a0123b1/llvm-project-23.1.3.src.tar.xz" @@ -11,6 +11,19 @@ SRCURL[0]="https://beehive.molgen.mpg.de/d7e227fe471d80d5e2d411127a0123b1/llvm-p sourcesubdir_append compiler-rt +# Bare-metal Arm builtins, one build per multilib variant: the directory below +# ${LIBDIR}/clang-runtimes, the target, and the flags. multilib.yaml, written +# in mee_install(), lets clang pick a variant from -mcpu/-mfloat-abi/-mfpu. +# picolibc.be0 installs the C library into the same directories. +BAREMETAL_VARIANTS=( + "arm-none-eabi/armv6m_soft_nofp thumbv6m-none-eabi -mfloat-abi=soft" + "arm-none-eabi/armv7m_soft_nofp thumbv7m-none-eabi -mfloat-abi=soft" + "arm-none-eabi/armv7em_soft_fpv4_sp_d16 thumbv7em-none-eabi -mfloat-abi=softfp -mfpu=fpv4-sp-d16" + "arm-none-eabi/armv7em_hard_fpv4_sp_d16 thumbv7em-none-eabihf -mfloat-abi=hard -mfpu=fpv4-sp-d16" + "arm-none-eabi/armv8m.main_soft_fp thumbv8m.main-none-eabi -mfloat-abi=softfp -mfpu=fpv5-sp-d16" + "arm-none-eabi/armv8m.main_hard_fp thumbv8m.main-none-eabihf -mfloat-abi=hard -mfpu=fpv5-sp-d16" +) + #mee_extract() { # bee_extract "${@}" #} @@ -33,12 +46,102 @@ mee_configure() { -DCMAKE_CXX_COMPILER=clang++ \ -Wno-dev \ -G Ninja + + local dir target flags + for variant in "${BAREMETAL_VARIANTS[@]}" ; do + read -r dir target flags <<< "${variant}" + start_cmd cmake \ + -S ${S}/lib/builtins \ + -B ${B}/baremetal/${dir} \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_SYSTEM_NAME=Generic \ + -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_ASM_COMPILER=clang \ + -DCMAKE_C_COMPILER_TARGET=${target} \ + -DCMAKE_ASM_COMPILER_TARGET=${target} \ + -DCMAKE_C_FLAGS="${flags}" \ + -DCMAKE_ASM_FLAGS="${flags}" \ + -DCMAKE_AR=$(command -v llvm-ar) \ + -DCMAKE_RANLIB=$(command -v llvm-ranlib) \ + -DCOMPILER_RT_STANDALONE_BUILD=ON \ + -DCOMPILER_RT_BAREMETAL_BUILD=ON \ + -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \ + -DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=ON \ + -DCOMPILER_RT_INSTALL_LIBRARY_DIR=${LIBDIR}/clang-runtimes/${dir}/lib \ + -DLLVM_CMAKE_DIR=$(llvm-config --cmakedir) \ + -Wno-dev \ + -G Ninja + done } mee_build() { start_cmd ninja -v -C ${B} ${BEE_NINJAFLAGS} "${@}" + + local dir target flags + for variant in "${BAREMETAL_VARIANTS[@]}" ; do + read -r dir target flags <<< "${variant}" + start_cmd ninja -v -C ${B}/baremetal/${dir} ${BEE_NINJAFLAGS} "${@}" + done } mee_install() { DESTDIR=${D} start_cmd ninja -C ${B} install "${@}" + + local dir target flags + for variant in "${BAREMETAL_VARIANTS[@]}" ; do + read -r dir target flags <<< "${variant}" + DESTDIR=${D} start_cmd ninja -C ${B}/baremetal/${dir} install "${@}" + + # compiler-rt appends the normalized target to the library + # directory, but clang only searches the variant's lib directory. + local libdir=${D}${LIBDIR}/clang-runtimes/${dir}/lib + start_cmd mv ${libdir}/*/libclang_rt.builtins.a ${libdir}/ + start_cmd rmdir ${libdir}/*/ + done + + # A variant matches if clang's flags (see -print-multi-flags-experimental) + # contain all of its flags, and of the matching variants the last one + # wins. The mappings let compatible cores use them: a library runs on any + # core of the same or a later architecture, and an FPv5 unit executes + # FPv4 code. The variants without FPU require -mfpu=none, as their crt0 + # does not enable the FPU, which code built with -mfloat-abi=softfp for + # a core with FPU uses. + cat > ${D}${LIBDIR}/clang-runtimes/multilib.yaml <<-'EOF' + MultilibVersion: 1.0 + + Groups: + - Name: stdlibs + Type: Exclusive + + Variants: + - Dir: arm-none-eabi/armv6m_soft_nofp + Flags: [--target=thumbv6m-unknown-none-eabi, -mfpu=none] + Group: stdlibs + - Dir: arm-none-eabi/armv7m_soft_nofp + Flags: [--target=thumbv7m-unknown-none-eabi, -mfpu=none] + Group: stdlibs + - Dir: arm-none-eabi/armv7em_soft_fpv4_sp_d16 + Flags: [--target=thumbv7em-unknown-none-eabi, -mfpu=fpv4-sp-d16] + Group: stdlibs + - Dir: arm-none-eabi/armv7em_hard_fpv4_sp_d16 + Flags: [--target=thumbv7em-unknown-none-eabihf, -mfpu=fpv4-sp-d16] + Group: stdlibs + - Dir: arm-none-eabi/armv8m.main_soft_fp + Flags: [--target=thumbv8m.main-unknown-none-eabi, -mfpu=fpv5-sp-d16] + Group: stdlibs + - Dir: arm-none-eabi/armv8m.main_hard_fp + Flags: [--target=thumbv8m.main-unknown-none-eabihf, -mfpu=fpv5-sp-d16] + Group: stdlibs + + Mappings: + - Match: --target=thumbv8m\.base-unknown-none-eabi + Flags: [--target=thumbv6m-unknown-none-eabi] + - Match: --target=thumbv(7em|8m\.main|8\.1m\.main)-unknown-none-eabi + Flags: [--target=thumbv7m-unknown-none-eabi] + - Match: -mfpu=fpv5-(sp-)?d16 + Flags: [-mfpu=fpv4-sp-d16] + - Match: -mfpu=fpv5-d16 + Flags: [-mfpu=fpv5-sp-d16] + EOF } From 01fd678f4e0ecde81d0c3ab87e3c89d7693da0cd Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Fri, 16 Oct 2026 10:31:08 +0200 Subject: [PATCH 2/4] picolibc: Add version 1.8.12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clang finds the compiler-rt builtins for bare-metal Arm now, but there is no C library for these targets: $ echo '#include ' | clang --target=arm-none-eabi -mcpu=cortex-m4 -x c -c - -o /dev/null :1:10: fatal error: 'stdio.h' file not found picolibc is the C library for embedded systems that Arm’s LLVM Embedded Toolchain uses. Build it for the six Cortex-M variants of compiler-rt.be0, into the same directories below /usr/lib/clang-runtimes/arm-none-eabi, so that clang’s multilib.yaml selects both together. Each variant gets its own meson cross file, as picolibc’s own cross-clang-*.txt files call arm-none-eabi-ar and link against libgcc from an arm-none-eabi-gcc installation. Deviations from them: - -nostdlibinc, so that the headers of an installed picolibc do not shadow the ones being built. - c_ld = 'lld', as clang’s bare-metal driver defaults to GNU ld, which on MarIuX only supports x86: ERROR: Unable to detect linker for compiler `clang --target=thumbv6m-none-eabi -mfloat-abi=soft -nostdlibinc -nostdlib -Wl,--version` stderr: /usr/bin/ld: unrecognised emulation mode: armelf - No -fshort-enums. picolibc passes it to match arm-none-eabi-gcc, but clang defaults to 32-bit enums, and the library has to match the code compiled with clang that links against it. -Dspecsdir=none, as picolibc would otherwise install picolibc.specs into clang’s install directory, /usr/bin. Linked with the defaults (crt0-semihost.o, picolibc.ld, -lsemihost, -fuse-ld=lld), a hello world doing 64-bit division and float/double printf runs in QEMU 11.1.0 for every variant: -mcpu=cortex-m0plus microbit armv6m_soft_nofp -mcpu=cortex-m3 mps2-an385 armv7m_soft_nofp -mcpu=cortex-m4 -mfloat-abi=soft mps2-an386 armv7m_soft_nofp -mcpu=cortex-m4 mps2-an386 armv7em_soft_fpv4_sp_d16 -mcpu=cortex-m4 -mfloat-abi=hard mps2-an386 armv7em_hard_fpv4_sp_d16 --target=thumbv7em-none-eabihf mps2-an386 armv7em_hard_fpv4_sp_d16 -mcpu=cortex-m7 mps2-an500 armv7em_soft_fpv4_sp_d16 -mcpu=cortex-m7 -mfloat-abi=hard mps2-an500 armv7em_hard_fpv4_sp_d16 -mcpu=cortex-m33 mps2-an505 armv8m.main_soft_fp -mcpu=cortex-m33 -mfloat-abi=hard mps2-an505 armv8m.main_hard_fp -mcpu=cortex-m33 -mfloat-abi=soft mps2-an505 armv7m_soft_nofp hello: 176366841446 6.250 0.333333 Assisted-by: claude-opus-5.5 (Claude Code) --- picolibc.be0 | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100755 picolibc.be0 diff --git a/picolibc.be0 b/picolibc.be0 new file mode 100755 index 000000000..ab0b9d118 --- /dev/null +++ b/picolibc.be0 @@ -0,0 +1,91 @@ +#!/usr/bin/env beesh + +# BEE_VERSION picolibc-1.8.12-0 + +# SRCURL[0]="https://github.com/picolibc/picolibc/releases/download/${PKGVERSION}/picolibc-${PKGVERSION}.tar.xz" +SRCURL[0]="https://beehive.molgen.mpg.de/7219ddf0193c83528f22b7b8fbc1fff4/picolibc-1.8.12.tar.xz" + +# PATCHURL+=() + +# build_in_sourcedir + +# sourcesubdir_append src + +# Bare-metal Arm C library, one build per multilib variant: the directory +# below ${LIBDIR}/clang-runtimes, the target, and the flags. Keep the list in +# sync with compiler-rt.be0, which builds the builtins for these variants and +# installs the multilib.yaml that lets clang pick one. +BAREMETAL_VARIANTS=( + "arm-none-eabi/armv6m_soft_nofp thumbv6m-none-eabi -mfloat-abi=soft" + "arm-none-eabi/armv7m_soft_nofp thumbv7m-none-eabi -mfloat-abi=soft" + "arm-none-eabi/armv7em_soft_fpv4_sp_d16 thumbv7em-none-eabi -mfloat-abi=softfp -mfpu=fpv4-sp-d16" + "arm-none-eabi/armv7em_hard_fpv4_sp_d16 thumbv7em-none-eabihf -mfloat-abi=hard -mfpu=fpv4-sp-d16" + "arm-none-eabi/armv8m.main_soft_fp thumbv8m.main-none-eabi -mfloat-abi=softfp -mfpu=fpv5-sp-d16" + "arm-none-eabi/armv8m.main_hard_fp thumbv8m.main-none-eabihf -mfloat-abi=hard -mfpu=fpv5-sp-d16" +) + +#mee_extract() { +# bee_extract "${@}" +#} + +#mee_patch() { +# bee_patch "${@}" +#} + +mee_configure() { + local dir target flags + for variant in "${BAREMETAL_VARIANTS[@]}" ; do + read -r dir target flags <<< "${variant}" + + # -nostdlibinc keeps the headers of an installed picolibc out of + # the build, -nostdlib the libraries. clang's bare-metal driver + # defaults to GNU ld, which on MarIuX only knows x86. + mkdir -p ${B}/cross/${dir%/*} + cat > ${B}/cross/${dir}.txt <<-EOF + [binaries] + c = ['clang', '--target=${target}', $(printf "'%s', " ${flags})'-nostdlibinc', '-nostdlib'] + c_ld = 'lld' + ar = 'llvm-ar' + nm = 'llvm-nm' + strip = 'llvm-strip' + + [host_machine] + system = 'none' + cpu_family = 'arm' + cpu = 'arm' + endian = 'little' + + [properties] + skip_sanity_check = true + EOF + + # Without -Dspecsdir=none the GCC specs file would be installed + # into clang's install directory, /usr/bin. + start_cmd meson setup \ + --cross-file ${B}/cross/${dir}.txt \ + --prefix=${LIBDIR}/clang-runtimes/${dir} \ + --includedir=include \ + --libdir=lib \ + --wrap-mode=nofallback \ + -Dmultilib=false \ + -Dspecsdir=none \ + ${B}/${dir} \ + ${S} + done +} + +mee_build() { + local dir target flags + for variant in "${BAREMETAL_VARIANTS[@]}" ; do + read -r dir target flags <<< "${variant}" + start_cmd ninja -v -C ${B}/${dir} ${BEE_NINJAFLAGS} "${@}" + done +} + +mee_install() { + local dir target flags + for variant in "${BAREMETAL_VARIANTS[@]}" ; do + read -r dir target flags <<< "${variant}" + DESTDIR=${D} start_cmd ninja -C ${B}/${dir} install "${@}" + done +} From 9375747f16da64f4acca469cda4c888f5bab3751 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Fri, 16 Oct 2026 14:52:52 +0200 Subject: [PATCH 3/4] smoke-tests: Add baremetal-arm-smoke-test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compiler-rt.be0 and picolibc.be0 build the same six Cortex-M variants into /usr/lib/clang-runtimes, and multilib.yaml lets clang pick one. A wrong pick still links and only fails on the hardware, so check the selection for 17 combinations of core, float ABI and target spelling, the files of every variant, and run a program doing 64-bit division and float and double printf for eleven of them in QEMU with semihosting. Like llvm-smoke-test, --prefix mounts packages over /usr in a user namespace, so they are tested before they are installed. With the packages from this branch: $ TMPDIR=/dev/shm scripts/smoke-tests/baremetal-arm-smoke-test \ --qemu /pkg/qemu-11.1.0-0/bin/qemu-system-arm \ --prefix compiler-rt-23.1.3-1.x86_64.bee.tar.bz2 \ --prefix picolibc-1.8.12-0.x86_64.bee.tar.bz2 […] OK Cortex-M33 softfp runs on mps2-an505 with arm-none-eabi/armv8m.main_soft_fp OK Cortex-M33 hard runs on mps2-an505 with arm-none-eabi/armv8m.main_hard_fp OK default crt0, libc and builtins from arm-none-eabi/armv7em_hard_fpv4_sp_d16 PASS (0 failed, 0 warned) The QEMU 5.1.0 in /usr/bin has no Cortex-M4 or Cortex-M7 board, so with it those six programs are only linked, and reported as WARN. With the first draft of multilib.yaml, which had no softfp variants and did not require -mfpu=none for the variants without FPU, it fails: FAIL Cortex-M4 softfp (--target=arm-none-eabi -mcpu=cortex-m4): expected arm-none-eabi/armv7em_soft_fpv4_sp_d16, got 'arm-none-eabi/armv7m_soft_nofp' […] FAIL Cortex-M4 softfp on mps2-an386: exit status 1: ARM fault: hardfault FAIL Cortex-M7 softfp on mps2-an500: exit status 1: ARM fault: hardfault FAIL Cortex-M33 softfp on mps2-an505: exit status 1: ARM fault: hardfault FAIL (7 failed, 0 warned) Every link passes -fuse-ld=lld. For bare-metal targets clang defaults to the GNU ld in /usr/bin, which on MarIuX only knows x86: $ clang --target=arm-none-eabi -mcpu=cortex-m3 -T picolibc.ld \ -o ld.elf hello.c -lsemihost /usr/bin/ld: unrecognised emulation mode: armelf That is clang’s configuration, not a fault of the runtimes, so it is not checked. CLANG_DEFAULT_LINKER would change the host’s linker too; clang configuration files per target triple are the better candidate. Two things found on the way: an automount such as /pkg/qemu-11.1.0-0 cannot be triggered from within the user namespace ("Too many levels of symbolic links"), so the script runs QEMU once before entering it; and QEMU 11 writes the semihosting console to stderr unless that is a tty. Assisted-by: claude-opus-5.5 (Claude Code) --- scripts/smoke-tests/baremetal-arm-smoke-test | 381 +++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100755 scripts/smoke-tests/baremetal-arm-smoke-test diff --git a/scripts/smoke-tests/baremetal-arm-smoke-test b/scripts/smoke-tests/baremetal-arm-smoke-test new file mode 100755 index 000000000..982a79e1b --- /dev/null +++ b/scripts/smoke-tests/baremetal-arm-smoke-test @@ -0,0 +1,381 @@ +#! /usr/bin/env python3 + +# Smoke-test the bare-metal Arm runtimes: compiler-rt's Cortex-M builtins and +# picolibc below /usr/lib/clang-runtimes, selected by its multilib.yaml. +# +# scripts/smoke-tests/baremetal-arm-smoke-test +# TMPDIR=/dev/shm scripts/smoke-tests/baremetal-arm-smoke-test \ +# --qemu /pkg/qemu-11.1.0-0/bin/qemu-system-arm \ +# --prefix /src/mariux/beeroot/packages/compiler-rt-23.1.3-1.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/picolibc-1.8.12-0.x86_64.bee.tar.bz2 +# +# compiler-rt.be0 and picolibc.be0 each build the same list of multilib +# variants into /usr/lib/clang-runtimes/arm-none-eabi/, and clang picks +# one from -mcpu, -mfloat-abi and -mfpu. A wrong pick still links, and only +# fails on the hardware: with only soft-float variants, code built with the +# default -mfloat-abi=softfp for a Cortex-M33 got the crt0 of a variant without +# FPU, which never enables it, and hard-faulted on the first VLDR. So this +# +# - checks which variant clang selects for each core and float ABI, and that +# a core without a variant gets -Wmissing-multilib instead of a wrong one, +# - checks that each variant has crt0, libc, the builtins, the linker script +# and the headers, +# - links a program doing 64-bit division and float and double printf for +# each core and runs it in QEMU with semihosting, +# - links once without naming crt0, so that clang adds crt0.o, -lc and the +# builtins on its own. +# +# Every link passes -fuse-ld=lld: for bare-metal targets clang defaults to the +# GNU ld in /usr/bin, which on MarIuX only knows x86 ("unrecognised emulation +# mode: armelf"). That is clang's configuration, not a fault of the runtimes, +# so it is not checked here. +# +# --prefix (repeatable) takes the image directory of a build, or a .bee.tar.bz2 +# package, which is unpacked below $TMPDIR. They are mounted read-only over /usr +# in a user namespace, so packages can be checked before they are installed, +# without root: clang looks for the runtimes in ../lib/clang-runtimes relative +# to its binary, so they have to show up below the same /usr as clang. +# +# --qemu names qemu-system-arm. The QEMU 5.1.0 in /usr/bin has no mps2-an386 +# (Cortex-M4) or mps2-an500 (Cortex-M7); cores without a board are linked but +# not run, and that is reported as WARN. Failures exit non-zero. + +import argparse +import os +import re +import shlex +import shutil +import subprocess +import sys +import tempfile + +# How --prefix survives the re-exec into the mount namespace. +PREFIX_VARIABLE = "BAREMETAL_ARM_SMOKE_TEST_PREFIX" + +RUNTIMES = "/usr/lib/clang-runtimes" + +HELLO = r""" +#include +int main(void) +{ + volatile unsigned long long a = 1234567890123ULL, b = 7; + volatile float f = 2.5f; + volatile double d = 1.0 / 3.0; + printf("hello: %llu %.3f %.6f\n", a / b, (double)(f * f), d); + return 0; +} +""" +EXPECT = "hello: 176366841446 6.250 0.333333" + +VARIANTS = [ + "arm-none-eabi/armv6m_soft_nofp", + "arm-none-eabi/armv7m_soft_nofp", + "arm-none-eabi/armv7em_soft_fpv4_sp_d16", + "arm-none-eabi/armv7em_hard_fpv4_sp_d16", + "arm-none-eabi/armv8m.main_soft_fp", + "arm-none-eabi/armv8m.main_hard_fp", +] + +# Name, clang flags, expected variant (None: no variant, -Wmissing-multilib). +SELECTIONS = [ + ("Cortex-M0+", "--target=arm-none-eabi -mcpu=cortex-m0plus", VARIANTS[0]), + ("Cortex-M23", "--target=arm-none-eabi -mcpu=cortex-m23", VARIANTS[0]), + ("thumbv6m triple", "--target=thumbv6m-none-eabi", VARIANTS[0]), + ("Cortex-M3", "--target=arm-none-eabi -mcpu=cortex-m3", VARIANTS[1]), + ("Cortex-M4 soft", "--target=arm-none-eabi -mcpu=cortex-m4 -mfloat-abi=soft", + VARIANTS[1]), + ("Cortex-M4 softfp", "--target=arm-none-eabi -mcpu=cortex-m4", VARIANTS[2]), + ("Cortex-M4 hard", "--target=arm-none-eabi -mcpu=cortex-m4 -mfloat-abi=hard", + VARIANTS[3]), + ("armv7em-eabihf triple", "--target=armv7em-none-eabihf", VARIANTS[3]), + ("thumbv7em-eabihf triple", "--target=thumbv7em-none-eabihf", VARIANTS[3]), + ("Cortex-M7 softfp", "--target=arm-none-eabi -mcpu=cortex-m7", VARIANTS[2]), + ("Cortex-M7 hard", "--target=arm-none-eabi -mcpu=cortex-m7 -mfloat-abi=hard", + VARIANTS[3]), + ("Cortex-M33 soft", + "--target=arm-none-eabi -mcpu=cortex-m33 -mfloat-abi=soft", VARIANTS[1]), + ("Cortex-M33 softfp", "--target=arm-none-eabi -mcpu=cortex-m33", VARIANTS[4]), + ("Cortex-M33 hard", + "--target=arm-none-eabi -mcpu=cortex-m33 -mfloat-abi=hard", VARIANTS[5]), + ("Cortex-M33 without FPU", "--target=arm-none-eabi -mcpu=cortex-m33+nofp", + VARIANTS[1]), + ("Cortex-M55 softfp", "--target=arm-none-eabi -mcpu=cortex-m55", None), + ("Cortex-M55 hard", + "--target=arm-none-eabi -mcpu=cortex-m55 -mfloat-abi=hard", None), +] + +# Memory maps: flash, flash size, RAM, RAM size. The micro:bit has 16 KiB of +# RAM; a stack placed above it locks the core up before main(). +MICROBIT = ("0x0", "0x40000", "0x20000000", "0x4000") +MPS2 = ("0x0", "0x100000", "0x20000000", "0x100000") +MPS2_AN505 = ("0x10000000", "0x100000", "0x38000000", "0x100000") + +# Name, clang flags, QEMU machine, memory map. The micro:bit's Cortex-M0 runs +# Armv6-M code just like a Cortex-M0+. +PROGRAMS = [ + ("Cortex-M0+", "--target=arm-none-eabi -mcpu=cortex-m0plus", "microbit", + MICROBIT), + ("Cortex-M3", "--target=arm-none-eabi -mcpu=cortex-m3", "mps2-an385", MPS2), + ("Cortex-M4 soft", "--target=arm-none-eabi -mcpu=cortex-m4 -mfloat-abi=soft", + "mps2-an386", MPS2), + ("Cortex-M4 softfp", "--target=arm-none-eabi -mcpu=cortex-m4", "mps2-an386", + MPS2), + ("Cortex-M4 hard", "--target=arm-none-eabi -mcpu=cortex-m4 -mfloat-abi=hard", + "mps2-an386", MPS2), + ("thumbv7em-eabihf triple", "--target=thumbv7em-none-eabihf", "mps2-an386", + MPS2), + ("Cortex-M7 softfp", "--target=arm-none-eabi -mcpu=cortex-m7", "mps2-an500", + MPS2), + ("Cortex-M7 hard", "--target=arm-none-eabi -mcpu=cortex-m7 -mfloat-abi=hard", + "mps2-an500", MPS2), + ("Cortex-M33 soft", + "--target=arm-none-eabi -mcpu=cortex-m33 -mfloat-abi=soft", "mps2-an505", + MPS2_AN505), + ("Cortex-M33 softfp", "--target=arm-none-eabi -mcpu=cortex-m33", + "mps2-an505", MPS2_AN505), + ("Cortex-M33 hard", + "--target=arm-none-eabi -mcpu=cortex-m33 -mfloat-abi=hard", "mps2-an505", + MPS2_AN505), +] + +status = {"fail": 0, "warn": 0} + + +def report(state, message): + if state != "ok": + status[state] += 1 + print("%-5s %s" % (state.upper(), message)) + + +def run(command, **kwargs): + """Run command, capturing its output, never raising for its exit status.""" + try: + return subprocess.run(command, capture_output=True, text=True, + timeout=120, **kwargs) + except (OSError, subprocess.TimeoutExpired) as error: + return subprocess.CompletedProcess(command, 127, "", str(error)) + + +def last_line(process): + """The line of the output that says what went wrong.""" + lines = (process.stderr or process.stdout).strip().splitlines() + for line in lines: + # The linker says why before clang says that it failed. + if re.search(r"error|cannot find|undefined symbol|unrecognised|fault", + line) and \ + "linker command failed" not in line: + return line.strip() + return lines[-1] if lines else "exit status %d" % process.returncode + + +# ---------------------------------------------------------------- --prefix + +def unpack(items, workdir): + """Return the directories to overlay, unpacking package archives.""" + lowers = [] + for number, item in enumerate(items): + if os.path.isdir(item): + path = os.path.abspath(item.rstrip("/")) + elif os.path.isfile(item): + path = os.path.join(workdir, str(number)) + os.mkdir(path) + print(" unpacking %s" % item, flush=True) + process = subprocess.run(["tar", "-C", path, "-xf", item], + capture_output=True, text=True) + if process.returncode: + sys.exit("cannot unpack %s: %s" % (item, process.stderr)) + else: + sys.exit("no such directory or package: %s" % item) + if ":" in path or "," in path: + sys.exit("cannot overlay %s: overlayfs separates layers by ':'" + " and options by ','" % path) + if not os.path.isdir(path + "/usr"): + sys.exit("%s has no usr/" % item) + lowers.append(path) + return lowers + + +def run_in_overlay(lowers): + """Run this script again with lowers mounted over /usr.""" + layers = ":".join([lower + "/usr" for lower in lowers] + ["/usr"]) + setup = "set -e\nmount -t overlay overlay -o lowerdir=%s /usr\nexec \"$@\"" \ + % shlex.quote(layers) + environment = dict(os.environ) + environment[PREFIX_VARIABLE] = ":".join(lowers) + command = ["unshare", "--user", "--map-root-user", "--mount", + "sh", "-c", setup, "sh", sys.executable, + os.path.abspath(__file__)] + sys.argv[1:] + try: + return subprocess.run(command, env=environment).returncode + except OSError as error: + sys.exit("cannot re-run in a mount namespace (%s); install the" + " packages and run without --prefix" % error) + + +# ----------------------------------------------------------------- checks + +def check_layout(): + if not os.path.isfile(RUNTIMES + "/multilib.yaml"): + report("fail", "no %s/multilib.yaml" % RUNTIMES) + for variant in VARIANTS: + directory = os.path.join(RUNTIMES, variant) + missing = [name for name in ("lib/crt0.o", "lib/crt0-semihost.o", + "lib/libc.a", "lib/libsemihost.a", + "lib/libclang_rt.builtins.a", + "lib/picolibc.ld", "include/stdio.h") + if not os.path.isfile(os.path.join(directory, name))] + if missing: + report("fail", "%s lacks %s" % (variant, ", ".join(missing))) + else: + report("ok", "%s: crt0, libc, builtins, picolibc.ld, headers" + % variant) + + +def check_selection(name, flags, expected): + process = run(["clang"] + flags.split() + ["-print-multi-directory"]) + selected = process.stdout.strip() + warned = "no multilib found" in process.stderr + if expected is None: + if warned and not selected: + report("ok", "%s (%s): no variant, -Wmissing-multilib" + % (name, flags)) + else: + report("fail", "%s (%s): expected -Wmissing-multilib, got %r" + % (name, flags, selected)) + elif selected == expected and not warned: + report("ok", "%s (%s) -> %s" % (name, flags, selected)) + else: + report("fail", "%s (%s): expected %s, got %r%s" + % (name, flags, expected, selected, + ", -Wmissing-multilib" if warned else "")) + + +def link_flags(memory): + flash, flash_size, ram, ram_size = memory + return ["-O2", "-fuse-ld=lld", "-T", "picolibc.ld", + "-Wl,--defsym=__flash=" + flash, + "-Wl,--defsym=__flash_size=" + flash_size, + "-Wl,--defsym=__ram=" + ram, + "-Wl,--defsym=__ram_size=" + ram_size] + + +def qemu_machines(qemu): + process = run([qemu, "-M", "help"]) if qemu else None + if not process or process.returncode: + return set() + return {line.split()[0] for line in process.stdout.splitlines()[1:] + if line.strip()} + + +def check_program(name, flags, machine, memory, qemu, machines, directory): + flags = flags.split() + variant = run(["clang"] + flags + ["-print-multi-directory"]).stdout.strip() + output = re.sub(r"\W+", "-", name.lower()) + ".elf" + # crt0-semihost.o makes main()'s return value QEMU's exit status. + process = run(["clang"] + flags + link_flags(memory) + + ["-nostartfiles", + os.path.join(RUNTIMES, variant, "lib/crt0-semihost.o"), + "-o", output, "hello.c", "-lsemihost"], cwd=directory) + if process.returncode: + report("fail", "%s: link failed: %s" % (name, last_line(process))) + return + if machine not in machines: + report("warn", "%s: linked, but not run: %s has no %s board" + % (name, qemu or "no qemu-system-arm", machine)) + return + process = run(["timeout", "30", qemu, "-M", machine, "-nographic", + "-monitor", "none", "-serial", "none", + "-semihosting-config", "enable=on,target=native", + "-kernel", output], cwd=directory) + # QEMU 11 writes the semihosting console to stderr unless it is a tty. + printed = process.stdout + process.stderr + if process.returncode or EXPECT not in printed: + report("fail", "%s on %s: exit status %d: %s" + % (name, machine, process.returncode, + last_line(process) if process.returncode else + printed.strip())) + else: + report("ok", "%s runs on %s with %s" % (name, machine, variant)) + + +def check_default_link(directory): + """Let clang add crt0.o, -lc and the builtins itself.""" + name = "default crt0, libc and builtins" + flags = ["--target=arm-none-eabi", "-mcpu=cortex-m4", "-mfloat-abi=hard"] + command = ["clang"] + flags + link_flags(MPS2) + \ + ["-o", "default.elf", "hello.c", "-lsemihost"] + process = run(command, cwd=directory) + if process.returncode: + report("fail", "%s: link failed: %s" % (name, last_line(process))) + return + variant = os.path.join(RUNTIMES, VARIANTS[3], "lib") + # clang spells the paths /usr/bin/../lib/clang-runtimes/... + arguments = set() + for line in run(command + ["-###"], cwd=directory).stderr.splitlines(): + if "ld.lld" in line: + arguments = {os.path.normpath(argument) + for argument in shlex.split(line)} + missing = [item for item in (variant + "/crt0.o", "-lc", + variant + "/libclang_rt.builtins.a") + if item not in arguments] + if missing: + report("fail", "%s: link line lacks %s" % (name, ", ".join(missing))) + else: + report("ok", "%s from %s" % (name, VARIANTS[3])) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--prefix", action="append", default=[], + metavar="DIR|PACKAGE", + help="test the image directory DIR of a build, or the" + " .bee.tar.bz2 PACKAGE, instead of what is" + " installed; may be repeated") + parser.add_argument("--qemu", default=shutil.which("qemu-system-arm"), + help="qemu-system-arm to run the programs with" + " (default: from PATH)") + arguments = parser.parse_args() + + if arguments.prefix and PREFIX_VARIABLE not in os.environ: + # An automount, such as /pkg/qemu-11.1.0-0, cannot be triggered from + # within the user namespace ("Too many levels of symbolic links"), + # so mount QEMU's directory now, for the namespace to inherit it. + if arguments.qemu: + run([arguments.qemu, "--version"]) + workdir = tempfile.mkdtemp(prefix="baremetal-arm-smoke-") + try: + return run_in_overlay(unpack(arguments.prefix, workdir)) + finally: + shutil.rmtree(workdir, ignore_errors=True) + if PREFIX_VARIABLE in os.environ: + print(" testing, over /usr: %s" + % os.environ[PREFIX_VARIABLE].replace(":", " ")) + + machines = qemu_machines(arguments.qemu) + if arguments.qemu: + version = run([arguments.qemu, "--version"]).stdout.splitlines() + print(" running with %s (%s)" + % (arguments.qemu, version[0] if version else "unknown version")) + + directory = tempfile.mkdtemp(prefix="baremetal-arm-smoke-") + try: + with open(os.path.join(directory, "hello.c"), "w") as source: + source.write(HELLO) + check_layout() + for name, flags, expected in SELECTIONS: + check_selection(name, flags, expected) + for name, flags, machine, memory in PROGRAMS: + check_program(name, flags, machine, memory, arguments.qemu, + machines, directory) + check_default_link(directory) + finally: + shutil.rmtree(directory, ignore_errors=True) + + print("%s (%d failed, %d warned)" + % ("FAIL" if status["fail"] else "PASS", status["fail"], + status["warn"])) + return 1 if status["fail"] else 0 + + +if __name__ == "__main__": + sys.exit(main()) From f1a9e32b497613ec5fd5c231dc2061c779ecad05 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Fri, 16 Oct 2026 16:09:45 +0200 Subject: [PATCH 4/4] smoke-tests: Let the --prefix namespace see automounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For --prefix, the smoke tests re-run themselves in a mount namespace made with `unshare --user --map-root-user --mount`, and unshare(1) defaults to private mount propagation. An automount triggered from inside, such as a /pkg package not yet mounted, is then mounted by the daemon in the host’s namespace and never shows up in the private copy: $ unshare --user --map-root-user --mount ls /pkg/boost-1.86.0-0 ls: cannot open directory '/pkg/boost-1.86.0-0': Too many levels of symbolic links So pass `--propagation slave`: host mounts, the automounts included, then reach the namespace, while the overlay over /usr and every other mount made inside still do not leak out to the host. $ unshare --user --map-root-user --mount --propagation slave \ ls /pkg/perl-5.34.0-0/profile /pkg/perl-5.34.0-0/profile This makes running QEMU once before re-executing, which baremetal-arm-smoke-test did to mount /pkg/qemu-11.1.0-0, otiose, so drop it. Now every /pkg path works, not only QEMU’s. Host mount changes now also reach the namespace, among them the autofs expiry after timeout=300, but the next access simply mounts it again. With /pkg/qemu-11.1.0-0 not mounted beforehand: $ TMPDIR=/dev/shm scripts/smoke-tests/baremetal-arm-smoke-test \ --qemu /pkg/qemu-11.1.0-0/bin/qemu-system-arm \ --prefix compiler-rt-23.1.3-1.x86_64.bee.tar.bz2 \ --prefix picolibc-1.8.12-0.x86_64.bee.tar.bz2 testing, over /usr: /dev/shm/baremetal-arm-smoke-d9n6vdgy/0 /dev/shm/baremetal-arm-smoke-d9n6vdgy/1 running with /pkg/qemu-11.1.0-0/bin/qemu-system-arm (QEMU emulator version 11.1.0) […] OK Cortex-M33 hard runs on mps2-an505 with arm-none-eabi/armv8m.main_hard_fp OK default crt0, libc and builtins from arm-none-eabi/armv7em_hard_fpv4_sp_d16 PASS (0 failed, 0 warned) Assisted-by: claude-opus-5.5 (Claude Code) --- scripts/smoke-tests/baremetal-arm-smoke-test | 6 +----- scripts/smoke-tests/llvm-smoke-test | 1 + scripts/smoke-tests/mesa-smoke-test | 1 + scripts/smoke-tests/webkitgtk-smoke-test | 1 + 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/smoke-tests/baremetal-arm-smoke-test b/scripts/smoke-tests/baremetal-arm-smoke-test index 982a79e1b..b18c9ba99 100755 --- a/scripts/smoke-tests/baremetal-arm-smoke-test +++ b/scripts/smoke-tests/baremetal-arm-smoke-test @@ -203,6 +203,7 @@ def run_in_overlay(lowers): environment = dict(os.environ) environment[PREFIX_VARIABLE] = ":".join(lowers) command = ["unshare", "--user", "--map-root-user", "--mount", + "--propagation", "slave", "sh", "-c", setup, "sh", sys.executable, os.path.abspath(__file__)] + sys.argv[1:] try: @@ -337,11 +338,6 @@ def main(): arguments = parser.parse_args() if arguments.prefix and PREFIX_VARIABLE not in os.environ: - # An automount, such as /pkg/qemu-11.1.0-0, cannot be triggered from - # within the user namespace ("Too many levels of symbolic links"), - # so mount QEMU's directory now, for the namespace to inherit it. - if arguments.qemu: - run([arguments.qemu, "--version"]) workdir = tempfile.mkdtemp(prefix="baremetal-arm-smoke-") try: return run_in_overlay(unpack(arguments.prefix, workdir)) diff --git a/scripts/smoke-tests/llvm-smoke-test b/scripts/smoke-tests/llvm-smoke-test index 252e4c854..23141297c 100755 --- a/scripts/smoke-tests/llvm-smoke-test +++ b/scripts/smoke-tests/llvm-smoke-test @@ -173,6 +173,7 @@ def run_in_overlay(lowers): environment = dict(os.environ) environment[PREFIX_VARIABLE] = ":".join(lowers) command = ["unshare", "--user", "--map-root-user", "--mount", + "--propagation", "slave", "sh", "-c", setup, "sh", sys.executable, os.path.abspath(__file__)] + sys.argv[1:] try: diff --git a/scripts/smoke-tests/mesa-smoke-test b/scripts/smoke-tests/mesa-smoke-test index 4017ef51c..dbe645413 100755 --- a/scripts/smoke-tests/mesa-smoke-test +++ b/scripts/smoke-tests/mesa-smoke-test @@ -111,6 +111,7 @@ def run_in_overlay(lowers): environment = dict(os.environ) environment[PREFIX_VARIABLE] = ":".join(lowers) command = ["unshare", "--user", "--map-root-user", "--mount", + "--propagation", "slave", "sh", "-c", "\n".join(setup), "sh", sys.executable, os.path.abspath(__file__)] + sys.argv[1:] try: diff --git a/scripts/smoke-tests/webkitgtk-smoke-test b/scripts/smoke-tests/webkitgtk-smoke-test index 3b921f294..c885650b1 100755 --- a/scripts/smoke-tests/webkitgtk-smoke-test +++ b/scripts/smoke-tests/webkitgtk-smoke-test @@ -478,6 +478,7 @@ ln -s %s /usr/libexec/%s exec "$@" """ % (shlex.quote(libexec), api["subdir"]) command = ["unshare", "--user", "--map-root-user", "--mount", + "--propagation", "slave", "sh", "-c", setup, "sh", sys.executable, os.path.abspath(__file__)] + sys.argv[1:] try: