diff --git a/app/deploy/linux/libva-probe.c b/app/deploy/linux/libva-probe.c new file mode 100644 index 00000000..64dea7e9 --- /dev/null +++ b/app/deploy/linux/libva-probe.c @@ -0,0 +1,35 @@ +/* + * libva-probe: loadability oracle for the AppImage AppRun. + * + * Moonlight and the bundled FFmpeg import libva through symbols carrying + * VA_API_* ELF version nodes. Whether the libva installed on the host can + * satisfy those requirements cannot be inferred portably from file names or + * loader caches, so this program embeds the same DT_NEEDED entries and + * versioned symbol references as those binaries without ever calling into + * them: either the dynamic loader resolves everything and main() runs (the + * host libva is usable), or the exec itself fails and AppRun must fall back + * to the build-environment libva staged next to this binary. + * + * scripts/build-appimage.sh verifies at build time that this file references + * every VA_API_* node required by the binaries shipped in the AppImage, so + * it stays automatically in sync with the libva usage there. + */ + +#include + +#include +#include + +/* Address references are enough: they produce relocations against the + * versioned symbols, which is what makes the loader check the host's + * VA_API_* version nodes. The functions must never actually be called. + * volatile keeps the table (and its relocations) alive through -O2. */ +static void *volatile requirements[] = { + (void *)vaCreateSurfaces, /* libva.so.2, VA_API_0.33.0 */ + (void *)vaGetDisplay, /* libva-x11.so.2 */ +}; + +int main(void) +{ + return requirements[0] == NULL; +} diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index c7d9c53f..a65e1c7e 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -62,11 +62,80 @@ popd export QML_SOURCES_PATHS=$SOURCE_ROOT/app/gui export QMAKE=qmake6 +# Stage the build-environment libva in opt/libva-fallback (outside usr/, so linuxdeploy +# does not scan it, and outside every loader search path), together with a small probe +# binary that carries the same libva ELF requirements as Moonlight. The AppRun below +# uses the probe to decide, via the dynamic loader itself, whether the host libva can +# satisfy Moonlight; when it cannot, the fallback copy is made visible. opt/ is a +# standard linuxdeploy location for application data that must not be processed. +LIBVA_FALLBACK_DIR=$DEPLOY_FOLDER/opt/libva-fallback +SYSTEM_LIBVA=$(ldconfig -p 2>/dev/null | awk '/libva\.so\.2/{print $NF; exit}') +mkdir -p $LIBVA_FALLBACK_DIR +if [ -n "$SYSTEM_LIBVA" ]; then + cp -a "$(dirname "$SYSTEM_LIBVA")"/libva*.so* $LIBVA_FALLBACK_DIR/ || fail "Unable to stage libva fallback copy!" + + echo Compiling libva-probe + cc -O2 -L"$(dirname "$SYSTEM_LIBVA")" -Wl,--no-as-needed -o $LIBVA_FALLBACK_DIR/libva-probe \ + $SOURCE_ROOT/app/deploy/linux/libva-probe.c -lva -lva-x11 || fail "Unable to compile libva-probe!" + + # Keep the probe honest: it must reference every VA_API_* version node that the + # binaries shipped in the AppImage require, otherwise a host libva could pass the + # probe and still fail to load Moonlight or the bundled FFmpeg. + va_nodes() { LC_ALL=C readelf -V "$1" 2>/dev/null | grep -oE 'VA_API_[0-9]+\.[0-9]+\.[0-9]+' | sort -u; } + NEEDED_NODES=$(for b in $DEPLOY_FOLDER/usr/bin/moonlight \ + /usr/local/lib*/libav*.so* /usr/local/lib*/libsw*.so* \ + /usr/lib/x86_64-linux-gnu/libav*.so* /usr/lib/x86_64-linux-gnu/libsw*.so*; do + [ -f "$b" ] && va_nodes "$b"; done | sort -u) + PROBE_NODES=$(va_nodes $LIBVA_FALLBACK_DIR/libva-probe) + [ -z "$(comm -13 <(echo "$PROBE_NODES") <(echo "$NEEDED_NODES"))" ] || \ + fail "libva-probe is missing version node(s): $(comm -13 <(echo "$PROBE_NODES") <(echo "$NEEDED_NODES")) - update app/deploy/linux/libva-probe.c!" +fi + +APP_RUN=$BUILD_ROOT/AppRun-libva +cat > $APP_RUN <<'APPRUN_EOF' +#!/bin/bash +# AppRun: prefer the host libva; use the staged copy only if the host cannot run us. +# +# VA-API driver modules on the host are loaded by libva and export an entrypoint +# named after the libva version they were built against (__vaDriverInit_1_XX). +# A libva can only load drivers that are not newer than itself, so bundling our +# own (older) libva silently breaks hardware decoding on up-to-date distros. +# Distros keep host libva and host drivers in step, so whenever the host libva +# can satisfy Moonlight's own ELF version requirements it is the right choice. +# +# "Can satisfy" is answered here by libva-probe, a tiny program linked against +# the same versioned libva symbols as Moonlight and the bundled FFmpeg: if the +# dynamic loader can start it, the host libva works; if not (no libva installed, +# or one too old to link), we make the staged build-environment copy visible +# via LD_LIBRARY_PATH, matching the pre-existing bundled behavior. +APPDIR="${APPDIR:-$(dirname "$(dirname "$(readlink -f "$0")")")}" +LIBVA_FALLBACK="$APPDIR/opt/libva-fallback" + +if [ -d "$LIBVA_FALLBACK" ] && ! "$LIBVA_FALLBACK/libva-probe" 2>/dev/null; then + export LD_LIBRARY_PATH="$LIBVA_FALLBACK${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +fi + +exec "$APPDIR/usr/bin/moonlight" "$@" +APPRUN_EOF +chmod +x $APP_RUN + echo Creating AppImage pushd $INSTALLER_FOLDER +# Don't bundle libva: the bundled build-environment version (jammy: VA-API 1.20/1.22) +# cannot dlopen GPU drivers compiled against newer libva on the host (which export +# __vaDriverInit_1_23+), so va_openDriver() always fails and VAAPI falls back to +# software decoding. The host always provides libva on systems where VA-API is +# usable, so link against it at runtime instead (the AppRun shim above keeps a +# bundled last-resort copy for hosts without libva). VERSION=$VERSION $LINUXDEPLOY --appdir $DEPLOY_FOLDER \ --library=/usr/local/lib/libSDL3.so.0 \ - --plugin qt --output appimage || fail "linuxdeploy failed!" + --plugin qt \ + --custom-apprun $APP_RUN \ + --exclude-library=libva.so* \ + --exclude-library=libva-drm.so* \ + --exclude-library=libva-wayland.so* \ + --exclude-library=libva-x11.so* \ + --output appimage || fail "linuxdeploy failed!" popd echo Build successful \ No newline at end of file