Skip to content

Add a cross-compiled linux-armv7 leg to the CI matrix - #47

Merged
chrisuthe merged 3 commits into
mainfrom
chrisuthe/task/cross-compile-armv6-and-armv7-release-legs-into
Aug 30, 2026
Merged

Add a cross-compiled linux-armv7 leg to the CI matrix#47
chrisuthe merged 3 commits into
mainfrom
chrisuthe/task/cross-compile-armv6-and-armv7-release-legs-into

Conversation

@chrisuthe

@chrisuthe chrisuthe commented Aug 30, 2026

Copy link
Copy Markdown
Member

This adds a linux-armv7 publishing leg so a Pi 2, Pi 3, Pi 4 or Pi Zero 2 running a 32-bit Raspberry Pi OS has something to install

32-bit Raspberry Pi hardware had no release archive: `get_started_linux.sh`
refused every 32-bit ARM userland and told the operator to reinstall a 64-bit
OS. This adds a `linux-armv7` publishing leg so a Pi 2, Pi 3, Pi 4 or Pi Zero 2
running a 32-bit Raspberry Pi OS has something to install, and so the tree is
proven to still build for that target on every push.

It is cross-compiled because nothing else can reach the target: GitHub offers no
armv7 runner, and its arm64 runners are Neoverse N1 with no AArch32 at EL0.
`scripts/build_arm32.sh` owns the cross configure -- a script rather than lines
of YAML, following the precedent build.yml states for `build_macos_pkg.sh`, so
it is reproducible without a runner and gated by ci.yml's shellcheck job. The
suite is run rather than skipped: `qemu-user-static` through binfmt executes the
ctest binaries and `smoke_test.sh` on the host, so the leg keeps the rule every
other leg holds.

The target flags go in `CMAKE_C_FLAGS`/`CMAKE_CXX_FLAGS` globally rather than on
our own targets, because every dependency arrives through FetchContent in the
one cmake invocation and a per-target flag would leave them compiling for the
host default. A new step reads the linked binary's merged ELF build attributes
back and fails the leg unless they say ARMv7, hard-float EABI -- the linker
merges attributes across every object in the link, so that one read covers the
whole dependency tree.

Two steps gated on `runner.os == 'Linux'` rather than on a matrix key would have
fired against a cross-built binary, so the matrix gains a `systemd:` key, spelled
out on every leg beside `publish` and `avahi`, and an `arm32:` key naming the
target. The arm32 leg installs the `:armhf` build dependencies instead of the
runner's own, which is what leaves `find_library` nothing of the wrong
architecture to answer with; its `expect_mdns` is anchored on the
`arm-linux-gnueabihf` path so a host-tree `libdns_sd` fails the leg rather than
satisfying the pattern the native legs use.

ARMv6 is deliberately not here. Debian and Ubuntu armhf are an ARMv7-A port, and
that is where the toolchain's own `crt1.o`, `crtbegin.o` and `libgcc.a` come
from -- all ARMv7, all linked in -- so compiling our sources `-march=armv6` does
not yield an ARMv6 archive, and the merged `Tag_CPU_arch` says so. It needs a
Raspberry Pi OS sysroot. `build_arm32.sh` refuses `armv6` rather than building
something that would trap on a Pi Zero, `get_started_linux.sh` refuses an
`armv6l` host, and ROADMAP item 12 records what it would take.

Downstream: both `for leg in ...` lists in release.yml and its notes prose, the
installer's architecture mapping, README, ROADMAP item 12, four wiki pages and a
Troubleshooting entry for the two ways a Pi install can fail to start.

The archives need glibc 2.38 and so will not start on Raspberry Pi OS bookworm
(2.36); they do on trixie (2.41), which is what Raspberry Pi OS ships. That floor
is not introduced here -- the existing linux-x86_64 and linux-arm64 archives are
built on the same image and carry it too -- but this is the leg that aims a Pi
audience at it, so BUILD-INFO.txt now states it where that reader will meet it.

Verified in containers: cross configure finds all six backends and resolves mDNS
to the armhf path with both dns_sd probes Failed; the build is clean under
-Werror on gcc 13; ctest is 426/426 and smoke_test.sh fully green under qemu;
Package and the payload assertion pass unchanged, unit and sysusers fragment
included. The new gates were checked in both directions -- the ELF assertion
rejects an armv8 build, a soft-float build and a native host binary, and the
anchored expect_mdns refuses a configure that resolved libdns_sd from the host
tree. The native branch still builds and passes 426/426.
@chrisuthe
chrisuthe requested a balanced review from Copilot August 30, 2026 17:00
@chrisuthe chrisuthe linked an issue Aug 30, 2026 that may be closed by this pull request

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Architecture detection can select incompatible binaries, and the ARMv7 build requires CPU extensions not guaranteed by advertised hosts.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a publishable 32-bit ARM build for supported Raspberry Pi systems.

