#!/bin/bash # Bundle system binaries (ffmpeg, ffprobe, vgmstream-cli, fluidsynth) # into resources/bin/ along with their non-glibc shared library # dependencies, then set RPATH=$ORIGIN so each binary loads its # siblings from its own directory at runtime. # # Without this, a build host with a different ffmpeg ABI than the user # (e.g. Ubuntu 22.04 ffmpeg 4.x → libav*.so.58, Fedora 44 / Arch # ffmpeg 7.x → libav*.so.62) ships a binary the user can't load. # # Linux-only. macOS bundling runs dylibbundler inline in the CI workflow # because it's a different dynamic-linker story (Mach-O load paths + # codesign invalidation). Windows downloads pinned zip archives inline. set -euo pipefail if [ "$(uname -s)" != "Linux" ]; then echo "bundle-binaries.sh is Linux-only. macOS/Windows bundling runs inline in CI." >&2 exit 1 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" BIN_DIR="$PROJECT_DIR/resources/bin" mkdir -p "$BIN_DIR" echo "=== Bundling system binaries ===" # patchelf is required to set RPATH=$ORIGIN on bundled binaries and # libs. Without that, the runtime linker on the user's machine falls # back to /usr/lib and fails when the host ABI doesn't match the build # host's. Fail here rather than letting fluidsynth's section fail later. if ! command -v patchelf >/dev/null 2>&1; then echo "ERROR: patchelf not found on PATH (apt: patchelf) - required to set RPATH on bundled binaries." >&2 exit 1 fi # is_skipped_lib() — the glibc/loader skip list, shared verbatim with # build-common.sh's audit so the two never drift. source "$SCRIPT_DIR/bundled-lib-skiplist.sh" # Copy every non-glibc shared library that the given binary links to # into resources/bin/. The final patchelf sweep (below) sets RPATH on # every binary and every .so so the directory becomes a single # self-contained load tree. bundle_with_deps() { local bin_path="$1" # ldd exits non-zero (and prints "not a dynamic executable") on # statically linked binaries like the vgmstream-cli GitHub release. # Trap that and treat it as "no deps to bundle" rather than letting # pipefail kill the build. local ldd_out if ! ldd_out=$(ldd "$bin_path" 2>/dev/null); then return 0 fi echo "$ldd_out" | awk '/=>/ {print $3}' | while read -r lib; do [ -n "$lib" ] && [ -f "$lib" ] || continue if is_skipped_lib "$(basename "$lib")"; then continue fi # -L follows symlinks; -n avoids re-copying a lib already # contributed by an earlier binary (every bundled binary on # the same build host links the same /usr/lib versions, so # first-wins is safe). Errors are NOT suppressed: -n makes the # already-bundled case a no-op exit 0, so any non-zero status # here is a real copy failure that must fail the build. cp -Ln "$lib" "$BIN_DIR/" done } # ffmpeg - used for WAV → OGG transcoding on GP5 imports. # verify_bundled_binaries treats resources/bin/ffmpeg as required and # will hard-fail later if it's missing — fail here with the actual cause # rather than letting that downstream check produce a less-direct error. if command -v ffmpeg >/dev/null 2>&1; then FFMPEG_SRC="$(command -v ffmpeg)" cp "$FFMPEG_SRC" "$BIN_DIR/" echo " ffmpeg: $(ls -lh "$BIN_DIR/ffmpeg" | awk '{print $5}')" else echo "ERROR: ffmpeg not found on PATH; resources/bin/ffmpeg is required for the bundled build (apt: ffmpeg / brew: ffmpeg)." >&2 exit 1 fi # Sloppak conversion encodes .ogg with -c:a libvorbis. Fail the build now # rather than ship an ffmpeg that produces "Unknown encoder 'libvorbis'" # at runtime on user machines. The lib/sloppak_convert.py fallback to # the built-in `vorbis -strict experimental` encoder is a safety net for # unbundled installs, not a license to ship a libvorbis-less binary. # Capture the encoder list into a variable BEFORE grepping. Piping ffmpeg # straight into `grep -wq` is racy under `set -o pipefail`: grep -q exits on the # first match and closes the pipe, ffmpeg then takes SIGPIPE (141) mid-write, and # pipefail makes the whole pipeline return 141 instead of grep's 0 — so the guard # intermittently "fails" even though libvorbis is present (reproducible on Docker # Desktop). Capturing first removes the pipe from ffmpeg entirely. FFMPEG_ENCODERS="$("$FFMPEG_SRC" -hide_banner -encoders 2>/dev/null || true)" if ! printf '%s\n' "$FFMPEG_ENCODERS" | grep -wq libvorbis; then echo "ERROR: bundled ffmpeg lacks libvorbis encoder. Sloppak conversion would fall back to the lower-quality built-in vorbis encoder on user machines." >&2 echo "Install an ffmpeg built with --enable-libvorbis (apt's ffmpeg ships it by default; check your distro's package if this fails)." >&2 exit 1 fi bundle_with_deps "$BIN_DIR/ffmpeg" # ffprobe - demucs's audio loader spawns ffprobe before ffmpeg to read # stream metadata; falling through to a host-installed ffprobe (or none # at all) means the desktop bundle behaves differently on each user's # machine. Ship the build host's ffprobe alongside ffmpeg so the bundle # is self-contained on every platform. apt's ffmpeg package includes # ffprobe, so this is universally available where ffmpeg already is. if command -v ffprobe >/dev/null 2>&1; then cp "$(which ffprobe)" "$BIN_DIR/" echo " ffprobe: $(ls -lh "$BIN_DIR/ffprobe" | awk '{print $5}')" else echo "ERROR: ffprobe not found on PATH; resources/bin/ffprobe is required so demucs can read stream metadata in stem-splitting (apt: ffmpeg / brew: ffmpeg)." >&2 exit 1 fi bundle_with_deps "$BIN_DIR/ffprobe" # vgmstream-cli - used for WEM → WAV decoding. # Download from GitHub releases if not in PATH (CI does this inline). # verify_bundled_binaries downstream treats this as required and will # hard-fail if it ends up missing — fail here with the actual cause # instead. if command -v vgmstream-cli >/dev/null 2>&1; then cp "$(which vgmstream-cli)" "$BIN_DIR/" echo " vgmstream-cli: $(ls -lh "$BIN_DIR/vgmstream-cli" | awk '{print $5}')" else for tool in curl unzip; do if ! command -v "$tool" >/dev/null 2>&1; then echo "ERROR: $tool not on PATH; required to download/extract vgmstream-cli." >&2 exit 1 fi done echo "Downloading vgmstream-cli from GitHub releases..." VGM_ASSET="vgmstream-linux.zip" if ! curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors \ "https://github.com/vgmstream/vgmstream/releases/latest/download/${VGM_ASSET}" \ -o /tmp/vgmstream.zip; then echo "ERROR: failed to download vgmstream-cli zip from upstream releases." >&2 exit 1 fi if ! unzip -q /tmp/vgmstream.zip -d /tmp/vgmstream; then echo "ERROR: failed to extract /tmp/vgmstream.zip — upstream archive may be malformed." >&2 exit 1 fi VGM_BIN=$(find /tmp/vgmstream -maxdepth 2 -name 'vgmstream-cli' -type f | head -1) if [ -z "$VGM_BIN" ]; then echo "ERROR: vgmstream-cli binary not found in downloaded archive — upstream zip layout may have changed." >&2 exit 1 fi cp "$VGM_BIN" "$BIN_DIR/vgmstream-cli" chmod +x "$BIN_DIR/vgmstream-cli" echo " vgmstream-cli: $(ls -lh "$BIN_DIR/vgmstream-cli" | awk '{print $5}') (downloaded)" rm -rf /tmp/vgmstream /tmp/vgmstream.zip fi bundle_with_deps "$BIN_DIR/vgmstream-cli" # fluidsynth - used for MIDI → WAV in GP5 imports. if command -v fluidsynth >/dev/null 2>&1; then cp "$(which fluidsynth)" "$BIN_DIR/fluidsynth" echo " fluidsynth: $(ls -lh "$BIN_DIR/fluidsynth" | awk '{print $5}')" else echo "ERROR: fluidsynth not found on PATH - it is required to bundle GP5 import support. Install fluidsynth and rerun this script (apt: fluidsynth)." >&2 exit 1 fi bundle_with_deps "$BIN_DIR/fluidsynth" # Final patchelf sweep: every dynamic binary gets RPATH=$ORIGIN, and # every dynamic .so does too so transitive deps also load from # resources/bin/. Done once at the end so the order in which binaries # contribute their libs doesn't matter. # # Be strict about patchelf success on dynamic binaries. The downstream # audit only checks lib *presence*, not RPATH — so a silent patchelf # failure here would ship a binary whose loader falls back to /usr/lib # at runtime, exactly the issue #68 regression. Detect static-vs-dynamic # explicitly via NEEDED entries in the .dynamic section and only skip # patchelf on the static case (e.g. the vgmstream-cli GitHub release). for bin in ffmpeg ffprobe vgmstream-cli fluidsynth; do bin_path="$BIN_DIR/$bin" if readelf -d "$bin_path" 2>/dev/null | grep -q '(NEEDED)'; then patchelf --set-rpath '$ORIGIN' "$bin_path" else echo " $bin: statically linked, RPATH not applicable" fi done for so in "$BIN_DIR"/*.so*; do [ -f "$so" ] || continue if readelf -d "$so" 2>/dev/null | grep -q '(NEEDED)'; then patchelf --set-rpath '$ORIGIN' "$so" fi done echo " Total resources/bin/: $(du -sh "$BIN_DIR" | cut -f1)" echo "=== Binary bundle complete ==="