Skip to content

Add a native CoreAudio sink and drop PortAudio from the macOS build #23

Add a native CoreAudio sink and drop PortAudio from the macOS build

Add a native CoreAudio sink and drop PortAudio from the macOS build #23

Workflow file for this run

name: Build ARMv6
# The linux-armv6 archive, built natively in an emulated Raspbian container (Debian armhf is ARMv7).
# ~23 minutes, so it runs post-merge, on a path filter and on dispatch rather than every push;
# release.yml calls it and blocks on it.
on:
# release.yml, on a tag.
workflow_call:
# After merge. Branch filters alone never fire for tags.
push:
branches:
- main
# Infrastructure changes only; source-level ARMv6 breaks are caught after merge or via dispatch.
pull_request:
paths:
- .github/workflows/build-armv6.yml
- scripts/build_armv6_container.sh
- CMakeLists.txt
# Opt a branch in; only works once this file is on the default branch.
workflow_dispatch:
# Cancel superseded runs, but never on a tag ref: release.yml calls this and must not be cancelled.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
# Restated: called workflows inherit no defaults, and bash -e without pipefail would let | tee hide a failed configure.
permissions:
contents: read
defaults:
run:
shell: bash
env:
# Floor for any cmake build; build_armv6_container.sh forwards it into the container.
CMAKE_BUILD_PARALLEL_LEVEL: 3
jobs:
build:
# Matches the archive's LEG name, which release.yml diffs.
name: linux-armv6
runs-on: ubuntu-24.04
# 23 minutes measured, plus margin.
timeout-minutes: 40
env:
# Raspbian ARMv6 userland (gcc --with-arch=armv6), pinned by digest; bookworm keeps glibc loadable on trixie too.
IMAGE: 'balenalib/rpi-raspbian:bookworm@sha256:0f3c33faa9d7b1dac778e8afb5854080dcfbddeac7437265598c6076bb0ffc82'
LEG: linux-armv6
steps:
# Actions pinned to commits; the trailing comment is the version.
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Registers qemu-arm in binfmt_misc; image pinned by digest since it runs --privileged.
- name: Register the ARM emulator
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
with:
image: docker.io/tonistiigi/binfmt@sha256:1b804311fe87047a4c96d38b4b3ef6f62fca8cd125265917a9e3dc3c996c39e6 # qemu-v9.2.2
platforms: arm
# One long-lived container, started after binfmt registration and run as a non-root user.
- name: Start the Raspbian build container
run: scripts/build_armv6_container.sh start "$IMAGE"
# Fetched sources and subbuild stamps only, never build outputs; key must stay linux-armv6.
- name: Cache the fetched sources
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.deps/*-src
.deps/*-subbuild
key: deps-linux-armv6-${{ hashFiles('CMakeLists.txt') }}
- name: Configure
env:
DEPS_DIR: ${{ github.workspace }}/.deps
run: |
# Same options as build.yml's legs; the container script owns the ARMv6 specifics.
options=(
-DCMAKE_BUILD_TYPE=Release
-DSENDSPIN_CLI_WERROR=ON
-DSENDSPIN_CLI_WITH_MDNS=ON
-DSENDSPIN_CLI_WITH_PORTAUDIO=ON
-DSENDSPIN_CLI_WITH_PULSE=ON
-DSENDSPIN_CLI_WITH_PIPEWIRE=ON
-DFETCHCONTENT_BASE_DIR="$DEPS_DIR"
)
scripts/build_armv6_container.sh configure build "${options[@]}" 2>&1 | tee configure.log
# Backends are optional, so assert each was found; the mDNS pattern pins the multiarch path.
- name: Assert the configure output found what this leg expects
run: |
patterns=(
'^-- sendspin-cli audio backends: null, stdout, alsa, portaudio, pulse, pipewire$'
'^-- sendspin-cli mDNS: dns_sd \(/usr/lib/arm-linux-gnueabihf/libdns_sd\.so\)$'
# Pinned: these probes fail open.
'^-- Performing Test SENDSPIN_CLI_HAVE_ERR_SERVICE_NOT_RUNNING - Failed$'
'^-- Performing Test SENDSPIN_CLI_HAVE_ERR_TIMEOUT - Failed$'
)
status=0
for expected in "${patterns[@]}"; do
if ! grep -Eq -e "$expected" configure.log; then
echo "::error::configure output has no line matching: $expected"
status=1
fi
done
echo 'What configure actually reported:'
grep -E -- '^-- (sendspin-cli|Performing Test SENDSPIN_CLI)' configure.log ||
echo '(configure.log has no sendspin-cli lines at all)'
exit "$status"
- name: Build
run: |
# Use every runner core; the env value is only a floor.
CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)" \
scripts/build_armv6_container.sh run cmake --build build
# Read ARMv6 and hard-float off the linked binary, before spending an emulated test run.
- name: Assert the binary is built for the architecture this leg claims
run: |
binary=build/sendspin-cli
# Into files first: grep -q on a pipe can SIGPIPE the writer under pipefail.
readelf -h "$binary" >elf-header.txt
readelf -A "$binary" >elf-attributes.txt
echo 'What the linked binary reports:'
file "$binary"
cat elf-header.txt elf-attributes.txt
field() {
sed -n "/^[[:space:]]*$1:/ { s/^[[:space:]]*$1:[[:space:]]*//p; q; }" "$2"
}
status=0
expect() {
[ "$2" = "$3" ] || {
echo "::error::$1 is '$2', not '$3'"
status=1
}
}
expect 'the ELF class' "$(field Class elf-header.txt)" 'ELF32'
expect 'the ELF machine' "$(field Machine elf-header.txt)" 'ARM'
expect 'Tag_CPU_arch' "$(field Tag_CPU_arch elf-attributes.txt)" 'v6'
# Hard-float calling convention, from the attribute rather than the flag.
expect 'Tag_ABI_VFP_args' "$(field Tag_ABI_VFP_args elf-attributes.txt)" 'VFP registers'
# Substring match: the flags line carries unrelated bits.
flags="$(field Flags elf-header.txt)"
case "$flags" in
*'hard-float ABI'*) ;;
*)
echo "::error::the ELF header flags are '$flags', which do not declare the hard-float EABI"
status=1
;;
esac
# The highest glibc symbol must stay within bookworm's 2.36, as BUILD-INFO.txt promises.
bookworm_glibc=GLIBC_2.36
readelf --version-info "$binary" >elf-versions.txt
floor="$(grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' elf-versions.txt | sort -uV | tail -1 || true)"
rm -f elf-versions.txt
if [ -z "$floor" ]; then
echo '::error::no GLIBC_ symbol version could be read off the binary, so the claim BUILD-INFO.txt makes about bookworm cannot be checked'
status=1
else
echo "The highest glibc symbol version this binary references: $floor"
# The higher of the two, compared against the bound: equal is fine, above is not.
if [ "$(printf '%s\n%s\n' "$floor" "$bookworm_glibc" | sort -V | tail -1)" != "$bookworm_glibc" ]; then
echo "::error::this binary references $floor, above the $bookworm_glibc Raspberry Pi OS bookworm carries -- its loader would refuse the archive this job is about to publish"
status=1
fi
fi
rm -f elf-header.txt elf-attributes.txt
exit "$status"
# Serial: flock and filesystem tests are not safe against themselves in parallel.
- name: Test
run: scripts/build_armv6_container.sh run ctest --test-dir build --output-on-failure
# No avahi here, so this exercises the non-fatal advertise-failure path.
- name: Smoke test
run: scripts/build_armv6_container.sh run scripts/smoke_test.sh build/sendspin-cli
# DESTDIR (not --prefix) keeps ExecStart valid; --component keeps it ours; tarred to keep +x.
- name: Package
id: package
env:
# Written verbatim into BUILD-INFO.txt; libatomic1 because ARMv6 needs it.
RUNTIME_PACKAGES: 'libasound2 libportaudio2 libpulse0 libpipewire-0.3-0 libavahi-compat-libdnssd1 libatomic1'
run: |
# Through the container: the binary is ARM, and cmake_install.cmake came from its cmake.
reported="$(scripts/build_armv6_container.sh run ./build/sendspin-cli --version)"
version="$(printf '%s\n' "$reported" | awk 'NR == 1 { print $2 }')"
lib_tag="$(printf '%s\n' "$reported" | awk 'NR == 2 { print $2 }')"
name="sendspin-cli-$version-$LEG"
mkdir -p "stage/$name"
scripts/build_armv6_container.sh run \
env DESTDIR="$PWD/stage/$name" cmake --install build --component sendspin-cli
cat >"stage/$name/BUILD-INFO.txt" <<INFO
sendspin-cli $version for $LEG
Built from commit $GITHUB_SHA on $RUNNER_OS/$RUNNER_ARCH
Linked against sendspin-cpp $lib_tag
Runtime packages this binary needs:
$RUNTIME_PACKAGES
Every path under usr/ here is where the file installs to, so it goes in with one
command from the directory holding the tarball:
sudo tar -xzf $name.tar.gz --strip-components=1 -C / $name/usr
Naming $name/usr is what leaves this file behind rather than unpacking it at /.
Or run it where you unpacked it: ./$name/usr/local/bin/sendspin-cli --help
This build is configured for the /usr/local prefix, which is why those paths are
what they are.
INFO
cat >>"stage/$name/BUILD-INFO.txt" <<'LINUX'
The systemd unit is at usr/local/lib/systemd/system/sendspin-cli.service, which is
on systemd's own search path, so installing as above needs no copying. The one
command a tarball cannot run for itself creates the account the unit runs as,
declared beside it in usr/local/lib/sysusers.d/sendspin-cli.conf:
sudo systemd-sysusers
sudo systemctl daemon-reload
sudo systemctl enable --now sendspin-cli
Skip the first line and the unit will not start: systemctl reports 217/USER.
It runs the player as an unprivileged sendspin-cli user, in the foreground, with its
state in /var/lib/sendspin-cli and its control socket in /run/sendspin-cli. An
existing /var/lib/sendspin-cli left behind by an earlier root-run version needs
nothing done to it -- systemd chowns it. Configure it in
/etc/sendspin-cli.conf; there is an annotated example beside the installed README,
under usr/local/share/doc/sendspin-cli. Its ExecStart names /usr/local/bin/sendspin-cli
absolutely, so a binary put anywhere else needs the unit changed to match.
LINUX
# Explains the build-host line, and reads the glibc floor off the binary.
glibc_floor="$(readelf --version-info build/sendspin-cli |
grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' | sort -uV | tail -1)"
cat >>"stage/$name/BUILD-INFO.txt" <<EMULATED
This was built for an ARM1176 -- a Pi Zero, a Pi Zero W or an original Pi -- inside a
Raspbian container on the runner named above, and not on a Pi. It is a native build
rather than a cross build: Raspbian's own gcc, libgcc and startup objects are ARMv6,
which is what makes this archive ARMv6, and the build attributes of the binary are
asserted to say ARMv6, hard-float EABI before the archive is made. Its unit suite and
smoke test ran there under qemu-user pinned to an ARM1176, so the emulator was no more
permissive than your hardware -- but what is proven is that the code builds and
behaves, not that it was tried on a Pi.
The runtime packages above are spelled the way Raspberry Pi OS bookworm spells them;
on trixie they are libasound2t64 and libpipewire-0.3-0t64. This binary needs no glibc
symbol newer than ${glibc_floor#GLIBC_}, which is below what either release carries.
So it loads on both -- which the linux-armv7 archive does not.
EMULATED
tar -czf "$name.tar.gz" -C stage "$name"
echo "name=$name" >>"$GITHUB_OUTPUT"
# Assert the exact payload; keep in step with build.yml's list of the same.
- name: Assert the payload holds exactly what it should
env:
NAME: ${{ steps.package.outputs.name }}
run: |
expected=$(mktemp)
{
echo BUILD-INFO.txt
echo usr/local/bin/sendspin-cli
echo usr/local/lib/sysusers.d/sendspin-cli.conf
echo usr/local/lib/systemd/system/sendspin-cli.service
echo usr/local/share/doc/sendspin-cli/LICENSE
echo usr/local/share/doc/sendspin-cli/README.md
echo usr/local/share/doc/sendspin-cli/contributors.md
echo usr/local/share/doc/sendspin-cli/sendspin-cli.conf.example
} | sort >"$expected"
# `! -type d` so symlinks count.
actual=$(mktemp)
(cd "stage/$NAME" && find . ! -type d | sed 's|^\./||') | sort >"$actual"
echo 'What the payload holds:'
cat "$actual"
diff -u "$expected" "$actual" || {
echo '::error::the staged payload is not the file list this workflow expects'
exit 1
}
# Again from the shipped tarball, with the executable bit; directory entries dropped first.
tar -tzf "$NAME.tar.gz" | grep -v '/$' | sed -n "s|^$NAME/||p" | sort >"$actual"
diff -u "$expected" "$actual" || {
echo '::error::the tarball does not hold what the staged payload does'
exit 1
}
[ -x "stage/$NAME/usr/local/bin/sendspin-cli" ] || {
echo '::error::the staged sendspin-cli is not executable'
exit 1
}
# The unit is not run here: MemoryDenyWriteExecute= forbids qemu-user's JIT.
- name: Upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.package.outputs.name }}
path: ${{ steps.package.outputs.name }}.tar.gz
# Long enough to bisect; releases take their own copy.
retention-days: 14
if-no-files-found: error
# always(), so a failed run still removes its container.
- name: Stop the build container
if: always()
run: scripts/build_armv6_container.sh stop