Changes:

  • Adds ARMv7 cross-compilation, emulated testing, and ELF validation.
  • Publishes the new archive and updates installation guidance.
  • Documents ARMv6 and older-glibc limitations.
File summaries
File Description
.github/workflows/build.yml Adds the ARMv7 CI leg.
.github/workflows/ci.yml Updates script execution notes.
.github/workflows/release.yml Publishes the ARMv7 archive.
scripts/build_arm32.sh Configures ARM cross-builds.
scripts/get_started_linux.sh Selects the ARMv7 archive.
packaging/sendspin-cli.service.in Clarifies native syscall ABI handling.
README.md Documents the expanded matrix and releases.
docs/ROADMAP.md Records ARMv7 delivery and remaining ARMv6 work.
docs/wiki/Home.md Advertises ARMv7 availability.
docs/wiki/Installation.md Adds ARMv7 installation instructions.
docs/wiki/Getting-Started-on-Linux.md Expands Linux prerequisites.
docs/wiki/Getting-Started-on-a-Raspberry-Pi.md Documents Pi architecture selection.
docs/wiki/Troubleshooting.md Adds ARM and glibc troubleshooting.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/wiki/Installation.md Outdated
Comment thread scripts/build_arm32.sh Outdated
Comment thread scripts/get_started_linux.sh Outdated
Comment thread docs/wiki/Getting-Started-on-a-Raspberry-Pi.md Outdated
Two ways the armv7 leg could hand somebody a binary their machine cannot run.

`uname -m` names the kernel, not the userland, and on a Raspberry Pi the two
routinely disagree: `arm_64bit` defaults to 1 on a Pi 4, a Pi 400 and a CM4, so a
32-bit armhf install on one of those boots a 64-bit kernel and reports `aarch64`
with no 64-bit library on the disk. `get_started_linux.sh` chose on that answer
and installed `linux-arm64`, whose loader is not there. It now asks the userland
-- `dpkg --print-architecture`, falling back to `getconf LONG_BIT` paired with
the kernel's family -- and keeps `uname -m` only for the question the userland
cannot answer: an ARMv6 board cannot run a 64-bit kernel, so `armv6l` there is
the CPU speaking. A soft-float `armel` userland is refused rather than handed a
hard-float archive, and an unknown architecture is refused rather than falling
through to x86-64.

The build targeted the Pi's FPU rather than the ABI's. `armv7l` guarantees
neither NEON nor VFPv4 -- a Cortex-A8 or A9 is ARMv7-A with VFPv3 and optional
NEON -- while the installer maps every ARMv7 host to this archive and the docs
advertise generic Linux `armv7`. `-mfpu` is now the armhf baseline of
`vfpv3-d16`, so the archive runs on what its name promises. The decoders are
fixed-point, so the baseline costs nothing on the path that matters.

Verified: the selection was driven through eleven host shapes, including the Pi 4
32-bit-userland-on-64-bit-kernel case that was broken, armel, i386 and s390x. The
rebuilt binary reports Tag_CPU_arch v7, Tag_FP_arch VFPv3-D16 and no
Tag_Advanced_SIMD_arch at all, still passes the leg's ELF assertion, and is
426/426 on ctest with the smoke test green under qemu.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two architecture-selection paths can still install binaries that the target userland or CPU cannot execute.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread docs/wiki/Installation.md
Comment thread scripts/get_started_linux.sh
…ppet

Two more ways to reach an archive the machine cannot run.

The `getconf` fallback passed `$MACHINE` through when it recognised neither the
width nor the kernel, which is the mistake the whole block exists to prevent made
a second time: a 32-bit x86 userland under an x86-64 kernel reports `x86_64`, and
passing that on as the userland hands it the 64-bit archive. It now leaves the
userland unset and refuses, so what cannot be positively identified is never
guessed at -- which closes the class rather than the one instance of it.

The manual-install snippet in the wiki carried its ARMv6 caveat as a trailing
comment, and a comment does not stop a Pi Zero downloading the ARMv7 archive and
meeting an illegal instruction. It now runs the same `uname -m` guard the script
does, for the same reason the script keeps one: an ARMv6 board reports `armhf`
like any other 32-bit ARM machine, and cannot run a 64-bit kernel, so `armv6l`
there is the CPU speaking.

Verified: the selection was driven through thirteen host shapes, the new x86 case
and the ten from the previous commit included, with no regression; and the doc
snippet was extracted from the page itself and run against six, refusing the Pi
Zero and choosing correctly for the rest.
@chrisuthe
chrisuthe marked this pull request as ready for review August 30, 2026 19:52
@chrisuthe
chrisuthe merged commit 57e7819 into main Aug 30, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Also build for armv6 ?

2 participants