Run unmodified SailfishOS apps on Ubuntu Touch (Lomiri/Mir).
- A Sailfish SDK target rootfs is the guest userspace, run inside an unprivileged
bubblewrap (
bwrap) mount namespace. No container daemon (no LXC/podman/docker). - The guest Qt 5.6 app talks directly to Lomiri's compositor over
wl_shell— no nested compositor, no protocol-translating proxy. - A small
LD_PRELOADshim fills the gaps Lipstick would normally provide (ambience background, orientation, missing globals). - GL: the SailfishOS build of libhybris is installed into the rootfs and
loads the device's own
/vendorAndroid GL blobs. UT's own libhybris is a glvnd vendor, and glvnd returns a nullglGetStringon threads it doesn't track (e.g. Gecko's compositor thread), which breaks GPU-accelerated web content. The SailfishOS build is non-glvnd and exports the real EGL API, so WebView apps and the browser render on the GPU.
| Path | What |
|---|---|
sfrun |
Python CLI: doctor, bootstrap, prepare-gl, run, … Encodes the validated bind/env recipe. |
shim/sailfish_shim.c |
LD_PRELOAD shim: registry diagnostics + (scaffolded) opaque background and orientation. |
clickable.yaml, click/ |
Click packaging (manifest, apparmor, desktop hook, icon). |
Code and data are decoupled (the UT rootfs is read-only). Code (sfrun,
shim/) is found relative to the script, so it can live on a read-only fs or in
/opt. Data (rootfs/, home/, cache/) goes to SFRUN_BASE, which defaults
to the script dir if it already holds a rootfs/ (dev layout), else
~/.local/share/sfrun.thekit when installed as a click, else ~/.local/share/sfrun.
Run these inside a Lomiri session, on the device:
# 1. Fill the guest rootfs from a Sailfish SDK target. With no argument, the
# latest is downloaded from releases.sailfishos.org (md5-checked, resumable,
# cached). Also takes a version (5.1.0.11), a local .tar/.tar.7z, or a dir.
# (.tar.7z needs 7zz/7z on PATH.)
python3 sfrun bootstrap
# 2. Install the SailfishOS libhybris stack into the rootfs (replaces the SDK
# target's software GL). Run once.
python3 sfrun prepare-gl
# 3. Patch Silica for Lomiri (drop the Lipstick cover window; paint a blurred
# in-window ambience instead). Idempotent.
python3 sfrun patch-silica
# 4. Check everything is ready.
python3 sfrun doctorApps that use Silica (libsailfishapp) need step 3. Keep
QT_QPA_PLATFORM=wayland (the default); Silica's desktop mode misrenders
(white-on-white, wrong rotation).
The SDK build sysroot lacks some runtime pieces, so after bootstrap install them and generate launchers:
python3 sfrun prepare-rootfs --scale 1.5 # runtime pkgs + z1.5 theme graphics
python3 sfrun set-scale 1.5 # Silica pixel ratio (user dconf)
python3 sfrun make-desktop harbour-fernschreiber # -> UT app drawer entryFrom the SailfishOS:Chum community repo (a zypper/OBS repo — no PackageKit or
ssu daemon needed; sfrun picks the newest chum release at or below the rootfs
version):
python3 sfrun chum enable # add + refresh the chum repo
python3 sfrun chum list # id / summary / installed, one per line
python3 sfrun install-pkg harbour-aenigma # install (deps resolved)Or from a local .rpm (deps still come from the repos):
python3 sfrun install-rpm ~/Downloads/harbour-foo.rpmInstalling an app auto-creates its drawer launcher. To remove an app plus its launcher and icons:
python3 sfrun remove harbour-fooThe Control panel (below) wraps all of this in a GUI.
python3 sfrun run harbour-someapp # launches per the app's .desktop Exec
python3 sfrun run /usr/bin/harbour-someapp # absolute path also works
SFRUN_DEBUG=1 python3 sfrun run ... # print the bwrap argvrun <id> follows the app's .desktop Exec, so both compiled apps
(Exec=harbour-foo) and QML-only apps (Exec=sailfish-qml harbour-foo) work; an
invoker booster prefix is stripped. Apps importing QML modules missing from the
rootfs (e.g. Sailfish.WebView, which needs the embedded Gecko engine) load to a
blank window — a missing dependency, not a render bug.
Apps share one guest home (SFRUN_HOME, default <base>/home) mounted at
/home/<device-user> and run as the device user, so dconf is shared — like a real
device.
python3 sfrun manage [cmd] # writable fake-root shell w/ net+DNS (zypper/rpm/patch).
# Project dir at /sfhome. bootstrap/prepare-gl/
# patch-silica are built on this.
python3 sfrun shell [cmd] # shell in the app sandbox (read-only, full GL/Wayland env)
python3 sfrun set-scale 1.5 # Silica theme pixel ratio (omit number to derive it)Build the shim: make -C shim
(cross: make -C shim CC=aarch64-linux-gnu-gcc).
clickable build --arch arm64 # -> sfrun.thekit_0.1.0_arm64.clickNeeds clickable >= 8.9 (older versions don't know the 24.04 frameworks and write
the wrong apparmor policy_version). The click is unconfined because bwrap
needs user namespaces; the bundled click-review fails on the new framework
(outdated review DB) but the package is fine — install with clickable install
or pkcon install-local --allow-untrusted.
The click ships only code (sfrun, the cross-built shim, the click/ hooks). It
installs under /opt/click.ubuntu.com/sfrun.thekit/<version>/; sfrun detects
that and keeps all data in ~/.local/share/sfrun.thekit/. make-desktop
launchers point at the package's current symlink, so they survive upgrades.
The click's desktop entry launches a Lomiri/PyOtherSide QML app (ui/Main.qml +
ui/backend.py) that drives sfrun as a subprocess: a status/doctor card,
one-tap Bootstrap and Setup, a searchable Chum store, Install from
.rpm, and a My apps list. Everything it does is a plain sfrun subcommand,
so CLI and GUI stay in sync.
- Shim hardening — land opaque-background + orientation in
sailfish_shim.c; stub any mandatory missing global the registry diagnostics reveal. - Real Harbour apps at scale, then harden the click packaging with on-device validation.
SFRUN_BASE (project base), SFRUN_ROOTFS, SFRUN_HOME, SFRUN_SHIM,
SFRUN_ANDROID_SDK (else auto via getprop), SFRUN_DEBUG,
SFRUN_TARGETS_URL (SDK target download base), SFRUN_TARGET_ARCH,
SFRUN_LIBDIR (guest libdir; default lib64 for aarch64, lib for armhf),
SFRUN_HALIUM_URL (libhybris HW-adaptation OBS base),
SFRUN_CHUM_URL (SailfishOS:Chum OBS base).