Halium GSI
nixos/halium/ is the phone and tablet target. It builds one generic system
image, a GSI, for every Treble device that takes generic system images and
runs Linux 5.10 or newer. It runs through Halium, the
Android hardware layer that ports such as Ubuntu Touch and Droidian build on.
nixosModules.gsi imports nixos/modules/base.nix, which holds everything a
PC and a phone share: the derisk desktop, homed accounts, NetworkManager, pm,
no Nix on the device, the /etc overlay and sysusers. It then adds its own
boot, boot.nix, and Android layer, android.nix, in place of the PC's UEFI,
verity /usr, sysupdate and installer. The web flasher
installs it.
There are no device ports. The target used to be a configuration that a port
filled in with its kernel, device tree, mkbootimg offsets, partition labels
and udev rules. All of that is gone, because two things Android made generic
cover it:
- Treble. A device's vendor HALs run under any system image of a newer
Android, so Halium's generic Android 14 system image starts them on every
device. It is pinned in
nixos/pkgs/halium-gsi.nixand lives in the store, so it updates with the OS. - The ramdisk is unpacked last. A device that launched with Android 13
or later boots its maker's kernel from
boot, its first drivers fromvendor_boot, and the generic ramdisk frominit_boot, and the flasher replaces only that ramdisk. An older device keeps kernel and ramdisk together inboot. There the flasher reads that boot image and writes it back with this ramdisk after the device's own, and the kernel unpacks both in turn. Either way the kernel, its modules and its device tree stay the device's own, and nothing here is built per device.
How a device boots it
- The bootloader loads the device's kernel and unpacks the device's own
ramdisk, from
vendor_boot, or frombooton a device withoutinit_boot. It then unpacks this one, NixOS's systemd initrd, over it. - In the initrd,
losos-gsi-first-stage-modulesloads the modules the device's ramdisk lists in/lib/modules/modules.load, which are the storage controller and what it hangs off. Without them there is nouserdata. - All of
userdatais the root filesystem. It is mounted by partition label, andx-systemd.growfsgrows it to fill the partition on the first boot. The command line comes from the device's own boot images, which keep theirs, so there is noinit=. The initrd boots/nix/var/nix/profiles/system, which the flashed filesystem carries. - After switch-root,
losos-gsi-superreadssuper's logical partition metadata for the booted slot withlpdumpand mapsvendor,odm,vendor_dlkmandsystem_dlkmwith device-mapper under their names without the slot suffix. They mount read-only at/vendor,/odm,/vendor_dlkmand/system_dlkm. losos-gsi-modulespoints the kernel's firmware loader at/vendor/firmwareand loads the generic kernel's modules fromsystem_dlkm, then the vendor's. None of these partitions carries amodules.dep.bin, so the loader,load-modules.sh, runsinsmodin passes until a pass loads nothing new, applyingmodules.optionsandmodules.blocklist. A module that never loads is logged and skipped.- Halium's system image is loop-mounted read-only from the store at
/android/system, and/android/system/systemat/system, where libhybris's linker looks. losos-android.servicestarts Android's init withlxc-start. The container shares the host's/devand network, gets the vendor partitions bind-mounted, and gets/var/lib/android/dataas/data. It uses LXC instead ofsystemd-nspawnbecause nspawn always gives a container a private/dev, and the host has to open the same HAL device nodes that ueventd creates. It starts before the display manager.- libhybris is a GLVND EGL vendor in
hardware.graphics, beside Mesa.
The system carries no kernel, with boot.kernel.enable = false. The kernel
that boots is the device's, so nixpkgs' would be dead weight in rootfs.
Every device in scope runs 5.10 or newer, which is systemd's minimum
baseline. The flasher reads the kernel's version out of any boot image it
repacks and refuses an older one.
Every mount on the Android side is nofail, so a device whose Android half is
missing or broken still reaches the login screen.
The container is not a security boundary. Android's init and the vendor HALs
run as host root with the host's whole /dev, every block device included,
so the vendor partition's closed blobs are trusted as much as the kernel, and
nothing here plays the part of the PC build's verity /usr. It drops
sys_module, sys_rawio, sys_time and the MAC capabilities; a /dev
holding only the HAL nodes would narrow it further, but which nodes those are
differs per device, so it waits for a device to find out.
What it builds
nix build .#packages.aarch64-linux.gsi produces the three files the
flasher writes, with SHA256SUMS:
losos-desktop_<version>_gsi-init_boot.imgis the initrd as an Android boot image. It has header v4, a ramdisk and nothing else, compressed with LZ4 in the legacy format the generic kernel decompresses.losos-desktop_<version>_gsi-vbmeta.imgis an empty vbmeta with verification turned off, so a bootloader that would checkinit_bootagainst the stock vbmeta boots this one.losos-desktop_<version>_gsi-userdata.simg.gzis the root filesystem as an Android sparse image. It is gzipped because a browser can decompress gzip natively.
The aarch64 release carries the same three files under its SHA256SUMS and
signature, which is where the flasher downloads them from.
Without the flasher, the same three go on with fastboot on a device with
init_boot:
fastboot flash vbmeta losos-desktop_<version>_gsi-vbmeta.img
fastboot flash init_boot losos-desktop_<version>_gsi-init_boot.img
gunzip losos-desktop_<version>_gsi-userdata.simg.gz
fastboot flash userdata losos-desktop_<version>_gsi-userdata.simg
fastboot reboot
Which devices
A device is in scope when three things hold. Its bootloader can be unlocked. It takes GSIs, which means Treble, and every device that launched with Android 9 or later has it. And its kernel is Linux 5.10 or newer, which in practice means it launched with Android 12 or later.
- On a device with
init_boot, which launched with Android 13 or later, onlyinit_bootis written. - On a device without it, which launched with Android 12,
bootis written. That is the device's own boot image, read back from the device in fastbootd or chosen from its factory image, with this ramdisk appended to its own. The flasher checks the kernel's version there and refuses a kernel older than 5.10, as on devices that launched with Android 11 or earlier.
Every such device has super, since devices that launched with Android 10
or later must have dynamic partitions. So losos-gsi-super has no fallback
for physical vendor partitions. A device without vendor_dlkm or
system_dlkm, as on Android 12, skips those mounts.
A device this image cannot carry is one with a 4.x or 5.4 kernel: systemd does not run on it, and a generic image cannot bring a newer kernel.
nixos/pkgs/ builds libhybris from its upstream master, and Halium's
android-headers, where one tree serves Halium 11 to 16.