Add a native CoreAudio sink and drop PortAudio from the macOS build #26
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
| 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 |