LuneOS runs the webOS user experience on phones and tablets, using webOS OSE's core and the webOS Ports card shell. It supports two kinds of device: Halium targets reuse the Android vendor stack through libhybris and binder, and mainline targets run upstream Linux drivers on Qualcomm, Allwinner, Rockchip, Broadcom and NXP silicon. Everything above the hardware-adaptation layer is shared. HP webOS 3.0.5 on the TouchPad is shown alongside for comparison.
The bands follow webOS OSE’s architecture diagram, from Core Applications down to BSP and Kernel. The bottom two bands split by target family, and the toggle dims whichever family you aren’t looking at. Each dot shows where a component’s source comes from.
meta-android-halium.inc adds the halium override, which swaps EGL/GLES to libhybris, pulls in the LIBHYBRIS_RDEPENDS set, and gives packages a separate -halium package arch. What each individual device gets on top is no longer written per machine: since the September packagegroup rework it comes from MACHINE_FEATURES (see below).OSE was designed for a TV or signage device with one full-screen app at a time. LuneOS keeps OSE’s core (LS2, SAM, WAM, LSM, DB8, Nyx, audiod), changes some parts, and adds what a phone needs: cards, telephony, sensors and Android hardware.
| OSE block | In LuneOS | Why |
|---|---|---|
| Home · Launchpad · Status Bar | luna-next-cardshell QML, loaded as LSM’s WebOSCompositor import | webOS card multitasking, gestures, dock mode and lock screen. OSE’s placeholder compositor import is removed at build time. |
| SAM | SAM, plus luna-appmanager serving com.palm.applicationManager | Legacy Enyo 1, Mojo and PDK apps still call the Palm-era API names. |
| WAM + Chromium | WAM fork (clang build) on webos-ports/chromium151 | Replaces the older QtWebEngine-based WebAppMgr. qtwebengine is blacklisted in the distro. Touch input is enabled. |
| Settings (Enact) | settings-qml | Phone-sized settings, with panels for telephony, NFC, fingerprint and so on. |
| Nyx HAL | nyx-modules with a per-machine .cmake, plus nyx-modules-hybris | Battery, haptics, LEDs and keys come from Android HALs on Halium and from sysfs on mainline. |
| Connectivity | OSE connman adapter and Bluetooth, plus webos-telephonyd on oFono | OSE has no cellular stack. Voice calls use Sailfish’s voicecall. |
| Media | OSE uMS and g-media-pipeline, plus gst-droid or libcamera, plus sensorfw | Cameras and sensors on phones and tablets. |
| BSP (Raspberry Pi 4) | meta-smartphone, meta-pine64-luneos, meta-rpi-luneos | Two hardware-adaptation families and 30+ machines. |
| Not in OSE | Enyo 1, Mojo, mojomail, PDK shim, Preware, novacomd, keymanager | Compatibility with Palm and HP webOS 1.x–3.x apps. |
Rows are the same layers as Figure 1. The legacy column is HP webOS 3.0.5, read directly from the TouchPad AT&T rootfs (build 86, Dec 2011). Where the Pre 2 on webOS 2.2.4 differs, the table notes it. In legacy webOS, one binary, LunaSysMgr, was the compositor, card manager, app manager and much of the system service layer. LuneOS still serves many of LunaSysMgr’s Palm bus names, but from separate luna-* services.
| Layer | HP webOS 3.0.5 · TouchPad | webOS OSE | LuneOS |
|---|---|---|---|
| Shell & compositor | LunaSysMgr, a single Qt process handling cards, launcher, notifications, dock, keys and display. No Wayland. | LSM with QML System UI (Home, Launchpad, Status Bar) | luna-surfacemanager + luna-next-cardshell: the webOS card UI rebuilt as a Wayland compositor |
| UI toolkit & GPU | Qt 4.8.0 (4.6.1 on 2.2.4) rendering straight to vendor EGL/GLES (Adreno on TouchPad, PowerVR SGX on Pre 2) | Qt 6, QtWayland, Mesa | Qt 6.12, QtWayland; libhybris over vendor EGL, or Mesa |
| App lifecycle | Built into LunaSysMgr: com.palm.applicationManager, com.palm.appinstaller | SAM, appinstalld2 | SAM, plus luna-appmanager serving the same Palm names |
| System bus names | LunaSysMgr also owns com.palm.display, com.palm.systemmanager, com.palm.vibrate, com.palm.keys | com.webos.* names | Split out: luna-displaymanager, luna-authmanager, luna-haptics |
| Web runtime | WebAppMgr, a forked child of LunaSysMgr running all web apps in one WebKit (libWebKitLuna) process | WAM + Chromium, one renderer per app | WAM + Chromium 151, one renderer per app |
| Browser | BrowserServer, a separate WebKit server process | Enact Browser | Atlas on WAM |
| App frameworks | Mojo, Enyo 1.0, foundations libraries | Enact | Enyo 1 and Mojo (compatibility), Enyo 2, Enact, QML |
| JS services | Node.js v0.4.12 (v0.2.3 on 2.2.4) with palmbus.node, fork_server.js | Node.js, webos-service | Node.js 22, mojoservicelauncher, nodejs-module-webos-* |
| Native apps | PDK: SDL 1.2, libpdl, GLES, started by LunaSysMgr’s IpcServer, with “direct rendering” for full-screen apps | Native Qt/QML apps | Qt 6 apps; PDK apps through pdk-luneos under qemu-user (armel) |
| Bus | ls-hubd_private + ls-hubd_public (two hubs, palm://) | LS2 (luna-service2) | luna-service2, one ls-hubd |
| Data | mojodb-luna and tempdb on an encrypted LVM volume (cryptofs), filecache | DB8 on LevelDB | DB8 on LevelDB, still named com.palm.db |
| Hardware abstraction | libhid* plugins (touch panel, keypad, accelerometer, compass, light, proximity), hidd, libhal | Nyx | nyx-modules, plus nyx-modules-hybris on Halium |
| Telephony & network | TelephonyInterfaceLayer, PmWanDaemon, qmuxd, PmNetConfigManager, PmWiFiService | connman adapter; no cellular | webos-telephonyd on oFono; connman adapter |
| Bluetooth | Proprietary PmBtStack / PmBtEngine | BlueZ | BlueZ 5 (with bluebinder on Halium) |
| Media | mediaserver, GStreamer 0.10, PulseAudio 0.9.22, audiod | uMediaServer, GStreamer 1.x, PulseAudio, audiod | uMediaServer, GStreamer 1.28, PulseAudio 17, audiod |
| Init | upstart 0.3.8 jobs in /etc/event.d | systemd, bootd | systemd 257, bootd |
| Kernel & storage | 2.6.35-palm-tenderloin (2.6.32 on Pre 2). Read-only ext3 root plus LVM store volumes (var, cryptodb, filecache, media) | Raspberry Pi 4 kernel | Android GKI or vendor kernel, or mainline 6.9–7.2. Rootfs in userdata or on a wic partition |
| Updates & developer access | UpdateDaemon (OMA DM), novacomd, ipkg | SW Updater, devmode | org.webosports.service.update, devmode, novacomd, opkg |
WebAppMgr child from the same binary. That child hosted every web app in one WebKit instance and passed rendered frames back over libLunaSysMgrIpc shared memory. LunaSysMgr watched for webKitDied because one crash took down every card. LuneOS uses the OSE model instead. Every app is a Wayland client, each web app gets its own Chromium renderer, and lifecycle has moved from the shell into SAM.Two separate mechanisms decide what lands in an image. The halium machine override decides whether the Android vendor stack is built at all. MACHINE_FEATURES decide which hardware stacks ride along, so a machine config declares what the device has rather than listing packages. This replaced the per-machine RDEPENDS:append:<machine> lines in packagegroup-luneos-extended.bb.
| Declared on the machine | Pulls in | Who sets it |
|---|---|---|
halium override | LIBHYBRIS_RDEPENDS: libhybris, android-system, lxc, android-property-service, android-tools, qbootctl, hwcomposer QPA, pulseaudio-modules-droid(-hidl), gst-droid, bluebinder, nyx-modules-hybris, wlan-dynamic-start, wlan-suspend-mode | Not a feature — set by meta-android-halium.inc for every Halium machine |
camera-rear · camera-front · camera-swivel | Camera app, v4l-utils, and on mainline libcamera + libcamera-gst. A front or swivel camera also pulls luneos-faced | 30 of 32 phones declare their camera topology; it replaced blanket camera dependencies |
fingerprint | biomd + webos-fingerprint-adapter | 19 machines, mostly Halium arm64 phones |
nfc | nfcd, nfcd-tools, nfcd-binder-plugin, webos-nfc-adapter | 19 machines. Deliberately absent where the hardware lacks it, or where there is no Android container to reach the binder NFC HAL |
esim | lpac, luneos-esim-adapter, a zbar GStreamer plugin for QR activation | The Pixels and halium-arm64 |
keyboard-qwerty · keyboard-t9 · keyboard-touch · trackpad | Hardware-keyboard support: maliit switches to Maliit::Hardware, the on-screen keyboard stays down, and kbdscroll turns key-surface or trackpad gestures into scrolling | KEY2, MP01, Q25, the three Titans, MindPhone (T9) |
killswitch | org.webosports.service.killswitch | FuriPhone FLX1s, PinePhone, PinePhone Pro |
wifi | wireless-regdb-static, cfg80211-regdb-reload | Every phone |
phone | oFono, webos-telephonyd, voicecall and the phone app | Every cellular device |
E Ink is the exception that proves the rule: there is no eink feature, and the MP01 pulls org.webosports.service.eink through MACHINE_EXTRA_RRECOMMENDS instead. Waydroid, the Qualcomm modem helpers and a few QEMU bits are also still per-machine lists.
Each subsystem reaches one shared LuneOS consumer in the middle column. The Halium path goes through Android HALs and the mainline path through kernel drivers. Services above this layer don’t know which path is in use.
| Subsystem | Halium path | Shared consumer | Mainline path |
|---|---|---|---|
| Graphics | Vendor EGL/GLES loaded by libhybris · qt6-qpa-hwcomposer-plugin · hybris EGL server buffer in qtwayland (dmabuf off) · LUNEOS_ANDROID_EGL names the driver where the vendor does not | luna-surfacemanager Wayland clients | Mesa (freedreno, panfrost, lima, vc4) · Qt eglfs with kms gbm · dmabuf · softpipe fallback where the board has no GPU driver |
| Audio | pulseaudio-modules-droid (+hidl) → audio HAL · droid-audio-config-gen | PulseAudio + palm-policy audiod | ALSA UCM profiles (msm8953, PinePhone Pro, RK817) · alsa-state |
| Telephony | ofono-binder-plugin + libgbinder-radio → radio HAL · rilmodem/qmimodem disabled | oFono 2.19 webos-telephonyd | QMI/MBIM via libqrtr-glib · rmtfs qrtr rpmsgexport · eg25-manager on PinePhone |
| Camera | gst-droid + droidmedia → camera HAL | GStreamer camera service | libcamera 0.7.2 + libcamera-gst · V4L2 |
| Sensors | sensorfw built with autohybris binder → sensors HAL | sensorfw qtsensors plugin | sensorfw iiosensorsadaptor · IIO drivers |
| Bluetooth | bluebinder bridges the BT HAL to a virtual HCI | BlueZ 5 bluetooth2 | Kernel HCI drivers · bes2600, brcmfmac firmware |
| Platform HAL | nyx-modules-hybris (libhybris, libsuspend, libgbinder) overrides individual modules; the LED module probes the Android lights HAL | nyx-lib sleepd · displaymanager | The same nyx modules. Device paths come from /etc/nyx.conf, written at boot by generator 30-nyx-conf |
| Biometrics · NFC | biomd → fingerprint HAL · nfcd-binder-plugin | fingerprint adapter webos-nfc-adapter | No fingerprint path · neard (NFC) |
| Android apps | Waydroid (halium-arm/arm64, *-halium) | Waydroid container | Waydroid (Pine64, Raspberry Pi) with binderfs |
A Halium device runs two userspaces on one Android kernel. LuneOS is the glibc host. Android’s /system and the device’s /vendor run in an LXC container started by android-system. Host code reaches vendor code in two ways: libhybris loads the vendor libraries into the host process, and libgbinder calls the HAL services over binder.
.so files into LuneOS processes through libhybris. Radio, sensors, Bluetooth, biometrics and NFC talk to HAL services over binder through libgbinder. luna-surfacemanager starts only after Android is up (surface-manager-daemon-after-android.conf). Current targets use the generic Halium 16 GSI as /system on top of the device’s own /vendor. Halium’s charger UI is stopped during start-up because it holds DRM master, which the compositor needs.The two families boot differently until the initramfs runs switch_root into systemd. After that they share one unit graph. On Halium, android-system.service is required by basic.target, and the graphics stack waits until Android reports its HALs are running. There is no android-boot-complete target; readiness is checked by polling getprop.
Unlocked (verifiedbootstate=orange). Loads boot.img and passes androidboot.slot_suffix on A/B devices.
boot.img: kernel + initramfs-android-imagePacked by kernel_android.bbclass (GKI devices also get init_boot). The Halium rootfs itself has no kernel, so one halium-arm64 rootfs serves every device.
/initLights the panel backlight, starts mdev, then synthesises by-partlabel links the kernel does not provide, so Halium’s own mountroot can stay unpatched. Loads vendor and GKI modules, waits for userdata, and holds boot to charge if the battery is flat.
Mounts userdata and looks for rootfs.img (loop) or /data/luneos (bind). The rootfs is read-only unless .writable_image exists, which dev images create.
Loop-mounts the GSI system.img at /var/lib/lxc/android/rootfs → /android. Mounts the device’s own vendor$slot. Binds /var, /home and /media/internal from userdata/luneos-data.
switch_root /rfs /sbin/initLuneOS’s own build of the vendor modules is copied out before the initramfs is freed. systemd then starts ls-hubd, configd and surface-manager early (from local-fs.target).
android-system.servicemount-android.sh (dynamic partitions, vendor_dlkm, APEX, binderfs), then lxc-start -n android -- /init. Android init brings up servicemanager and the HALs. start-android-hals.sh and wait-for-android.sh gate on getprop, for up to 120 s.
luneos-device-config runs twelve generators that write nyx.conf, the oFono binder slots, surface-manager.env, the battery and PulseAudio settings, the hardware-key map and luna-platform.conf. After it come surface-manager, oFono (which first waits for the RIL’s binder slots), PulseAudio, sensorfwd, bluebinder, biomd and nfcd.
When it goes wrong. With initrd_log_fail on the kernel command line, the initramfs dumps dmesg every three seconds into a spare partition — init_boot_a for the failure dump and init_boot_b for progress, or one partition named by initrd_log_part on devices without them. A failed boot now parks forever instead of panicking, so the dump and the USB gadget survive.
PinePhone: EFI firmware → systemd-boot. PinePhone Pro and PineTab2: megi U-Boot → extlinux → fitImage. Raspberry Pi: GPU firmware → U-Boot → boot.scr. msm8953: aboot, optionally with lk2nd prepended.
initramfs-simple-image (PinePhone, PineTab2 inside the FIT). PinePhone Pro’s FIT carries initramfs-uboot-image, but the kernel discards it and mounts root=PARTLABEL=rootfsA itself. msm8953 phones use initramfs-scripts-android inside an Android boot.img.
/initMounts pseudo-filesystems and starts mdev. Checks for recovery (bootmode=recovery or a key held down → luneos_recovery_ui). Sets up USB networking.
Looks up the partition named luneos-root* (using LoaderDevicePartUUID on EFI), grows it to fill the card, and mounts it read-write. There is no /var or /home split.
switch_root → systemdSame unit graph as Halium, without the Android units. luneos-device-config still runs early.
eg25-manager (before oFono), luneos-usb-gadget (before connman) and luneos-diag. PineTab2 also runs wifi-module-load and hciattach. Qualcomm phones load firmware with msm-firmware-loader.
ls-hubdThe Luna bus is up (Type=notify). PmLogDaemon has no unit; the bus starts it on demand.
surface-manager + card shellsurface-manager -platform eglfs loads WebOSCompositor = luna-next-cardshell. BootLoader.qml shows the logo, then runs initctl emit lsm-ready.
default-webos.targetStarts sam (after the bus and the compositor), then bootd. webapp-mgr is gated on /tmp/lsm-ready.
bootd → core-boot-donewebos-cbd.target: audiod, sleepd, power, Bluetooth, camera. Then init-boot-done → connman and PulseAudio.
datastore-init-startwebos-dis.target: DB8 (main, media, temp), configurator-db8, luna-sysservice, settings, pdm.
minimal-boot-donewebos-mbd.target: maliit, notificationmgr, uMediaServer, mediaindexer.
rest-boot-done → boot-doneactivitymanager, appinstalld, configurator-activity and the download manager. Then /tmp/boot-done triggers lsm-ready.service, which restarts WebAppMgr and maliit.
wlan-dynamic-start arms qcacld-3.0 before wpa_supplicant and connman, kbdscroll follows the compositor on keyboard devices, and the MP01 runs einkd for E Ink refresh modes.
luna-appmanager decides the UIServes org.webosports.bootmgr. If ran-firstuse is missing it launches First Use; otherwise it launches com.palm.launcher, com.palm.systemui and the boot-time apps. The card shell reads this state.
boot.bin)Palm’s bootloader. Loads /boot/uImage (2.6.35-palm-tenderloin) from the ext3 boot partition.
store/var and /var/log mount from LVM. cryptodb and cryptofilecache are encrypted and mounted later. Media is FAT.
rcS → mountall → bootmiscJobs in /etc/event.d start in order of start on stopped … events.
ls-hubd_private → ls-hubd_publicThe public hub starts on ls-hubd_private-ready. finish runs on ls-hubd_public-ready.
finishStarts together: powerd, mojodb, tempdb, filecache, audiod, mediaserver, TelephonyInterfaceLayer, PmNetConfigManager, bluetooth, sensord, hidd, keymanager and configurator.
LunaSysMgrStarts on stopped configurator. Without ran-first-use it runs with -a com.palm.app.firstuse. It forks WebAppMgr and emits LunaSysMgr-ready.
LunaReady, activitymanagerJobs follow LunaSysMgr’s ready event. activitymanager waits for datastore-initialized, and PmWanDaemon for configurator.
Every device now brings up its own USB network: each board has its own /24 (PinePhone 172.16.42, PinePhone Pro .43, PineTab2 .44, Halium .45, FuriPhone .46), the device takes .2, and a one-lease udhcpd hands the host .1, so a single host-side profile works everywhere. The gadget also carries a per-device product string and a serial taken from the device tree.
The webos-*.target files have no ordering of their own. bootd calls initctl emit, which touches /tmp/<event> and starts the matching target. The only LuneOS-specific targets are lsm-ready.target and nyx.target.
Launching an app is the same on every target. The shell asks SAM over the Luna bus, SAM picks a runtime from the app type in appinfo.json, and the app process draws a Wayland surface that the compositor shows as a card.
This is the bblayers.conf of the wrynose tree. Arrows are LAYERDEPENDS, and the amber number is BBFILE_PRIORITY, where the higher number wins when two layers provide the same recipe. Hardware layers are on the left, distro layers on the right, and both sit on OpenEmbedded.
meta-mecha-luneos as a third board layer beside Pine64 and Raspberry Pi. Branches here: meta-webos-ports herrie/latest-20260930, meta-smartphone herrie/fajita-and-q25, meta-pine64-luneos herrie/kernel-7.2, meta-mecha-luneos main, meta-qt6 6.12, everything else wrynose.Every target builds luneos-dev-image, which is webos_image plus the packagegroup-webos-extended, packagegroup-luneos-extended and packagegroup-luneos-development package groups. What differs per target is how the image is packaged and installed.
| Halium · GSI era | Halium · legacy | Mainline Qualcomm | Pine64 · Raspberry Pi | |
|---|---|---|---|---|
| Boot artefact | luneos-bootimg-gki → boot + init_boot / vendor_boot (header v3–v4), for both true Tier A devices and Tier B devices that only borrow the packaging. | kernel_android.bbclass, header v0 to v2 (the OnePlus 6T is the first v1, which keeps its device tree appended to the kernel) | Image.fastboot, optionally with lk2nd prepended at a configurable offset: 1 MiB for the msm8953 fork, 512 KiB for upstream lk2nd on msm8916 | Kernel and DTB inside the wic image; U-Boot or systemd-boot |
| Rootfs | rootfs.img inside userdata.img, next to the GSI | tar.gz in a recovery zip | Recovery zip | .wic.gz + .wic.bmap |
| Install | flash.sh: vbmeta, boot_a/b, userdata over fastboot | TWRP sideload · webos_deploy.sh | TWRP / fastboot | dd or bmaptool to SD or eMMC |
| Recipe | luneos-fastboot-package.inc | luneos-package.inc | kernel_android.bbclass | *-luneos-bsp-image.wks |
In the working tree, kernel_android.bbclass is also gaining an unsigned AVB hash footer for vendors that chain boot from vbmeta. No machine config switches it on yet.
The UBports Installer fork has validated configs for mako and hammerhead. Configs for mido, rosy and tissot are still to do, and Pine64 needs a new block-device plugin.
Every MACHINE in the tree: 44 of them, ten added in the last two weeks. Active means a current image in tmp/deploy/images or confirmed working by the maintainer. Builds means build history but no current image. Halium devices are split into two tiers: tier A runs the Android Common Kernel (GKI) with the stock vendor modules, tier B builds a device-specific vendor kernel in-tree. Three tier B devices (Q25, MP01, Pixel 4a 5G) borrow GKI packaging without a GKI kernel.
| Machine | Device | SoC | Family | Kernel | Layer | Status |
|---|
SoC names for the older Tier B devices are from general knowledge, not the machine configs. Three of these are not committed yet: the meta-unihertz layer with its three Titans, sm-t220, and mako-halium. rosy is a mainline machine but still carries a leftover android-system-image-rosy recipe.