Add a linux-armv6 release leg built in an emulated Raspbian container - #48
Merged
chrisuthe merged 6 commits intoAug 30, 2026
Merged
Conversation
A Pi Zero, a Pi Zero W and an original Pi are ARM1176, and until now they had no archive at all: scripts/get_started_linux.sh refused them and the docs told them to build from source. ARMv6 cannot be reached the way linux-armv7 is. Debian and Ubuntu armhf are an ARMv7-A port, so a cross toolchain's own crt1.o, crtbegin.o and every member of libgcc.a are ARMv7 and are linked into the binary whatever -march said -- which is why scripts/build_arm32.sh refuses armv6 by name, and why that refusal stands. Raspbian's really are ARMv6, its gcc being configured --with-arch=armv6 --with-float=hard, and Raspbian publishes no cross toolchain. So this leg is an ordinary native build run inside a digest-pinned Raspbian bookworm container under qemu-user, owned by scripts/build_armv6_container.sh for the reason build_arm32.sh is a script: the archive is the only build of this project a Pi Zero owner will ever run, and it has to be reproducible without a runner. Three things about that container are load-bearing and none of them are about the instruction set. Everything runs under QEMU_CPU=arm1176, because qemu-arm otherwise emulates a Cortex-A15-class core and would be more permissive than the hardware -- at configure time as well as in the suite, a cmake try_run probe asking what the CPU can do being answered for the wrong CPU. The container runs --init and builds as a non-root user, without which the smoke test reads an unreaped zombie as a player that outlived SIGTERM and three StateStore cases fail on a permission bit root ignores. And the link needs -latomic, ARMv6 having no LDREXD; it goes in CMAKE_CXX_STANDARD_LIBRARIES rather than CMAKE_EXE_LINKER_FLAGS, which places it ahead of the objects that reference it and leaves the linker to discard it. The leg is the one in the matrix without -DSENDSPIN_CLI_WERROR=ON, for the reason the pipewire-minimum job already documents: gcc 12.2.0's -Wrestrict false positive, at src/control_common.cpp:391 and :401 and additionally at tests/cli_test.cpp:1133. -Wno-restrict is not the narrower fix it looks like -- CMAKE_CXX_FLAGS lands before the -Wall that target_compile_options adds, which turns the warning back on. The archive's glibc floor improves for free. It needs no more than GLIBC_2.34, where the others need 2.38, so it loads on Raspberry Pi OS bookworm as well as trixie -- which the linux-armv7 archive does not. Its BUILD-INFO.txt says so rather than inheriting the ARMv7 leg's bookworm caveat, which is inverted here. Verified by building: configure reports the same backend and mDNS lines the linux-x86_64 leg expects, the linked binary reads Tag_CPU_arch v6 with VFP registers and hard-float EABI and NEEDED libatomic.so.1, 426/426 ctest cases pass under arm1176, and scripts/smoke_test.sh reports every check passed.
…t its glibc floor The release notes still told every downloader that a Pi Zero, a Pi Zero W and an original Pi had no build and pointed them at a roadmap item that is now closed, which is the widest-audience place the claim appears. Five file counts across release.yml and README.md were left at the pre-armv6 arithmetic, and Getting-Started-on-Linux listed the architectures without armv6 in the line a Pi Zero owner reads first. The one claim the archive shipped that nothing checked was its glibc floor. BUILD-INFO.txt tells a Pi owner the binary loads on Raspberry Pi OS bookworm, and a container digest moved off bookworm would raise the floor silently and leave that text promising something the loader would refuse. The architecture assertion now reads the highest GLIBC_ symbol version off the binary and refuses anything above the 2.36 bookworm carries, and BUILD-INFO.txt states the measured value rather than a written-down one. build_armv6_container.sh was fail-open on its likeliest failure: `docker run` succeeds whether or not the kernel can execute what it started, so with no binfmt handler the container is gone before anything looks at it. It is proven now, where the message can name the cause. `stop` reports rather than refuses when there is nothing to remove, which is what lets build.yml run it with `if: always()` -- the step that makes the script's fourth verb reachable from the file documenting it. Three claims contradicted their own neighbours: build.yml, README.md and ROADMAP.md each said the ARMv7 leg is cross-compiled because "nothing else can build it" within a paragraph of describing the leg that does. The claim is that no runner builds it natively. The pipewire-minimum comment said -Werror was absent on the ARMv6 leg, where the matrix insists every leg spell the key out. ci.yml and get_started_linux.sh both still counted three scripts run on a build.
…riting the glibc floor down `build.yml` uses a real job-level `container:` on the pipewire-minimum job eighty lines below the ARMv6 leg's `matrix.container`, and never said why one starts Docker by hand. The answer is ordering: that key creates the container before the first step runs, and the emulator has to be registered on the host before an ARM container can execute anything, so there would be nowhere to put docker/setup-qemu-action. The script's own claim that YAML could not express `--init`, the uid or QEMU_CPU was overstated -- `container.options:` and `container.env:` take all three. What a workflow file cannot be is somewhere a developer runs, which is the argument that actually holds. Four places still wrote `GLIBC_2.34` down while the assertion added beside them deliberately does not: it bounds the floor at the 2.36 bookworm carries, so a drift to 2.35 would go green and silently make all four false. They now claim what is asserted, and the exact figure survives only where it is measured -- in the BUILD-INFO.txt the archive ships. The rest is rewrapping four comment and prose blocks that were edited in place without being reflowed.
The first CI run came in at 24m56s against a 30-minute timeout, where the plan expected six to nine minutes. The build is 16m59s of it, and it was running three ways parallel on a four-core image: `CMAKE_BUILD_PARALLEL_LEVEL: 3` at the top of this file is a floor, and what sets it is the three-core macOS image rather than anything about Linux. That costs nothing on the legs that compile natively, where the whole build is seconds. On this one every compile is emulated, so it is the difference between a leg with headroom and a leg that fits its timeout by five minutes. `nproc` reads the count off the runner rather than writing one down, so an image with more cores is a faster build instead of an edit nobody remembers to make. The timeout comment claimed the 30 minutes was well clear of the slowest leg, which the measurement did not support. It now says what the margin is really for.
The leg measures 22m49s against the 30 minutes this job allowed, a margin of under a quarter. Hosted-runner variance is routinely that wide, so the first thing to go red would have been the weather rather than a defect -- and a leg that flakes teaches people to re-run it rather than to read it. 40 is the measurement plus room for a slow runner. It costs the other legs nothing, all of which finish inside three minutes; the six-hour default is still the thing being guarded against, and a player that ignores SIGTERM still fails rather than holding a runner all day. The pipewire-minimum job keeps its own 30, being a two-minute native build with no emulation in it.
There was a problem hiding this comment.
🟡 Changes recommended
The architecture fallback can install ARMv7 on older ARM hosts, and the privileged binfmt image remains mutable.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds ARMv6 release support for original Raspberry Pi and Pi Zero hardware.
Changes:
- Builds and tests ARMv6 artifacts in an emulated Raspbian container.
- Publishes and selects the new
linux-armv6archive. - Updates installation, release, and troubleshooting documentation.
File summaries
| File | Description |
|---|---|
scripts/get_started_linux.sh |
Selects ARMv6 releases. |
scripts/build_armv6_container.sh |
Adds containerized ARMv6 builds. |
scripts/build_arm32.sh |
Documents the separate ARMv6 path. |
.github/workflows/build.yml |
Adds the ARMv6 matrix leg. |
.github/workflows/release.yml |
Publishes ARMv6 artifacts. |
.github/workflows/ci.yml |
Updates shell-script CI notes. |
README.md |
Documents ARMv6 CI and releases. |
docs/ROADMAP.md |
Records ARMv6 support as shipped. |
docs/wiki/Installation.md |
Adds ARMv6 installation instructions. |
docs/wiki/Home.md |
Lists ARMv6 support. |
docs/wiki/Troubleshooting.md |
Updates ARM compatibility guidance. |
docs/wiki/Getting-Started-on-Linux.md |
Documents architecture selection. |
docs/wiki/Getting-Started-on-a-Raspberry-Pi.md |
Adds Pi-specific ARMv6 guidance. |
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 5
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…and pin the binfmt image Three things a review of the branch caught. `scripts/get_started_linux.sh` refused `armv5*` and bare `arm` but left an open fallback to `linux-armv7` beneath them, so `armv4l` -- and any other pre-v5 spelling -- would have installed a binary that traps. It is reachable: the getconf branch maps every 32-bit `arm*` kernel to an armhf userland. Every pre-v6 `uname` is named now, and the fallback is left to the machines it is for. Turning SENDSPIN_CLI_WERROR off suppressed every diagnostic on this leg to accommodate one that is wrong. `-Wno-error=restrict` demotes that one and leaves -Werror standing, so an ARMv6-only warning nobody has met yet still fails the leg. The earlier reasoning against a targeted flag held for `-Wno-restrict`, which a later `-Wall` re-enables, and does not hold for `-Wno-error=`: a later blanket -Werror does not re-promote a diagnostic already exempted. Verified on the real toolchain, whose compile line reads `-Wno-error=restrict -O3 -DNDEBUG -Wall -Wextra -Wpedantic -Werror` -- three -Wrestrict warnings, no errors, 426/426 and the smoke test green. With every leg back on -Werror the `werror` matrix key says nothing, so it is gone. docker/setup-qemu-action was pinned by commit while its `image` input defaulted to the mutable `tonistiigi/binfmt:latest`, which it runs `--privileged` to edit the kernel's binfmt handlers. Pinning the action and not that image is pinning the wrong half. It also installed every emulator rather than the one handler anything here uses. Also: the `stop` usage line ended mid-sentence, and a rewrap had eaten four words out of the pipewire-minimum comment.
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Pi Zero, a Pi Zero W and an original Pi are ARM1176, and until now they had no
archive at all —
scripts/get_started_linux.shrefused them by name and the docstold them to build from source. This adds the leg that serves them.
Why it is not the ARMv7 leg with a different
-marchDebian and Ubuntu
armhfare an ARMv7-A port. A cross toolchain's owncrt1.o,crtbegin.oand every member oflibgcc.acome from there, and theyare linked into the binary whatever
-marchsaid — the linker merges buildattributes and reports the maximum, so the result reads
v7regardless. That iswhy
scripts/build_arm32.shrefusesarmv6by name, and that refusal stands:that script cross-compiles against the host distribution's armhf tree, and
nothing here changes what is in it.
Raspbian's really are ARMv6. Its gcc is configured
--with-arch=armv6 --with-float=hard, so this leg passes no-marchat all, and Raspbianpublishes no cross toolchain. So
linux-armv6is not a cross build: it is anordinary native build run inside a digest-pinned Raspbian bookworm container
under
qemu-user. Emulating the whole tree is not the slower of two optionshere — it is the only one, which is the opposite of the trade the ARMv7 leg
makes.
scripts/build_armv6_container.showns the container, for the reasonbuild_arm32.shis a script: this archive is the only build of the project a PiZero owner will ever run, and it has to be reproducible without a runner — which
takes the image digest,
--init, the uid andQEMU_CPU, none of which YAML canhand anybody. It lands under
ci.yml'sshellcheck scripts/*.shgate too.Three things that are load-bearing and are not about ARMv6
QEMU_CPU=arm1176.qemu-armotherwise emulates aCortex-A15-class core and would execute ARMv7 instructions happily — an
emulator more permissive than the hardware proves nothing. It applies at
configure time as well as in the suite, a cmake
try_runprobe asking what theCPU can do being answerable for the wrong CPU otherwise.
--initand a non-root user. Without a reaping PID 1, an exited daemonstays a zombie,
kill -0keeps answering, andsmoke_test.shreports a playerthat outlived
SIGTERM. And threeStateStorecases assert that an unwritabledirectory is refused, which root is refused nothing by. Both failures look like
ARMv6 breakage and are neither.
-latomic. ARMv6 has noLDREXD, so 64-bit atomics becomelibatomic calls. It goes in
CMAKE_CXX_STANDARD_LIBRARIESrather thanCMAKE_EXE_LINKER_FLAGS, which places it ahead of the objects that referenceit and leaves the linker to discard it.
The warning line holds here too, with one diagnostic exempted
gcc 12.2.0 has a
-Wrestrictfalse positive — atsrc/control_common.cpp:391and
:401, as thepipewire-minimumjob already documents, and additionally attests/cli_test.cpp:1133, that third site 32-bit-specific. The leg passes-Wno-error=restrictand keeps-Werror, so an ARMv6-only warning nobody has metyet still fails it.
-Wno-error=is what makes that work and it is not interchangeable with-Wno-restrict.CMAKE_CXX_FLAGSlands before the-Wall -Wextra -Wpedantic -Werrorthattarget_compile_optionsadds, and a later-Wallturns adisabled warning back on — where a later blanket
-Werrordoes not re-promote adiagnostic an earlier
-Wno-error=has already exempted. Verified on the realtoolchain: the compile line reads
-Wno-error=restrict -O3 -DNDEBUG -Wall -Wextra -Wpedantic -Werror, and the build reports three-Wrestrictwarnings and noerrors. Nothing else in our own targets warns.
The glibc floor improves for free
The archive needs no more than
GLIBC_2.34, where the others need2.38. So itloads on Raspberry Pi OS bookworm as well as trixie, which the
linux-armv7archive does not. The architecture-assertion step refuses anything referencing a glibc symbol above
the
2.36bookworm carries, andBUILD-INFO.txtstates the measured floorrather than a written-down one — so the claim the archive ships cannot drift from
the binary. That replaces the ARMv7 leg's bookworm caveat, which is exactly
inverted for a binary built on bookworm. Raising the floor for
linux-x86_64,linux-arm64andlinux-armv7is a separate piece of work and deliberately not attempted here.
Support tier
No OHF project currently ships 32-bit ARM: the MA Local Audio add-on that ships
this binary is
amd64/aarch64only, ESPHome removed armv7 in Feb 2025, andOHF-Voice's C++ wheels are
CIBW_ARCHS: auto64. This archive is offered on thesame footing the
linux-armv7one is — built and exercised under emulation onevery push, never on the hardware — and its
BUILD-INFO.txtsays so in thosewords. It is a convenience build for hardware nobody upstream targets, not a
tier-1 platform.
Verified by running it, not by reasoning
Built in the container against this tree:
null, stdout, alsa, portaudio, pulse, pipewire,dns_sd (/usr/lib/arm-linux-gnueabihf/libdns_sd.so), both dns_sd probesFailedTag_CPU_arch: v6,Tag_ABI_VFP_args: VFP registers,hard-float ABI,NEEDED libatomic.so.1, and tops out atGLIBC_2.34— underthe
GLIBC_2.36bookworm carries, which the leg now asserts rather than assumesQEMU_CPU=arm1176as a non-root userscripts/smoke_test.shreports every check passedDESTDIRcomponent install through the container stages exactly the sixfiles the payload assertion expects
Alongside that: the host suite is 410/410 with a clean
-Werrorbuild,shellcheckis clean on every script, and both ofrelease.yml's asset listswere cross-checked against the matrix's
publish: truelegs and match exactly.Job time — the plan's estimate was wrong by 3x
Both CI runs on this branch are green on every leg. But the plan projected
6–9 minutes for this job from a 4m49s local measurement, and the first run
came in at 24m56s against the 30-minute timeout. Reported here rather than quietly
absorbed, as the plan asked; the timeout was then raised deliberately, below.
-j3)-j$(nproc)= 4, warm cache)The estimate did not fail on the emulation multiplier, which held. It failed on
the hardware: it was taken on an Apple M5 Pro at
-j4, and a hosted runner vCPUis several times slower than an M5 Pro core.
CMAKE_BUILD_PARALLEL_LEVEL: 3at the top ofbuild.ymlis a floor set by thethree-core macOS image, and this leg runs on a four-core one, so the Build
step now reads the count off the runner with
nproc. That bought less than Iexpected — 1m08s on the build, about 7%, not the 25% four-over-three suggests.
The emulated build does not scale cleanly with cores. The change is still right
(a floor set for a different platform should not govern this leg), but it is not
the reason the total came down; the warm FetchContent cache is.
So ~23 minutes is what an emulated ARMv6 build costs, against 1m27s–3m01s for
every other leg. Two things follow, and both are judgement calls rather than
defects:
timeout-minutesis now 40, up from 30. At 30 the margin was under aquarter, and hosted-runner variance is routinely that wide — the first thing to
go red would have been the weather rather than a defect, and a leg that flakes
teaches people to re-run it rather than read it. 40 is the measurement plus room
for a slow runner. It costs the other legs nothing (all finish inside three
minutes), the six-hour default is still what is being guarded against, and the
pipewire-minimumjob keeps its own 30.ci.ymlbuilds every branch.fail-fast: falsemeans it never blocks the other legs' results, and thearchive is the point — but it is worth knowing before merging rather than
after.