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
316 changes: 278 additions & 38 deletions .github/workflows/build.yml

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ jobs:
# Stays here rather than moving into build.yml with the rest, and the asymmetry is worth being
# honest about now that build_macos_pkg.sh shapes a released artifact: what this job adds is
# *lint*, and a tag push runs release.yml alone, so it is lint that a tag does not get. What a
# tag does get is three of them being run -- smoke_test.sh on every publishing leg,
# build_arm32.sh on the 32-bit ARM one, and build_macos_pkg.sh on the macOS one, whose .pkg is
# then installed and its receipt asserted -- so a release is gated on the scripts working
# whether or not it is gated on their style.
# tag does get is four of them being run -- smoke_test.sh on every publishing leg,
# build_arm32.sh on the cross-compiled 32-bit ARM one, build_armv6_container.sh on the
# containerised one, and build_macos_pkg.sh on the macOS one, whose .pkg is then installed and
# its receipt asserted -- so a release is gated on the scripts working whether or not it is
# gated on their style.
shellcheck:
name: shellcheck
runs-on: ubuntu-24.04
Expand Down
20 changes: 12 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ jobs:
# `…-macos-arm64-installer` and merge-multiple lays its one file out flat here.
expected=$(mktemp)
{
for leg in linux-x86_64 linux-arm64 linux-armv7 macos-arm64; do
for leg in linux-x86_64 linux-arm64 linux-armv7 linux-armv6 macos-arm64; do
echo "sendspin-cli-$VERSION-$leg.tar.gz"
done
echo "sendspin-cli-$VERSION-macos-arm64.pkg"
Expand Down Expand Up @@ -208,10 +208,14 @@ jobs:
macOS installer `.pkg`, built and published by `.github/workflows/release.yml` from
the tag alone.

`linux-armv7` is the 32-bit Raspberry Pi build — a Pi 2, a Pi 3, a Pi 4 or a Pi
Zero 2 running a 32-bit Raspberry Pi OS. It is cross-compiled, and its unit suite and
smoke test are run under emulation rather than on a Pi. A Pi Zero, a Pi Zero W and an
original Pi are ARMv6 and this archive is not for them; see the roadmap below.
`linux-armv7` and `linux-armv6` are the two 32-bit Raspberry Pi builds, and which one
a board takes is a question about its CPU. `linux-armv7` is for a Pi 2, a Pi 3, a Pi 4
or a Pi Zero 2 running a 32-bit Raspberry Pi OS; `linux-armv6` is for a Pi Zero, a Pi
Zero W or an original Pi, which are ARM1176 and would meet an illegal instruction in
the other. `scripts/get_started_linux.sh` picks between them for you. Neither is built
on a Pi — the unit suite and smoke test of both are run under emulation on the runner
that built them — and `linux-armv6` is the only archive here that loads on Raspberry
Pi OS bookworm as well as trixie.

## Install

Expand Down Expand Up @@ -274,7 +278,7 @@ jobs:

## Checksums

`SHA256SUMS` covers the five files above. It does not cover the `Source code`
`SHA256SUMS` covers the six files above. It does not cover the `Source code`
archives GitHub attaches on its own, which are generated by GitHub rather than built
here.

Expand All @@ -284,7 +288,7 @@ jobs:
```

Run that from the directory you downloaded into. `--ignore-missing` is there because
`SHA256SUMS` lists all five and you have almost certainly taken one: without it, the
`SHA256SUMS` lists all six and you have almost certainly taken one: without it, the
ones you did not download are reported as failures and the command exits non-zero on a
perfectly good file.

Expand Down Expand Up @@ -387,7 +391,7 @@ jobs:
expected=$(mktemp)
{
echo SHA256SUMS
for leg in linux-x86_64 linux-arm64 linux-armv7 macos-arm64; do
for leg in linux-x86_64 linux-arm64 linux-armv7 linux-armv6 macos-arm64; do
echo "sendspin-cli-$VERSION-$leg.tar.gz"
done
echo "sendspin-cli-$VERSION-macos-arm64.pkg"
Expand Down
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1326,22 +1326,34 @@ path. CI runs it on every platform leg; run it yourself against any build.
## CI

Every branch push and pull request builds on `ubuntu-24.04`, `ubuntu-24.04-arm`
and `macos-14`, a fourth leg cross-compiled for 32-bit ARM on `ubuntu-24.04`, and a
fifth configured `-DSENDSPIN_CLI_WITH_MDNS=OFF` — which compiles
and `macos-14`, a fourth leg cross-compiled for ARMv7 on `ubuntu-24.04`, a fifth
built for ARMv6 inside an emulated Raspbian container on the same runner, and a
sixth configured `-DSENDSPIN_CLI_WITH_MDNS=OFF` — which compiles
`src/mdns_null.cpp` in place of `src/mdns_dnssd.cpp`, so that configuration is
built rather than assumed. Every leg builds with `-DSENDSPIN_CLI_WERROR=ON` and
runs the unit suite, and each asserts from its own configure output that it found
the backends it expects: a missing `-dev` package does not fail a configure, so
without that check the matrix would happily go green on a deaf, undiscoverable
binary.

The 32-bit ARM leg is cross-compiled because nothing else can build it: GitHub has
no armv7 runner, and its arm64 runners cannot execute 32-bit ARM at all.
The ARMv7 leg is cross-compiled because no runner can build it natively: GitHub
has no armv7 runner, and its arm64 runners cannot execute 32-bit ARM at all.
`scripts/build_arm32.sh` owns the cross configure, the suite and the smoke test run
under `qemu-user`, and the linked binary's own ELF build attributes are asserted to
say ARMv7, hard-float EABI before an archive is made — which is what catches a
dependency quietly compiled for something else.

The ARMv6 leg cannot be that, and the difference is the toolchain rather than the
runner: Debian and Ubuntu `armhf` are an ARMv7-A port, so a cross toolchain's own
`crt1.o` and `libgcc.a` are ARMv7 and end up in the binary whatever `-march` said.
Raspbian's are genuinely ARMv6, so that leg runs an ordinary native build inside a
digest-pinned Raspbian container under `qemu-user` — with its suite and smoke test
under `QEMU_CPU=arm1176`, so the emulator is no more permissive than an ARM1176 —
and asserts ARMv6, hard-float EABI off the finished binary the same way. It holds
the warning line with one exemption: Raspbian's gcc 12 has the same `-Wrestrict`
false positive the `pipewire-minimum` job documents, so that leg passes
`-Wno-error=restrict` and leaves `-Werror` standing over everything else.

The matrix lives in `.github/workflows/build.yml`, which both `ci.yml` and
`release.yml` call, so a release is built and gated exactly the way a push is.
`ci.yml` ignores tags for that reason — otherwise a tag would build twice.
Expand Down Expand Up @@ -1370,9 +1382,9 @@ kept for 14 days. For something that does not expire, take a

## Releases

Pushing a `vMAJOR.MINOR.PATCH` tag builds the same matrix and publishes the four
Pushing a `vMAJOR.MINOR.PATCH` tag builds the same matrix and publishes the five
platform archives and the macOS installer `.pkg`, plus a `SHA256SUMS` covering all
five, as a GitHub Release. The workflow triggers on `v*` but refuses anything else
six, as a GitHub Release. The workflow triggers on `v*` but refuses anything else
that matches — a prerelease like `v0.2.0-rc1` is rejected rather than quietly
published as the latest release, until somebody decides what it should mean.
Nothing else publishes, and the workflow never creates a tag: a release exists
Expand All @@ -1390,9 +1402,9 @@ sha256sum --ignore-missing -c SHA256SUMS # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS # macOS
```

`--ignore-missing` because `SHA256SUMS` lists all five and you have almost
`--ignore-missing` because `SHA256SUMS` lists all six and you have almost
certainly taken one; without it the rest are reported as failures and the command
exits non-zero on a file that is fine. Those checksums cover the five things built
exits non-zero on a file that is fine. Those checksums cover the six things built
here, not the `Source code` archives GitHub attaches on its own. Neither the macOS
binary nor the `.pkg` around it is signed — see below. A Developer ID signature and
notarization are still owed, tracked in [`docs/ROADMAP.md`](docs/ROADMAP.md) item 10.
Expand Down
84 changes: 63 additions & 21 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -1494,19 +1494,20 @@ unproven is what the paragraph above already says only a real tag can answer.

Optional, later. Upstream's `examples/tui_client` shows the shape.

### 12. CI and tests — *shipped (matrix and smoke test; ARMv6 and the sink contract still owed)*
### 12. CI and tests — *shipped (matrix and smoke test; the sink contract still owed)*

`.github/workflows/ci.yml` builds and tests every branch push and pull request on
`ubuntu-24.04`, `ubuntu-24.04-arm` and `macos-14`, a fourth cross-compiled for 32-bit ARM on
`ubuntu-24.04`, and a fifth configured
`-DSENDSPIN_CLI_WITH_MDNS=OFF` — which compiles `src/mdns_null.cpp` instead of
`src/mdns_dnssd.cpp`, so that translation unit is built rather than assumed. Every leg
configures `-DSENDSPIN_CLI_WERROR=ON` and runs the CTest suite, the no-mDNS leg included:
`ubuntu-24.04`, `ubuntu-24.04-arm` and `macos-14`, a fourth cross-compiled for ARMv7 on
`ubuntu-24.04`, a fifth built natively for ARMv6 inside an emulated Raspbian container on that
same runner, and a sixth configured `-DSENDSPIN_CLI_WITH_MDNS=OFF` — which compiles
`src/mdns_null.cpp` instead of `src/mdns_dnssd.cpp`, so that translation unit is built rather
than assumed. Every leg configures `-DSENDSPIN_CLI_WERROR=ON` and runs the CTest suite, the
no-mDNS leg included:
`discovery_test.cpp` links whichever `MdnsService` went in, and that configuration has no
other coverage. Each leg also asserts against its own configure output that it found the
backends it expects — a missing `-dev` package does not fail a configure, since every
backend is optional and auto-detected, so without that assertion the matrix would go green
on a null-sink-only, mDNS-less binary. The four platform legs additionally run the smoke
on a null-sink-only, mDNS-less binary. The five platform legs additionally run the smoke
test and upload the binary they built, kept 14 days.

The matrix itself lives in `.github/workflows/build.yml`, called by `ci.yml` and by item 10's
Expand Down Expand Up @@ -1562,21 +1563,62 @@ shutdown path, and produced zero reports when run that way by hand.
**The 32-bit Pi leg is there.** `linux-armv7` cross-compiles on `ubuntu-24.04` against the
armhf multiarch tree — `scripts/build_arm32.sh` owns the configure — and runs the whole unit
suite and `scripts/smoke_test.sh` under `qemu-user` rather than skipping them, so it holds the
same rule every other leg does. A cross build is the only shape available: GitHub has no armv7
runner, and its arm64 runners are Neoverse N1 with no AArch32 at EL0, so no `runs-on` value
reaches the target at all. What keeps the leg's name honest is a `readelf` of the linked
binary: the linker merges build attributes across every object in the link and reports the
highest, so one read covers the FetchContent tree as well as our own sources, and a dependency
compiled for the wrong architecture fails the leg instead of shipping.

**ARMv6 is still owed, and it is not the same job.** A Pi Zero, a Pi Zero W and an original Pi
are ARM1176 cores, and Debian and Ubuntu armhf are an ARMv7-A port — which is where a cross
toolchain's own `crt1.o`, `crtbegin.o` and `libgcc.a` come from. Compiling our sources
same rule every other leg does. Nothing builds it natively: GitHub has no armv7 runner, and its
arm64 runners are Neoverse N1 with no AArch32 at EL0, so no `runs-on` value reaches the target
at all — and a cross build is what buys back everything the ARMv6 leg below has to emulate.
What keeps the leg's name honest is a `readelf` of the linked binary: the linker merges build
attributes across every object in the link and reports the highest, so one read covers the
FetchContent tree as well as our own sources, and a dependency compiled for the wrong
architecture fails the leg instead of shipping.

**The ARMv6 leg is there as well, and it is not the same job.** A Pi Zero, a Pi Zero W and an
original Pi are ARM1176 cores, and Debian and Ubuntu armhf are an ARMv7-A port — which is where
a cross toolchain's own `crt1.o`, `crtbegin.o` and `libgcc.a` come from. Compiling our sources
`-march=armv6` therefore does not produce an ARMv6 archive: the startup and helper objects
linked in beside them are ARMv7, and the merged `Tag_CPU_arch` says so. It needs a Raspberry Pi
OS armhf sysroot, whose libgcc and startup objects really are ARMv6, and that sysroot is the
work. `scripts/build_arm32.sh` refuses `armv6` outright rather than building something that
would trap, and `scripts/get_started_linux.sh` refuses an `armv6l` host for the same reason.
linked in beside them are ARMv7, and the merged `Tag_CPU_arch` says so. What an ARMv6 build
needs is an ARMv6 `libgcc` and ARMv6 startup objects, which is a *distribution* rather than a
flag — and Raspbian is one. Its gcc is configured `--with-arch=armv6 --with-float=hard`, and
every one of `libgcc.a`'s members reads `Tag_CPU_arch: v6`, so the leg passes no `-march` at
all.

So `linux-armv6` is not a cross build at all: it is an ordinary native build run inside a
digest-pinned Raspbian bookworm container under `qemu-user`, which is why it is the one leg
`scripts/build_arm32.sh` has nothing to do with. That script still refuses `armv6`, and its
reasoning is untouched — it cross-compiles against the *host* distribution's armhf tree, which
is still ARMv7. `scripts/get_started_linux.sh` now maps an `armv6l` host onto the new archive
instead of refusing it; ARMv5 and a bare `arm` are still refused, the ARMv6 archive being the
oldest one built.

Three things about that leg are load-bearing and none of them are about ARMv6 the instruction
set. Everything it runs in the container runs under `QEMU_CPU=arm1176`, because `qemu-arm`
defaults to a Cortex-A15-class core and would happily execute the ARMv7 instructions the
hardware cannot — an emulator more permissive than the target proves nothing. That covers the
configure as well as the suite, a CMake `try_run` probe asking what the CPU can do being
answerable for the wrong CPU otherwise. The container runs `--init` and builds as a non-root
user, because without a reaping PID 1 the smoke test reads an exited daemon's zombie as a player
that outlived `SIGTERM`, and because three `StateStore` cases assert that an unwritable
directory is refused, which root is refused nothing by. And the link needs `-latomic`: ARMv6 has
no `LDREXD`, so the 64-bit atomics in `src/pulse_sink.cpp`, `src/player_listener.cpp` and
sendspin-cpp's own `connection_manager.cpp` become libatomic calls. It goes in
`CMAKE_CXX_STANDARD_LIBRARIES` rather than `CMAKE_EXE_LINKER_FLAGS`, which places it ahead of
the objects that need it and leaves the linker to discard it.

That leg holds the warning line like the rest, with one diagnostic exempted: Raspbian's gcc
12.2.0 has a `-Wrestrict` false positive on `line += " " + std::to_string(...)`, at
`src/control_common.cpp:391` and `:401` and at `tests/cli_test.cpp:1133`. It passes
`-Wno-error=restrict`, which is not interchangeable with `-Wno-restrict`: `CMAKE_CXX_FLAGS`
lands before the `-Wall` `target_compile_options` adds, and a later `-Wall` turns a *disabled*
warning back on — where a later blanket `-Werror` does not re-promote a diagnostic an earlier
`-Wno-error=` has already exempted. So an ARMv6-only warning nobody has met yet still fails the
leg.

One thing improves for free. The published archives need `GLIBC_2.38`, which Raspberry Pi OS
bookworm's 2.36 refuses at load; the ARMv6 binary references nothing newer than bookworm's own
2.36, so it loads on bookworm and trixie alike. That is asserted off the finished binary rather
than assumed, and its `BUILD-INFO.txt` names the version it actually came out at. The bookworm caveat the
ARMv7 archive's `BUILD-INFO.txt` carries therefore does not apply to this one, and it does not
say it. Raising the floor for `linux-x86_64`, `linux-arm64` and `linux-armv7` is a separate
piece of work and still owed.

There is no macOS x86_64 leg. Artifacts are per-commit workflow artifacts only; the tagged
release that does not expire is item 10's, and shipped. The hand-rolled tar this entry used to
Expand Down
11 changes: 6 additions & 5 deletions docs/wiki/Getting-Started-on-Linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ From nothing to a player your Sendspin server can find. On a Raspberry Pi, read
[Getting Started on a Raspberry Pi](Getting-Started-on-a-Raspberry-Pi) instead — it is this
page plus the handful of things a Pi does differently.

**You need:** a Linux host (`x86_64`, `arm64`, or `armv7` on a 32-bit Raspberry Pi OS),
systemd, a sound card, and root.
**You need:** a Linux host (`x86_64`, `arm64`, or `armv7`/`armv6` on a 32-bit Raspberry Pi
OS), systemd, a sound card, and root.

## The short way

Expand Down Expand Up @@ -41,9 +41,10 @@ instead of the newest.

1. **Checks the architecture** — the userland's, read from `dpkg --print-architecture`
rather than from `uname -m`, which names the kernel and disagrees with the userland on a
32-bit Raspberry Pi OS. `amd64`, `arm64` and `armhf` have builds. ARMv6 — a Pi Zero, a Pi
Zero W, an original Pi — does not, and is refused with the reason rather than an
"unsupported" shrug.
32-bit Raspberry Pi OS. `amd64`, `arm64` and `armhf` have builds. `armhf` then splits on
`uname -m`, which is the one question the userland cannot answer: `armv6l` — a Pi Zero, a
Pi Zero W, an original Pi — takes `linux-armv6`, and everything else `linux-armv7`. ARMv5
and older is refused with the reason rather than an "unsupported" shrug.
2. **Finds the newest release** and downloads that archive plus `SHA256SUMS`.
3. **Verifies the checksum**, and stops without installing anything if it does not match.
4. **Unpacks it into `/`** with the member-selected `tar` form, so `BUILD-INFO.txt` stays in
Expand Down
Loading
Loading