What a release contains
Every push tomain (unless the head commit message contains [skip ci]) is a release, built by
.github/workflows/release.yml:
Flatpak for Termux is not shipped as a package:
install.sh downloads the build kit
(packaging/termux-flatpak, published on the site) and builds Flatpak on the phone. Bump KIT_REV
in the kit whenever its patches change.
Two builds of the CLI
- Termux gets a binary built against Android’s bionic (
cargo ndk -P 24, targetsaarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android). A static musl binary does not work there: Android’s app seccomp filter kills processes that use legacy syscalls musl still issues on x86_64, x86 and arm32. - Everything else (every distro under proot, plain Linux) gets a static musl binary
(
*-unknown-linux-musl, linked withrust-lld, see.cargo/config.toml): the same binary in the Debian.deb, the Arch package, the.rpm, the.apk, the.xbpsand the tarball.
27.2.12479018) from the runner image when it has that version, else
downloads android-ndk-r27c-linux.zip from Google, checks its sha256 and caches it. The optional
window app’s build is the exception: it uses a JDK and the SDK’s android.jar, aapt2, d8,
zipalign and apksigner.
The display is a web page the server itself serves (embedded in the binary), so the CLI and its
packages are complete on their own. So is the template of the programs’ own apps
(crates/lindroids-apk/template/window-app.apk, APPS.md): it is committed, as
shortcut.dex is, and rebuilt with packaging/build-app-template.sh (a JDK and the SDK’s
android.jar, aapt2, d8, zipalign) only when packaging/webview-app changes; the phone fills
it in and signs it in Rust. Optionally, a release also carries the shared window app
(BROWSER_AGNOSTIC_APK.md): a small APK built by
packaging/build-webview-apk.sh with javac/d8/aapt2/apksigner, signed with
LINDROIDS_APP_KEYSTORE (base64 PKCS#12), LINDROIDS_APP_KEYSTORE_PASS and
LINDROIDS_APP_KEY_ALIAS (default lindroids). The release workflow fails if the key is not set,
so a published release always carries the APK; LINDROIDS_APP_TEST_SIGN=1 builds a throwaway
test-signed one for the lab, which is never published.
xwayland-satellite (X11)
lindroids run --x11 runs X11 programs through
xwayland-satellite (MPL-2.0), which runs
Xwayland rootless and makes each X11 window its own toplevel, so each one becomes its own window
on the page. The CLI looks for it at <dir of lindroids>/../libexec/lindroids/xwayland-satellite,
then on PATH. Nobody packages it for Termux, Debian or Ubuntu, so
scripts/build-xwayland-satellite.sh builds it from
the pinned release tarball (version, URL and sha256 at the top of the script), unmodified, with
default features:
- Termux (
aarch64 arm x86_64 i686): bindgen (xcb-util-cursor-sys), pkg-config and the linker are pointed at a per-arch sysroot unpacked from Termux’s ownlibxcb,xcb-util-cursorand dependency.debs, pinned with sha256 (main and x11-repo; packages.termux.dev and its mirrors). The binary needs onlylibc libdl libm libxcb libxcb-cursor, has RUNPATH/data/data/com.termux/files/usr/lib(as Termux’s own binaries) and 16 KB-aligned segments. Two dependency crates (smithay-client-toolkit, wayland-cursor) do not compile for Android as published (they fall back toshm_open, which bionic lacks); the Termux build uses them with the small patches inthird_party/xwayland-satellite/through--config patch.crates-io..., so xwayland-satellite’s own source stays unchanged. Why each patch exists: third_party/README.md. - Debian (
arm64 armhf amd64 i386): built on bookworm, so it needs glibc 2.34 or newer (Debian 12, Ubuntu 22.04 and later) pluslibxcb1,libxcb-cursor0andxwayland, which the.debrecommends (the static CLI must still install without them). The host’s Rust toolchain is mounted into the container read-only; crates are downloaded inside it. - Termux
.debs carry the bionic build only: X11 programs in proot-distro distros reach Termux’s display, so they need no satellite of their own. It adds about 5 MB installed (1.3 MB download) to a.deb. Termux recommendslibxcb,xcb-util-cursorandxwayland(the last two needpkg install x11-repo). - Every output is checked with
readelf(NEEDED, RUNPATH, interpreter, alignment, glibc version);scripts/build-xwayland-satellite.sh --checkre-runs the checks. Outputs carry a stamp of the script and the patches, so a release rebuilds them only when the pin changes; CI cachestarget/xwls/outwith the same key.
SATELLITE_VERSION, SATELLITE_COMMIT and
SATELLITE_SHA256 (curl -fsSL <tag tarball> | sha256sum; the script checks the commit in the
tarball’s header), check the crate versions in ANDROID_CRATE_PATCHES against the new
Cargo.lock (a patch that no longer applies fails the build), and update the version table in
third_party/README.md.
Versioning
scripts/version.sh defines the version for every artifact:
- version
<major>.<minor>.<N>: major.minor from[workspace.package] versioninCargo.toml, N =git rev-list --count HEAD(so CI checks out withfetch-depth: 0); .debversion = the version; git tagv<version>;LINDROIDS_BUILD_ID= the short commit hash (compiled into the binaries).
Cargo.toml; N keeps counting, so versions stay
monotonic for apt and pacman. Anything set in the environment (LINDROIDS_VERSION,
LINDROIDS_BUILD_ID) overrides the computed value.
lindroids version prints CARGO_PKG_VERSION+LINDROIDS_BUILD_ID (for example 0.1.0+9a35dc7),
so it shows the patch number from Cargo.toml, not N. The commit hash identifies the build.
Building locally
Toolchain: Rust stable with the 8 targets above plusaarch64-unknown-linux-gnu,
armv7-unknown-linux-gnueabihf and i686-unknown-linux-gnu (and the host’s
x86_64-unknown-linux-gnu) for xwayland-satellite, cargo-ndk with the Android NDK r27c
(ANDROID_NDK_HOME: android-ndk-r27c-linux.zip unpacked; default the newest
~/Android/Sdk/ndk/*), dpkg-deb, bsdtar, gpg, openssl, git; for xwayland-satellite also libclang (Fedora clang-libs, Debian
libclang-dev), pkg-config, patch, and podman or docker (inside a distrobox the script uses
the host’s podman through distrobox-host-exec; LINDROIDS_CONTAINER_ENGINE overrides). The
rpm, apk and xbps packages and repositories are made with those distributions’ own tools
(rpmbuild, createrepo_c, abuild-tar, apk index, abuild-sign, xbps-create,
xbps-rindex) in fedora, alpine and void containers (packaging/container.sh).
scripts/build-xwayland-satellite.sh,
packaging/build-debs.sh --version <v> [--arches "x86_64 amd64"] [--xwls target/xwls/out],
packaging/build-pacman.sh [--debs dist/debs] [--out dist/pacman] (needs bsdtar),
packaging/build-rpm.sh, packaging/build-apk.sh, packaging/build-xbps.sh
(--version <v> [--dist dist] [--arches ...]).
Testing a repository before publishing
tests/install-repos.sh makes the x86_64 package of every kind from
one static CLI, a repository signed with throwaway GPG and RSA keys, serves it on
127.0.0.1:8790, and runs curl …/install.sh | sh twice in containers of Debian, Ubuntu, Kali,
Arch, Fedora, Rocky 9, openSUSE Tumbleweed and Leap 15.6, Alpine (stable and edge), Chimera, Void
(glibc and musl), and Amazon Linux 2 and busybox (which must fall back to the static tarball,
served there too): each must pick the expected method and install a working lindroids. CI runs
it on every pull request (logs uploaded as install-repos-logs).
In Termux (the Android emulator reaches the host as 10.0.2.2), against a repository from
packaging/repo/build-repo.sh .scratch/repo served with python3 -m http.server 8790:
tests/host-integration.sh runs the server, lindroids debug-app (a stand-in for the browser
page) and foot through lindroids run, types into it, and checks the window’s frames; CI runs it
on every pull request and before every release. On pull requests CI also builds (or restores and
re-checks) the 8 xwayland-satellite binaries, and runs tests/install-repos.sh (which also checks,
in Debian, that the satellite’s libraries resolve and Xwayland comes with it).
The repositories
Static files on GitHub Pages, built bypackaging/repo/build-repo.sh:
site/: SvelteKit with adapter-static,
Tailwind and shadcn-svelte, theme following the device (mode-watcher), showing media/ (copied
by site/copy-media.js) and the latest VERSION. site.yml
builds it only when site/, media/ or the workflow change, and pull requests only build it.
It shares the release’s concurrency group, starts from the published branch and replaces only
the website’s files; the release keeps them, and build-repo.sh writes a plain index.html
only into a repository that has none. Locally: cd site && npm ci && npm run dev.
-
RPM (
packaging/build-rpm.sh, rpmbuild infedora): one repository for dnf, yum and zypper, every architecture in onerepodata/(createrepo_c, gzip so older zypper and yum read it). v4 packages with an xz payload,AutoReqProv: no(the CLI is static), and the satellite’s needs as Recommends by file and soname (/usr/bin/Xwayland,libxcb.so.1,libxcb-cursor.so.0) so Fedora’s and openSUSE’s package names both match.repomd.xmlis signed with the APT key (repomd.xml.asc; the key also asrepomd.xml.key); clients check it (repo_gpgcheck=1) and the packages through the checksums it signs (gpgcheck=0: the packages themselves are unsigned; the signed metadata covers them). yum 3 (RHEL/CentOS 7, Amazon Linux 2) has GnuPG 2.0, which cannot check ed25519 signatures, so install.sh uses the static tarball there. -
apk (
packaging/build-apk.sh, abuild-tar inalpine): v2 packages (apk-tools 2 and 3, Alpine, postmarketOS, Adélie, Chimera), made as abuild makes them; static CLI only.APKINDEX.tar.gzper architecture is signed with the RSA key byabuild-sign(.SIGN.RSA.lindroids.rsa.pub, so clients need/etc/apk/keys/lindroids.rsa.pub) and checked withapk verifyagainstlindroids.rsa.pubalone. Packages are unsigned: apk checks them against the checksum in the signed index. -
xbps (
packaging/build-xbps.sh, xbps-create invoid): the glibc architectures carry the satellite, the-muslones not.xbps-rindex --signsigns every<arch>-repodata,--sign-pkgevery package (.sig2), with the same RSA key. -
Without the RSA key (
LINDROIDS_APK_RSA_KEY) the apk and xbps repositories are left as they are and the release says so; install.sh falls back to the static tarball while a repository is not published. -
The pacman packages are made from the
.debs bypackaging/build-pacman.sh(same files;.PKGINFOand.MTREEas makepkg writes them; the Termux ones keep thedata/data/com.termux/files/usrpaths, as Termux’s own pacman packages do).build-repo.shsigns every package and the database with the same key (binary detached signatures;%PGPSIG%in the database) and keeps the newest 3 per architecture.lindroids.dbis a copy oflindroids.db.tar.gz: GitHub Pages serves no symlinks. install.sh trusts the key withpacman-key --add/--lsign-key; the fingerprint it expects is stamped into it bybuild-repo.sh(DEFAULT_KEY_FPR). -
Releasecarries MD5Sum/SHA1/SHA256 for every index;InRelease(clearsigned) andRelease.gpg(detached) are signed with SHA-512. NoValid-Until, so the repository does not expire when no release happens for a while. -
The workflow copies the current
gh-pagescontent intosite/first; the script adds the new.debs and keeps the newest 3 versions per architecture (--keep).gh-pagesis pushed as a single orphan commit, so old packages do not accumulate in git history. -
install.shisinstall.shwith the published URL andowner/reposubstituted (--base-url,--github-repo).VERSIONis the newest version in the pool; the static installer downloads that release’s tarball. -
The script refuses to sign with any key other than the one committed as
packaging/repo/lindroids.gpg, and verifies both signatures with that file alone, as apt will. -
Termux’s architecture
x86_64makes stockdpkg-debwarn (“not a valid architecture name”); Termux’s dpkg accepts it, and the scripts filter the warning.
Keys and secrets
APT key:
Lindroids APT repository <lindroids@users.noreply.github.com>, fingerprint
CF71814EDB63C911B430479C6FFD91C90F43BF7B, no expiry, no passphrase (it lives in a CI secret).
A revocation certificate is in ~/.local/share/lindroids/gnupg/openpgp-revocs.d/. Back up
~/.local/share/lindroids/gnupg offline: losing the key means every user has to fetch a new key.
scripts/set-github-secrets.sh sets both secrets with
gh secret set (nothing is printed). The first time, it generates the RSA key and writes
packaging/repo/lindroids.rsa.pub, which must be committed: build-repo.sh refuses an RSA key
that does not match it, and publishes it as lindroids.rsa.pub. Back up lindroids.rsa with the
GnuPG directory.
Rotating the APT key
- Generate a new key in a fresh GNUPGHOME, export it over
packaging/repo/lindroids.gpgand.asc, update the fingerprint here and in INSTALL.md, and runscripts/set-github-secrets.sh. - Commit; the next release is signed with the new key. Users re-run the installer (it rewrites the key file). If the old key was compromised, publish its revocation certificate.
Rotating the RSA key
Move~/.local/share/lindroids/lindroids.rsa and packaging/repo/lindroids.rsa.pub away, run
scripts/set-github-secrets.sh (it makes a new pair and sets the secret), commit the new
lindroids.rsa.pub. Alpine and Void users re-run the installer (it installs the new key; xbps
asks to trust it).
Publishing without the secret
scripts/publish-local.sh makes the same release from the machine
that holds the key: it builds and signs everything locally, creates the GitHub release, pushes
gh-pages and turns Pages on. The key stays on that machine. Without the secret the release
workflow still builds and tests every push, and says it did not publish.
One-time repository setup
- Create
github.com/dixonSolutions/lindroidsand pushmain. scripts/set-github-secrets.sh --release dixonSolutions/lindroids: sets the secret, runs the release workflow, waits for it and turns Pages on (step 4 below).- Settings → Actions → General → Workflow permissions: “Read and write” (the workflow pushes
gh-pagesand creates releases; it also declarescontents: write). - After the first successful release: Settings → Pages → Deploy from branch
gh-pages, folder/(root). No custom domain (no CNAME).
github.repository. Only the defaults in install.sh and the
URLs in the docs mention dixonSolutions/lindroids.