#!/usr/bin/env bash # Build on an x86-64 Linux Docker host; writes the archive to the current directory. set -euo pipefail out="${1:-$PWD/alice-spotify-runtime.tar.gz}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT cat > "$work/build.sh" <<'INNER' #!/bin/bash set -euo pipefail export DEBIAN_FRONTEND=noninteractive dpkg --add-architecture arm64 apt-get update -qq apt-get install -y -qq --no-install-recommends curl ca-certificates binutils python3 > /dev/null mkdir -p /work/debs/partial /work/root /work/out/lib /work/out/bin /work/out/etc # An empty dpkg status prevents installed amd64 packages from satisfying arm64 dependencies. apt-get -y --download-only --no-install-recommends -o APT::Architecture=arm64 -o Dir::State::status=/dev/null -o Dir::Cache::archives=/work/debs install pulseaudio:arm64 pulseaudio-utils:arm64 libstdc++6:arm64 > /work/apt.log for f in /work/debs/*.deb; do dpkg-deb -x "$f" /work/root; done curl -fL --retry 3 https://soloist-builds.spotifycdn.com/soloist_release_arm64.tar.gz -o /work/soloist.tar.gz mkdir /work/soloist tar -xzf /work/soloist.tar.gz -C /work/soloist find /work/soloist -type f -maxdepth 4 -print cp /work/root/usr/bin/pulseaudio /work/root/usr/bin/pacat /work/root/usr/bin/pactl /work/out/bin/ cp -a /work/soloist/. /work/out/soloist/ # Follow DT_NEEDED edges, including module dependencies and PulseAudio's private libs. python3 - <<'PY' import os, subprocess, shutil, glob root='/work/root'; out='/work/out'; paths={} for base, dirs, files in os.walk(root): for f in files: p=os.path.join(base,f) if '.so' in f and os.path.isfile(p): paths.setdefault(f,p) seeds=[out+'/bin/pulseaudio',out+'/bin/pacat',out+'/bin/pactl'] # The wake helper's bundled TensorFlow Lite C library loads libstdc++ at runtime. cxx=out+'/lib/libstdc++.so.6' shutil.copy2(paths['libstdc++.so.6'],cxx,follow_symlinks=True) seeds.append(cxx) for base,dirs,files in os.walk(out+'/soloist'): for f in files: p=os.path.join(base,f) if open(p,'rb').read(4)==b'\x7fELF': seeds.append(p) mods=glob.glob(root+'/usr/lib/aarch64-linux-gnu/pulseaudio/modules/module-{alsa-sink,native-protocol-unix}.so') # glob does not expand braces. mods=[glob.glob(root+'/usr/lib/**/module-'+n+'.so',recursive=True)[0] for n in ('alsa-sink','native-protocol-unix','remap-sink')] for p in mods: shutil.copy2(p,out+'/lib/'); seeds.append(out+'/lib/'+os.path.basename(p)) seen=set(); missing=set() while seeds: p=seeds.pop() try: s=subprocess.check_output(['readelf','-d',p],stderr=subprocess.DEVNULL,text=True) except subprocess.CalledProcessError: continue for line in s.splitlines(): if '(NEEDED)' not in line: continue name=line.split('[')[1].split(']')[0] if name in seen: continue seen.add(name) if name not in paths: missing.add(name); continue src=paths[name]; dest=out+'/lib/'+name shutil.copy2(src,dest,follow_symlinks=True); seeds.append(dest) if missing: raise SystemExit('Missing ELF dependencies: '+', '.join(sorted(missing))) loader=glob.glob(root+'/lib/ld-linux-aarch64.so.1')+glob.glob(root+'/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1') shutil.copy2(loader[0],out+'/lib/ld-linux-aarch64.so.1',follow_symlinks=True) PY # ALSA configuration is data loaded at runtime, not an ELF dependency. cp -a /work/root/usr/share/alsa /work/out/alsa cat > /work/out/etc/default.pa <<'PA' load-module module-native-protocol-unix auth-anonymous=1 load-module module-alsa-sink sink_name=station device=hw:0,0 load-module module-remap-sink sink_name=music master=station set-default-sink music PA cat > /work/out/etc/daemon.conf <<'PA' daemonize = no exit-idle-time = -1 flat-volumes = no PA cat > /work/out/run <<'RUN' #!/bin/sh set -eu base=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) export ALSA_CONFIG_PATH="$base/alsa/alsa.conf" export ALSA_CONFIG_DIR="$base/alsa" export PULSE_RUNTIME_PATH="${PULSE_RUNTIME_PATH:-/data/local/tmp/alice-pulse}" export PULSE_CONFIG_PATH="$base/etc" mkdir -p "$PULSE_RUNTIME_PATH" exec "$base/lib/ld-linux-aarch64.so.1" --library-path "$base/lib" "$@" RUN chmod +x /work/out/run (cd /work/out && find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS) for f in /work/debs/*.deb; do dpkg-deb -f "$f" Package Version Architecture; printf "\n"; done > /work/out/DEBIAN-PACKAGES.txt (cd /work/out && sha256sum DEBIAN-PACKAGES.txt >> SHA256SUMS) sha256sum /work/soloist.tar.gz > /work/out/SOLOIST-ARCHIVE-SHA256.txt (cd /work/out && sha256sum SOLOIST-ARCHIVE-SHA256.txt >> SHA256SUMS) tar -czf /output/runtime.tar.gz -C /work/out . du -sh /work/out /output/runtime.tar.gz INNER chmod +x "$work/build.sh" docker run --rm --platform linux/amd64 -v "$work:/output" debian:trixie-slim bash /output/build.sh cp "$work/runtime.tar.gz" "$out" printf 'Built %s\n' "$out"