Skip to content

Desktop: ship a complete Linux AppImage - #9113

Merged
oobabooga merged 12 commits into
unslothai:mainfrom
oobabooga:complete-appimage-reland
Aug 19, 2026
Merged

oobabooga merged 12 commits into
unslothai:mainfrom
oobabooga:complete-appimage-reland

Conversation

@oobabooga

Copy link
Copy Markdown
Member

Re-lands #8695, reverted in #8823, together with #8813 and the remaining fixes needed for a complete AppImage. #8695 is @wasimysaid's work; #8813 ports the two runtime fixes @ThomasEricB identified and tested in #8343.

Why

The AppImage has been thin since #7953. That avoided mixing an Ubuntu 22.04 web runtime with newer host libraries, which caused failures such as:

libcurl-gnutls.so.4: undefined symbol: nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation

The thin bundle instead requires the host to provide GTK, WebKitGTK 4.1, JavaScriptCore and AppIndicator. This does not work on distributions such as Fedora 44 KDE or immutable systems where those packages are unavailable or cannot be installed.

This PR restores a complete AppImage while preserving the #7953 boundary: the bundle owns one coherent Ubuntu 22.04 userspace web runtime, while libraries coupled to host services, display servers and drivers remain host-owned.

What changed

  • Replace the thin packager with Tauri's AppImage bundle and pin every linuxdeploy tool and plugin by digest.
  • Use a repository AppRun with no AppDir-wide LD_LIBRARY_PATH. Bundled ELF objects use $ORIGIN RUNPATHs instead.
  • Keep glibc, the loader, graphics, display, audio and other host-coupled libraries out of the AppImage.
  • Pin GTK_PATH, GIO_MODULE_DIR and the GStreamer plugin paths to the bundle. Remove foreign-architecture GIO modules copied from multilib build hosts.
  • Bundle the GStreamer plugins required for MP4, WebM, WAV and microphone capture. Keep libva and libvdpau so libgstlibav also loads on hosts without VA-API.
  • Run the idempotent AppDir finalizer after every input plugin because linuxdeploy does not guarantee plugin order.
  • Treat $APPDIR/usr as an invalid working directory for managed children and fall back to ~/.unsloth. Scrub bundle-only GUI and Python variables before launching managed processes, browsers or file managers.
  • Disable DMA-BUF rendering for the complete AppImage on Wayland and when the host has no usable GLES library, covering the Studio: Appimage Improvements for Immutable Distros (Currently outdated due to mostly being superseded by #8695) #8343 failure paths.
  • Repair .DirIcon so it is relative rather than a build-machine symlink.

The finished-artifact verifier now checks every ELF object, not only the main executable. It rejects foreign architectures, absolute or non-$ORIGIN RUNPATHs, forbidden host libraries, unpinned module paths, missing media plugins, and invalid desktop links. LC_ALL=C makes its readelf checks independent of the builder locale.

Verification

All results below use one AppImage built in ubuntu:22.04 with the release workflow's pinned toolchain:

Unsloth.AppImage
190634488 bytes
sha256 627a2f9019ee2bbe7ef7f7bea4246c39d82ac5dcdcb99b9427b1186e68f5fdd9
Verified complete x86_64 AppImage runtime (GLIBC_2.34)

Startup and desktop authentication passed without host GTK, WebKitGTK, JavaScriptCore or AppIndicator:

Host Display Result
Ubuntu 22.04 X11 pass
Ubuntu 22.04 without libGLESv2.so.2 X11 pass, DMA-BUF disabled
Ubuntu 24.04 Wayland via Weston pass, DMA-BUF disabled
Debian 12 X11 pass
Fedora 44 X11 pass
Bazzite 44.20260801.0 X11 pass
Linux Mint 22 with NVIDIA X11 pass

The Mint run used the live session that reproduced #7953, including its GTK_MODULES and GTK3_MODULES. The bundled GTK declined the host modules and started without loader errors. The managed backend ran from ~/.unsloth, not the read-only AppImage mount.

The packaged WebKitGTK model-download flow passed on Ubuntu 22.04, Ubuntu 24.04 and Mint 22:

POST /api/hub/download
POST /api/hub/download/cancel
PASS packaged AppImage model picker sent download, rendered progress, and cancelled
{"mp4": "maybe", "webm": "maybe", "wav": "maybe", "capture": true}

Every portability lane also loaded the bundled GStreamer registry and decoded real H.264 frames:

PASS bundled GStreamer decoded 12 H.264 frames (1382400 bytes of I420) on this host

The artifact contains 581 x86-64 dynamic ELF objects. None has a RUNPATH outside $ORIGIN, an absolute DT_NEEDED, or a baked-in builder path. The bundle ships no libnghttp2, libcurl or libstdc++, and no launcher or hook exports a global LD_LIBRARY_PATH.

For comparison, a rebuilt pre-#7953 complete AppImage failed on Mint by loading host gvfs modules into bundled GLib, failed on Fedora with EGL_BAD_PARAMETER, and shipped a dangling absolute .DirIcon. The new artifact passes those same lanes and checks.

Test suites:

  • cargo test --no-fail-fast in studio/src-tauri: 378 passed
  • AppImage security and Studio contract tests: 398 passed, 26 skipped
  • ruff check and the repository format hook on changed Python files
  • bash -n on the packaging scripts and git diff --check

Boundaries and limits

  • The host still owns glibc and the loader, GL, EGL, GLES, GBM, DRM, Vulkan, X11, Wayland, ALSA, PulseAudio, libstdc++, libgcc_s, libnghttp2 and libcurl.
  • The AppImage is 190 MB, compared with 123 MB for the reverted complete build and 45 MB for the thin build. About 29 MB is gstreamer1.0-libav and its dependency closure.
  • Bazzite was exercised through its published userspace under Xvfb and llvmpipe, not on Steam Deck hardware or a real Plasma compositor.
  • Pinning the module directory means the WebView gets GDummyProxyResolver rather than the host's GLibproxyResolver, so Hugging Face README, avatar and dataset panels ignore a desktop proxy. Model downloads run in the backend and updates in the Rust HTTP client, so both still honour the proxy environment. This arrived with Desktop: ship a complete Linux AppImage #8695's pin, which is what stops host GIO modules loading into the bundled GLib.
  • The in-app updater path was not exercised. The bundled binary does carry Tauri's AppImage marker, which selects that updater path.
  • Local builds must use the preseeded tool cache. The verifier rejects Tauri's unpatched downloaded AppRun and plugins. Release and PR build jobs both preseed the cache.
  • Nightly clean-machine runs will continue failing their AppImage verifier until a complete AppImage is published. PR runs test the artifact built from the PR.

@oobabooga

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59d45f24c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +8 to +10
APPDIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
PATH="$APPDIR/usr/bin:$APPDIR/usr/sbin:${PATH:-/usr/local/bin:/usr/bin:/bin}"
export APPDIR PATH

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear inherited library overrides before launching the AppImage

When the AppImage is started from an activated Conda/venv or any shell with LD_LIBRARY_PATH set, this launcher preserves that value. The dynamic loader searches LD_LIBRARY_PATH before the newly assigned DT_RUNPATHs, so host copies of GLib, GTK, WebKit, or GStreamer can override the bundled runtime and recreate the mixed-library crashes this complete AppImage is intended to prevent. Remove or sanitize inherited library paths before executing the desktop binary while continuing to leave graphics/driver libraries available through the normal system lookup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4087cbe. The AppRun now moves an inherited LD_LIBRARY_PATH into UNSLOTH_HOST_LD_LIBRARY_PATH and unsets it before exec'ing the bundled binary, so the $ORIGIN RUNPATHs the finalizer sets are what resolve GLib, GTK, WebKit and GStreamer.

Managed children keep the old behaviour: scrub_appimage_library_path in process.rs falls back to the saved value, so host CUDA and ROCm directories still reach the backend and host launchers, and graphics and driver libraries still resolve through ld.so.cache and the default directories.

verify-complete-appimage.sh now rejects a launcher that keeps the inherited value, and tests/security/test_release_desktop_appimage.py runs the real AppRun with LD_LIBRARY_PATH set and asserts the bundled binary does not receive it. That test fails against the previous launcher.

@oobabooga

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 4087cbe98d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasEricB

Copy link
Copy Markdown
Contributor

Going to test out later today! Sorry for the wait. I am concerned about merging that LD_LIBRARY_PATH change, that could easily cause problems on immutable systems.

@oobabooga

Copy link
Copy Markdown
Member Author

@ThomasEricB what kind of problems? Can you generate a reproducible one?

@ThomasEricB

Copy link
Copy Markdown
Contributor

Sometimes LD_LIBRARY_PATH can be way too finicky on my linux experience. I would have to test to see if everything is airtight as it seems.

@danielhanchen

Copy link
Copy Markdown
Member

Confirmed the thin path in studio/src-tauri/linux/build-thin-appimage.sh still requires host WebKitGTK 4.1, GTK and AppIndicator, which is exactly what breaks startup on Fedora and immutable distros. The branch is conflicting with main right now, so please rebase and I will get this reviewed.

@oobabooga

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 87248a2eee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasEricB

Copy link
Copy Markdown
Contributor

Give me 30 minutes I will turn on my steam deck. I will not keep you waiting sorry @oobabooga

@oobabooga
oobabooga merged commit de10203 into unslothai:main Aug 19, 2026
52 checks passed
@oobabooga

Copy link
Copy Markdown
Member Author

@ThomasEricB if you encounter any issues let me know and I'll address in a follow-up.

@ThomasEricB

Copy link
Copy Markdown
Contributor

Ok apologies something came up. I'll try to test it soon.

danielhanchen added a commit that referenced this pull request Aug 20, 2026
…giene (#9370)

`Desktop app clean machine` is red nightly on main, seven AppImage jobs, all with
the app's own preflight naming four missing sonames. The tree is not at fault and
neither is the test, so this changes only what the verifier says.

What is actually happening
------------------------------------------------------------------------
The scheduled run downloads the published release rather than building one.
`REL_TAG` is `v0.1.800-beta`, whose Linux asset was uploaded 2026-08-14T15:26Z and
never rebuilt. I extracted it: `usr/bin/unsloth-studio` and zero `.so` files, so
it resolves webkit2gtk, libsoup, javascriptcoregtk and appindicator from the host.
It is a pre-#9113 thin build, provably: the diagnostic it prints lives at line 210
of `build-thin-appimage.sh`, a script #9113 deleted, and the replacement
`appimage-apprun.sh` does not contain that string.

#9113 ("Desktop: ship a complete Linux AppImage") landed 2026-08-19T13:09Z and
also dropped the line that used to install the host runtime into the appimage row,
which is correct under a self-contained bundle and is why that row flipped on an
unchanged asset. The Aug 18 run was green, Aug 19's red was the ManagedStale
incident that #9263 fixed, and Aug 20 is the first scheduled run after #9113.

So the asset under test predates the packaging it is being tested against. The
first desktop release cut from current main will satisfy these jobs; nothing in
the tree needs changing to make that true, and until that release exists this
workflow is red for a correct reason.

Note this is invisible on a pull request by construction: on `pull_request` the
AppImage jobs consume the artifact from `appimage-pr-build`, built from the PR, so
#9113 was green when it merged. Only `schedule` and `push` reach the release.

The one real defect
------------------------------------------------------------------------
`verify-complete-appimage.sh` run against that asset reports:

    Complete AppImage does not clear an inherited LD_LIBRARY_PATH

It exits 1 correctly, but it fails at the launcher-hygiene check and never reaches
the required-component loop, so a bundle that ships NO runtime at all is reported
as an environment-variable bug. That is the difference between an hour of
diagnosis and a minute of it.

The required-component check now runs first and reports every miss instead of
exiting on the first, mirroring the AppRun preflight, which names all four sonames
at once rather than making the reader rediscover them one launch at a time. On the
published asset it now says:

    Complete AppImage is missing required runtime component: libwebkit2gtk-4.1.so*
    ... 21 lines ...
    Complete AppImage is missing 21 of 21 required runtime components; it resolves
    them from the host

Order only. No check is removed and no assertion is weakened.

Verified
------------------------------------------------------------------------
`bash -n` clean. A synthetic AppDir with all 21 components present falls through
to the launcher-hygiene check exactly as before, so the hygiene checks still run
and still fail when they should. Removing only webkit from that AppDir yields
`missing 1 of 21`, so the count is real rather than a formatting flourish.
tests/security/test_release_desktop_appimage.py: 15 passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants