From c2c6699c51ad7a004d42386226c49943e8260849 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 26 Jul 2026 00:17:12 +0200 Subject: [PATCH] Don't bundle alsa lib --- README.md | 2 +- data/apprun.sh | 11 +++++++++++ src/appimagedeploy.h | 2 +- src/dependencywalker.cpp | 8 ++++++++ src/dependencywalker.h | 3 +++ src/miscdeploy.cpp | 14 +++----------- src/miscdeploy.h | 2 +- 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8f7e1ee..a8a164f 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The AppDir can either be produced by a single application's own build (e.g. Stra | GStreamer 1.0 | `libgstreamer-1.0.so` | The gstreamer-1.0 plugin directory (filtered - see [GStreamer plugin selection](#gstreamer-plugin-selection)) plus `gst-plugin-scanner`, plus `libsoup-3.0.so.0` if `libgstsoup.so` is present (it's `dlopen()`'d, not linked, so the ordinary dependency walk misses it) | | Gdk-Pixbuf | `libgdk_pixbuf` | Pixbuf loaders + a patched `loaders.cache` | | GTK 2/3/4 | `libgtk-<2\|3\|4>` | The matching GTK module directory, default theme (GTK ≤ 3), and a patched `immodules.cache` (GTK ≤ 3) | -| ALSA | `libasound` | The `alsa-lib` plugin directory | +| ALSA | `libasound` | **Never bundled** - `libasound.so*` is removed from the dependency set so it always resolves against the running system's alsa-lib (plugins, config, and hardware UCM profiles are versioned together and bundling them breaks device access on a mismatched host) | | PulseAudio | `libpulse` | The PulseAudio plugin directory | | GIO | `libgio-2.0` | `gio/modules/` + `giomodule.cache`, exposed via `GIO_EXTRA_MODULES` (see [GIO modules and the GnuTLS trust store](#gio-modules-and-the-gnutls-trust-store)) | | GLib schemas | `usr/share/glib-2.0/schemas` present in the AppDir | Compiles them with `glib-compile-schemas` if not already compiled | diff --git a/data/apprun.sh b/data/apprun.sh index a37001b..77101b9 100644 --- a/data/apprun.sh +++ b/data/apprun.sh @@ -69,6 +69,16 @@ if [ -e "${ROOT}/etc/crypto-policies/back-ends/gnutls.config" ]; then fi +# ALSA (libasound.so.2) is deliberately never bundled (see MiscDeploy::HandleAlsa): its plugin modules, config, and hardware UCM profiles are versioned together by the running distro, and bundling a build-time snapshot instead breaks device access on a mismatched host. +# But the bundled ld-linux has had "/lib", "/usr", and "/etc" patched out of its own compiled-in strings (see DeployInterpreter) so it can no longer fall back to the default system library directories or /etc/ld.so.cache to find anything not explicitly rpath'd within the AppDir. +# Preloading the host's copy by its exact absolute path satisfies that one SONAME directly for the whole process, without reopening host-library search (and the ABI mismatches that guards against) for anything else that *is* bundled. +for ALSA_LIB in /usr/lib/x86_64-linux-gnu/libasound.so.2 /usr/lib/aarch64-linux-gnu/libasound.so.2 /usr/lib64/libasound.so.2 /usr/lib/libasound.so.2 /lib/x86_64-linux-gnu/libasound.so.2; do + if [ -e "${ALSA_LIB}" ]; then + export LD_PRELOAD="${LD_PRELOAD:+${LD_PRELOAD}:}${ALSA_LIB}" + break + fi +done + # Set GStreamer paths GST_PLUGIN_CORE_ELEMENTS="$(find "${ROOT}" -name "libgstcoreelements.so" -type f 2>/dev/null | head -1)" @@ -109,6 +119,7 @@ echo "GDK_PIXBUF_MODULEDIR: ${GDK_PIXBUF_MODULEDIR}" echo "GDK_PIXBUF_MODULE_FILE: ${GDK_PIXBUF_MODULE_FILE}" echo "SSL_CERT_FILE: ${SSL_CERT_FILE}" echo "GNUTLS_SYSTEM_PRIORITY_FILE: ${GNUTLS_SYSTEM_PRIORITY_FILE}" +echo "LD_PRELOAD: ${LD_PRELOAD}" echo "GST_PLUGIN_PATH: ${GST_PLUGIN_PATH}" echo "GST_PLUGIN_SYSTEM_PATH: ${GST_PLUGIN_SYSTEM_PATH}" echo "GST_PLUGIN_SCANNER: ${GST_PLUGIN_SCANNER}" diff --git a/src/appimagedeploy.h b/src/appimagedeploy.h index 77e1a47..3374ce3 100644 --- a/src/appimagedeploy.h +++ b/src/appimagedeploy.h @@ -27,7 +27,7 @@ #include "gstreamerdeploy.h" #include "qtdeploy.h" -// Top-level orchestration for the appimagedeploy tool: turns a prefix/ directory into a self-contained AppDir by discovering every ELF dependency, bundling desktop-integration support files (Gdk, Gtk, GStreamer, ALSA, PulseAudio, GLib schemas, fontconfig), Qt plugins/QML, the ELF interpreter, writing AppRun, copying and rpath-patching every dependency, and copying in copyright files. +// Top-level orchestration for the appimagedeploy tool: turns a prefix/ directory into a self-contained AppDir by discovering every ELF dependency, bundling desktop-integration support files (Gdk, Gtk, GStreamer, PulseAudio, GLib schemas, fontconfig), Qt plugins/QML, the ELF interpreter, writing AppRun, copying and rpath-patching every dependency, and copying in copyright files. ALSA is deliberately excluded rather than bundled (see MiscDeploy::HandleAlsa). namespace AppImageDeploy { // `desktop_file_path` is expected at /usr/share/applications/foo.desktop, as accepted by AppDir::Create(). `gstreamer_plugin_names` is only consulted when `gstreamer_plugin_set` is GStreamerDeploy::PluginSet::List, and `qt_sql_plugin_names` only when `qt_sql_plugin_set` is QtDeploy::SqlPluginSet::List. Returns false and sets *error_message on any unrecoverable failure. diff --git a/src/dependencywalker.cpp b/src/dependencywalker.cpp index 36fc56d..c58719f 100644 --- a/src/dependencywalker.cpp +++ b/src/dependencywalker.cpp @@ -136,6 +136,14 @@ QString DependencyWalker::FindLibrary(const QString &filename) { } +void DependencyWalker::RemoveElfsByFilenamePrefix(const QString &filename_prefix) { + + all_elfs_.removeIf([&filename_prefix](const QString &path) { + return QFileInfo(path).fileName().startsWith(filename_prefix); + }); + +} + QStringList DependencyWalker::FindWithPrefixInLibraryLocations(const QString &prefix) { EnsureDefaultLibraryLocations(); diff --git a/src/dependencywalker.h b/src/dependencywalker.h index 469ad87..c475ce5 100644 --- a/src/dependencywalker.h +++ b/src/dependencywalker.h @@ -49,6 +49,9 @@ class DependencyWalker { // With $QTDIR/$QT_ROOT_DIR added ahead of everything but rpath so a custom Qt build's own libraries are preferred over a same-named system copy. Returns an empty string if not found. QString FindLibrary(const QString &filename); + // Removes every already-registered ELF whose filename starts with `filename_prefix` from all_elfs(), so it is never copied into the AppDir. library_locations() is left untouched - a stale search directory is harmless. For libraries that must always be resolved against the running system rather than bundled (e.g. ALSA): once removed here, the dynamic linker falls through the AppDir rpath (now empty for this name) to the host's own copy at runtime, same as for any other unbundled system library. + void RemoveElfsByFilenamePrefix(const QString &filename_prefix); + private: void EnsureDefaultLibraryLocations(); void AppendLib(const QString &path); diff --git a/src/miscdeploy.cpp b/src/miscdeploy.cpp index 6904065..7937d10 100644 --- a/src/miscdeploy.cpp +++ b/src/miscdeploy.cpp @@ -170,17 +170,9 @@ void HandleAlsa(DependencyWalker &dependency_walker) { for (const QString &lib : dependency_walker.all_elfs()) { if (!QFileInfo(lib).fileName().startsWith("libasound.so"_L1)) continue; - qInfo() << "Bundling alsa-lib directory..."; - - const QStringList locations = dependency_walker.FindWithPrefixInLibraryLocations(u"alsa-lib"_s); - if (locations.isEmpty()) { - qWarning() << "Could not find alsa-lib directory"; - qWarning() << "E.g., in Alpine Linux: apk add alsa-plugins alsa-plugins-pulse"; - } - else { - qInfo() << "Bundling dependencies of alsa-lib directory..."; - dependency_walker.AddElfTree(locations.first()); - } + // ALSA's PCM/CTL name resolution (e.g. "default:CARD=...") depends on its plugin modules (dmix, dsnoop, pulse, pipewire, ...) and the /usr/share/alsa/{alsa.conf,cards,pcm,ucm2} snippets and hardware-specific UCM profiles maintained alongside it - all versioned together by the running distribution and whatever sound server is actually active on it. Bundling the build host's libasound.so and its plugin directory instead of the running system's reliably breaks device access (e.g. "Invalid argument" opening "default:CARD=...") even on a nominally identical host, once its alsa-lib/UCM data has moved on since the build. So ALSA must always come from the running system: remove it here rather than bundle it, the same way HandleNvidia refuses to bundle libnvidia*. + qInfo() << "Not bundling ALSA: removing libasound.so* so it resolves against the running system's alsa-lib at runtime"; + dependency_walker.RemoveElfsByFilenamePrefix(u"libasound.so"_s); break; } diff --git a/src/miscdeploy.h b/src/miscdeploy.h index 5c09277..965ab01 100644 --- a/src/miscdeploy.h +++ b/src/miscdeploy.h @@ -42,7 +42,7 @@ bool HandleGdk(DependencyWalker &dependency_walker, const AppDir &appdir, QStrin // Returns false and sets error_message on failure. bool DeployGtkDirectory(DependencyWalker &dependency_walker, const AppDir &appdir, const int gtk_version, QString &error_message); -// Bundles the alsa-lib plugin directory if libasound is a dependency. Best-effort: only warns if not found. +// Removes libasound.so* from the dependency set if present, so ALSA is always resolved against the running system's alsa-lib rather than bundled: its plugin modules, /usr/share/alsa config, and hardware UCM profiles are versioned together by the host and bundling them breaks device access on any host whose alsa-lib/UCM data doesn't exactly match the build host's (see miscdeploy.cpp for the failure mode this avoids). void HandleAlsa(DependencyWalker &dependency_walker); // Bundles the gio/modules directory (and its giomodule.cache, which unlike loaders.cache/immodules.cache holds no absolute paths so needs no patching) if libgio-2.0 is a dependency, so bundled GIO can still load its extra modules (gsettings backends, TLS backend, proxy resolvers, volume monitors) via GIO_EXTRA_MODULES. Best-effort: only warns if not found.