Browse the docs

Debian packages

A .deb is an ar archive with the app’s files in a data.tar member, laid out the way they would be installed: usr/bin, usr/lib/<name>, opt/<Name>, usr/share. The runtime has no ar or dpkg, but bsdtar reads the container, so apply_extra.sh pipes the data member into a second bsdtar and keeps the part of the tree the app runs from.

Nothing in the package’s maintainer scripts (postinst and friends) runs, and its Depends are not installed. Read both before packaging: whatever they would have done or pulled in has to come from the runtime, the wrapper, or a second download.

Start from one of these:

  • Tabularis: a single Tauri binary on the GNOME runtime.
  • Folo: an Electron app under usr/lib/folo, with the Electron base app. It reads the app directory and launcher name from the .deb’s own usr/bin symlink rather than hardcoding them.
  • Audionaut: on the freedesktop runtime.
For agents

apply_extra.sh

#!/bin/sh
set -eu

# Runs offline at install time inside org.freedesktop.Platform. The .deb keeps
# the Electron app under usr/lib/example; keep that directory at a stable path.

extra_root="${EXTRA_ROOT:-/app/extra}"
cd "$extra_root"

[ -f app.deb ] || { echo "missing extra-data: app.deb" >&2; exit 1; }

rm -rf stage app
mkdir stage
# The runtime has no ar/dpkg; bsdtar reads the .deb's ar container directly.
# --no-same-owner: a system-wide install runs this as root with no capabilities.
bsdtar -xOf app.deb 'data.tar*' | bsdtar --no-same-owner -xf - -C stage
[ -x stage/usr/lib/example/example ] || { echo "example not found in .deb" >&2; exit 1; }
mv stage/usr/lib/example app
rm -rf stage app.deb

To see what is in a .deb before writing this:

bsdtar -xOf app.deb 'data.tar*' | bsdtar -tvf -           # the files
bsdtar -xOf app.deb 'control.tar*' | bsdtar -xOf - ./control    # Depends
bsdtar -xOf app.deb 'control.tar*' | bsdtar -xOf - ./postinst   # what install would run

What to keep

  • Electron: the directory under usr/lib/<name> or opt/<Name> that holds the binary and resources/. The rest of the tarball notes on Electron apply: Electron base app, zypak, unset ELECTRON_RUN_AS_NODE. Debian packages often record chrome-sandbox setuid; clear it with chmod -R a-s app so a system install doesn’t write a setuid file.
  • Tauri: if the package has only usr/bin/<name>, moving that one binary is enough (Tabularis). If it also has usr/lib/<Name>/, keep the usr tree whole (mv stage/usr app/usr) and run app/usr/bin/<name>. Tauri looks for its resources at <exe dir>/../lib/<Name>; a flattened binary looks in the runtime’s /usr/lib instead, finds nothing, and usually runs on silently without them. Never set APPDIR to point it elsewhere: Tauri panics when APPDIR is set outside an AppImage.
  • Desktop entry, icons, metainfo under usr/share are not used from here. Copy the icon into the package directory once, and write our own desktop entry and metainfo.

Depends

Every library in Depends (and every NEEDED entry of the binary) must exist in the runtime you chose, or be shipped. Check with readelf -d, then look for each soname in the runtime:

ls "$(flatpak info --show-location org.gnome.Platform//51)"/files/lib/x86_64-linux-gnu/ | grep libsecret

A Qt .deb linked against the system Qt is the case that usually fails; see choosing a format.

Pinning a Debian package for a missing tool

When the app shells out to a command neither the runtime nor its own package ships (a clipboard helper, Ghostscript, sshpass), look in this order: upstream’s own Linux binary, then a Debian package pinned as extra-data, then a prebuilt stack.

A Debian pin goes in its own managed block, which debian-pins.yml re-resolves from the suite’s package index:

# BEGIN MANAGED DEBIAN trixie main
- type: extra-data
  filename: xclip.deb          # filename minus .deb is the package name
  only-arches: [x86_64]
  url: https://deb.debian.org/debian/pool/main/x/xclip/xclip_0.13-4_amd64.deb
  sha256: 14203d9a…
  size: 21328
# END MANAGED DEBIAN

Mark Shot pins wl-clipboard, xclip and libxmu6 this way. Lessons from doing it:

  • A stable suite (trixie) moves far less than sid. Debian deletes a superseded package from the pool at once, and from then until the re-pin job runs every new install fails on the download, while existing installs carry on. Use sid only when stable’s version is too old.
  • Copy shared libraries with their soname links: find … \( -type f -o -type l \) -name 'libXmu.so.*'. A plain -type f drops libXmu.so.6 -> libXmu.so.6.2.0, and the soname link is what the loader looks for.
  • Data lives in other packages. Debian splits data into Architecture: all packages that neither Depends on the library nor soname analysis reveal (Ghostscript’s ICC profiles in libgs-common, its fonts in fonts-urw-base35). Only running the tool finds them.
  • Give each tool its own wrapper that sets its library path; don’t add the pinned libraries to the app’s LD_LIBRARY_PATH, where a second fontconfig or freetype would shadow the runtime’s for the whole app.
  • “Detected” is not “works.” Apps often only test that a command exists. Run the feature that uses it, inside the sandbox.

rpm

Treat an .rpm like a .deb: bsdtar --no-same-owner -xf app.rpm -C stage unpacks its payload directly. Use it only when upstream publishes nothing else; the layout and the Requires need the same checks as above.

Edit this page on GitHub