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
314 changes: 290 additions & 24 deletions .github/workflows/build.yml

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ 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 both scripts being run -- smoke_test.sh on every publishing leg,
# 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 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.
shellcheck:
name: shellcheck
runs-on: ubuntu-24.04
Expand Down
19 changes: 12 additions & 7 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 macos-arm64; do
for leg in linux-x86_64 linux-arm64 linux-armv7 macos-arm64; do
echo "sendspin-cli-$VERSION-$leg.tar.gz"
done
echo "sendspin-cli-$VERSION-macos-arm64.pkg"
Expand Down Expand Up @@ -204,9 +204,14 @@ jobs:
# Heredoc quoted, so the only interpolation is the one done explicitly below: this
# text is full of backticks and $ that belong to the reader, not to the shell.
cat >notes.md <<'NOTES'
Binaries for Linux (x86-64 and arm64) and macOS (Apple silicon), plus a macOS
installer `.pkg`, built and published by `.github/workflows/release.yml` from the tag
alone.
Binaries for Linux (x86-64, arm64 and 32-bit ARM) and macOS (Apple silicon), plus a
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.

## Install

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

## Checksums

`SHA256SUMS` covers the four files above. It does not cover the `Source code`
`SHA256SUMS` covers the five 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 @@ -279,7 +284,7 @@ jobs:
```

Run that from the directory you downloaded into. `--ignore-missing` is there because
`SHA256SUMS` lists all four and you have almost certainly taken one: without it, the
`SHA256SUMS` lists all five 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 @@ -382,7 +387,7 @@ jobs:
expected=$(mktemp)
{
echo SHA256SUMS
for leg in linux-x86_64 linux-arm64 macos-arm64; do
for leg in linux-x86_64 linux-arm64 linux-armv7 macos-arm64; do
echo "sendspin-cli-$VERSION-$leg.tar.gz"
done
echo "sendspin-cli-$VERSION-macos-arm64.pkg"
Expand Down
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1326,13 +1326,21 @@ 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`, plus a fourth leg 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.
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
`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.
`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 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.
Expand Down Expand Up @@ -1362,9 +1370,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 three
Pushing a `vMAJOR.MINOR.PATCH` tag builds the same matrix and publishes the four
platform archives and the macOS installer `.pkg`, plus a `SHA256SUMS` covering all
four, as a GitHub Release. The workflow triggers on `v*` but refuses anything else
five, 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 @@ -1382,9 +1390,9 @@ sha256sum --ignore-missing -c SHA256SUMS # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS # macOS
```

`--ignore-missing` because `SHA256SUMS` lists all four and you have almost
`--ignore-missing` because `SHA256SUMS` lists all five 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 four things built
exits non-zero on a file that is fine. Those checksums cover the five 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
41 changes: 31 additions & 10 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@ operator to choose between `Type=simple` and `Type=forking` and write the unit t
publishing nothing* — `Upload` is `if: matrix.publish`, and `if-no-files-found: error`
catches an empty upload rather than an absent one, while `download-artifact` matching nothing
is an empty directory rather than an error. So the release job names the assets it expects and
diffs the set, in the shape the payload assertions already use — three tarballs and, since the
diffs the set, in the shape the payload assertions already use — four tarballs and, since the
third slice below, the macOS `.pkg`. Only the set: each
archive's own file list was diffed twice inside the build, and re-opening them here would be
the duplication this split exists to avoid.
Expand Down Expand Up @@ -1215,10 +1215,10 @@ operator to choose between `Type=simple` and `Type=forking` and write the unit t
- **`SHA256SUMS` is generated from inside the directory** so its entries are bare filenames:
`sha256sum -c` resolves paths relative to the working directory, and a file naming `dist/…`
is one a downloader cannot use without knowing that. The notes give the macOS spelling
(`shasum -a 256 -c`) beside the Linux one, two of the four assets being macOS-only, and
(`shasum -a 256 -c`) beside the Linux one, two of the five assets being macOS-only, and
say out loud that the checksums do not cover the `Source code` archives GitHub attaches on
its own. Both are given with `--ignore-missing`, which is not a detail: `SHA256SUMS` lists
all four and a reader has almost certainly taken one, so the bare form reports the
all five and a reader has almost certainly taken one, so the bare form reports the
rest as failures and exits non-zero on a file that is perfectly good. Supported by GNU
coreutils and by macOS's Perl `shasum` alike, both checked rather than assumed.
- **The notes are written by hand, not `--generate-notes`.** On a first tag that emits every
Expand Down Expand Up @@ -1493,18 +1493,19 @@ 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; sink contract still owed)*
### 12. CI and tests — *shipped (matrix and smoke test; ARMv6 and 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`, plus a fourth `ubuntu-24.04` leg configured
`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:
`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 three platform legs additionally run the smoke
on a null-sink-only, mDNS-less binary. The four 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 @@ -1557,11 +1558,31 @@ macOS. A leg that configures `-DCMAKE_CXX_FLAGS=-fsanitize=thread` and runs
reasoning into a check. It is cheap: the smoke test already drives the whole boot, socket and
shutdown path, and produced zero reports when run that way by hand.

The matrix also has no armv7 or 32-bit Pi leg, and 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 describe is gone: item 10 shipped the `install()` rules, and the archive is now a
**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
`-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.

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
describe is gone: item 10 shipped the `install()` rules, and the archive is now a
`DESTDIR`-staged `cmake --install` payload whose file list this workflow asserts — plus, on the
Linux legs, a `systemd-analyze verify` of the unit it installs.
legs that run on the architecture they built for, a `systemd-analyze verify` of the unit it
installs.

### 13. `PlayerRoleConfig` wiring — *shipped*

Expand Down
10 changes: 7 additions & 3 deletions docs/wiki/Getting-Started-on-Linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +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 64-bit Linux host (`x86_64` or `arm64`), systemd, a sound card, and root.
**You need:** a Linux host (`x86_64`, `arm64`, or `armv7` on a 32-bit Raspberry Pi OS),
systemd, a sound card, and root.

## The short way

Expand Down Expand Up @@ -38,8 +39,11 @@ instead of the newest.

### What it actually does

1. **Checks the architecture.** `x86_64` and `aarch64` have builds. 32-bit ARM does not, and
is refused with the reason and the fix rather than an "unsupported" shrug.
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.
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
60 changes: 33 additions & 27 deletions docs/wiki/Getting-Started-on-a-Raspberry-Pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,51 @@

A Pi makes an excellent Sendspin endpoint: it is quiet, it is cheap, and one per room is the
whole point of a synchronized multi-room protocol. Installing on one is
[Getting Started on Linux](Getting-Started-on-Linux) — a Pi is an arm64 Linux box and takes
the same `linux-arm64` archive an arm64 server does — plus the five things on this page.
[Getting Started on Linux](Getting-Started-on-Linux) — a Pi is an ordinary Linux box and takes
whichever archive its architecture names — plus the five things on this page.

## 1. You need a 64-bit OS. This is not negotiable
## 1. Check which build your OS wants

**The builds are `arm64` only. There is no 32-bit ARM build, and none is coming from CI.**
The matrix has no armv7 or 32-bit Pi leg, which is recorded in
[`docs/ROADMAP.md`](https://github.com/Sendspin/sendspin-cpp-cli/blob/main/docs/ROADMAP.md),
item 12.

Check what you are running before anything else:
Two archives serve a Pi, and it is the **userland** that picks between them — not
`uname -m`, which names the kernel:

```console
$ uname -m
aarch64
$ dpkg --print-architecture
arm64
```

| It says | Where you are |
| It says | What installs |
|---|---|
| `aarch64` | Good. Carry on. |
| `armv7l`, `armv6l` | A 32-bit userland. No archive will install. |
| `arm64` | `linux-arm64`, the 64-bit build. |
| `armhf` | `linux-armv7`, the 32-bit build — unless the board is ARMv6; see below. |

**Do not use `uname -m` for this.** `arm_64bit` defaults to on for a Pi 4, a Pi 400 and a
CM4, so a **32-bit** Raspberry Pi OS install on one of those boots a 64-bit kernel and
reports `aarch64` — while every library on the disk is armhf. Choosing on that answer
installs the arm64 archive, whose loader is not there, and the binary fails with a "No such
file or directory" naming a file that plainly exists.

`uname -m` is still the right question for one thing, because ARMv6 boards cannot run a
64-bit kernel at all: if it says `armv6l`, that is the CPU speaking, and there is no build.

A 32-bit answer on 64-bit hardware is the common case — Raspberry Pi OS shipped a 32-bit
userland by default for years, and plenty of installed cards still run it. The fix is to
reimage with **Raspberry Pi OS (64-bit)**, or *Raspberry Pi OS Lite (64-bit)* for a headless
player, which is what a Sendspin endpoint wants anyway. In Raspberry Pi Imager the 64-bit
builds are under **Raspberry Pi OS (other)**.
Either archive gets you a working player, and the getting-started script chooses for you. A
64-bit OS is still the better answer on hardware that can run one: `linux-arm64` is built and
run on a real arm64 machine, systemd unit and all, where `linux-armv7` is cross-compiled and
its suite run under emulation. In Raspberry Pi Imager the 64-bit builds are under **Raspberry
Pi OS (other)**.

| Model | 64-bit capable |
|---|---|
| Pi 5, Pi 4, Pi 400, Pi 3, Pi Zero 2 W, CM3/CM4/CM5 | Yes |
| Pi 1, Pi Zero, Pi Zero W, and Pi 2 boards before v1.2 | **No** — build from source, or use other hardware |

If your board is not on either row, do not go looking it up: install a 64-bit image and run
`uname -m`. That answer is the only one that decides anything here.

The getting-started script refuses a 32-bit userland with this whole answer rather than an
"unsupported architecture", because it is the single most common way a Pi install goes
wrong.
| Pi 1, Pi Zero, Pi Zero W, and Pi 2 boards before v1.2 | **No** |

**`armv6l` has no build.** A Pi Zero, a Pi Zero W and an original Pi are ARMv6, and the
32-bit archive is compiled for ARMv7 — its instructions are illegal on those cores, so there
is nothing to install and the getting-started script says so rather than handing you a binary
that traps.
[`docs/ROADMAP.md`](https://github.com/Sendspin/sendspin-cpp-cli/blob/main/docs/ROADMAP.md)
item 12 records what an ARMv6 leg would take. Building from source on the Pi itself works in
the meantime.

## 2. Install

Expand Down
9 changes: 5 additions & 4 deletions docs/wiki/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ $ sendspin-cli vol 40

| Platform | Architecture | How |
|---|---|---|
| Linux | `x86_64`, `arm64` | Release tarball, or `scripts/get_started_linux.sh` |
| Linux | `x86_64`, `arm64`, `armv7` | Release tarball, or `scripts/get_started_linux.sh` |
| macOS | Apple silicon (`arm64`) | Release tarball or installer `.pkg` |
| Raspberry Pi | `arm64` only — **a 64-bit OS is required** | The Linux tarball, same as any arm64 host |
| Raspberry Pi | `arm64`, or `armv7` on a 32-bit OS | The Linux tarball, same as any other Linux host |

The macOS builds are made on the `macos-14` CI runner and declare no minimum OS version;
what the installer `.pkg` does check is the architecture, read off the binary with `lipo` at
build time, so it turns an Intel Mac away rather than reporting success.

There is no 32-bit ARM build and no Intel-Mac build. The CI matrix has no armv7, 32-bit Pi
or macOS `x86_64` leg, which is recorded in
There is no ARMv6 build and no Intel-Mac build. ARMv6 — a Pi Zero, a Pi Zero W, an original
Pi — needs a Raspberry Pi OS sysroot the matrix does not have, and there is no macOS `x86_64`
leg; both are recorded in
[`docs/ROADMAP.md`](https://github.com/Sendspin/sendspin-cpp-cli/blob/main/docs/ROADMAP.md),
item 12.
Anything else builds from source.
Expand Down
Loading
Loading