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 ownusr/binsymlink 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>oropt/<Name>that holds the binary andresources/. The rest of the tarball notes on Electron apply: Electron base app, zypak,unset ELECTRON_RUN_AS_NODE. Debian packages often recordchrome-sandboxsetuid; clear it withchmod -R a-s appso 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 hasusr/lib/<Name>/, keep theusrtree whole (mv stage/usr app/usr) and runapp/usr/bin/<name>. Tauri looks for its resources at<exe dir>/../lib/<Name>; a flattened binary looks in the runtime’s/usr/libinstead, finds nothing, and usually runs on silently without them. Never setAPPDIRto point it elsewhere: Tauri panics whenAPPDIRis set outside an AppImage. - Desktop entry, icons, metainfo under
usr/shareare 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 thansid. 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. Usesidonly 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 fdropslibXmu.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: allpackages that neitherDependson the library nor soname analysis reveal (Ghostscript’s ICC profiles inlibgs-common, its fonts infonts-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.