Sprint 10 — Installer and Internal-Disk Deployment
Goal: Build a tested, user-confirmed installation path from a USB boot image to the ROG Ally internal NVMe SSD. After installation, the Ally boots PlayOS from the internal disk without a USB drive.
Primary Outcome: A user can boot the PlayOS installer from USB, confirm the target disk, wait for installation, remove the USB, and boot into the full PlayOS experience from internal storage.
Status: 🟢 Implemented and verified — installer source (src/playos-installer/), playos.mode=install trigger, complete FactoryReset handling, Buildroot installer-image wiring, and T8 validation (QEMU loopback + physical Ally install) have all landed; user confirmed NVMe install + boot from internal storage.
Prerequisites: Sprint 9 complete — complete MVP feature set running on the ROG Ally.
Why This Sprint Exists
Sprint 9 completes the feature set. Sprint 10 makes PlayOS installable to real hardware — without it, the system only runs from USB. This sprint also completes the FactoryReset IPC (all options) and establishes the full 5-partition installed-disk layout (ESP, A/B system slots, misc, data) that Sprint 11 builds the A/B update and rollback logic on.
Start Condition Checklist
- Sprint 9 complete: full feature set running on the ROG Ally from USB.
FactoryReset { erase_cache, erase_config }is implemented (Sprint 6);erase_games,erase_saves, anderase_logsare deferred to this sprint.playos-refdistroproduces a bootable USB image (make ally-usb-image→output/ally/images/playos-ally-usb.img).- QEMU can boot the dev image on the host for testing (
make qemu-run). - A spare NVMe or a test-only Ally is available for destructive disk tests.
Decisions Locked for This Sprint
- Partition layout (installed disk, 5 partitions): ESP (512 MiB FAT32, label
ESP), system A (4 GiB EROFS/squashfs, read-only, labelplayos-a), system B (4 GiB EROFS/squashfs, read-only, labelplayos-b, empty until Sprint 11),misc(64 MiB, A/B slot metadata, labelmisc), data (remainder ext4, labelplayos-data). The live USB keeps its current compact 3-partition layout — this 5-partition layout applies only to the installer-deployed internal disk. - Installer trigger:
playos.mode=installkernel cmdline flag, OR no existing PlayOS data partition on a removable-boot device - Installer NEVER silently formats: user must see disk details and hold A for 3 seconds
- Disk partitioning tool:
libfdiskor raw ioctl (no parted/fdisk subprocess) - UEFI fallback: always write
/EFI/BOOT/BOOTX64.EFI;efibootmgrregistration is a best-effort addition - Factory reset authority: only
playos-initperforms the filesystem deletion; the overlay sends an IPC command
Scope
In Scope
- Installer trigger mode in
playos-init - Installer Raylib UI (disk discovery, confirmation, progress, success, error screens)
- GPT partitioning via
libfdisk - FAT32 ESP format (
mkfs.fat) - Read-only system A slot from a pre-built EROFS/squashfs image (
playos-a) - Empty system B slot reserved for Sprint 11 A/B (
playos-b) - A/B metadata partition (
misc, 64 MiB) - ext4 data partition format (
mkfs.ext4) - EFI boot artifact write
- UEFI boot entry registration (
efibootmgr, best-effort) - First-boot from internal disk
- Complete
FactoryResetIPC (all five options) make installer-imageBuildroot target
Explicitly Out of Scope
- A/B update/rollback mechanism and dm-verity hashing (Sprint 11) — Sprint 10 only creates the partitions
- Network update download (Sprint 11/post-MVP)
- dm-verity (Sprint 11/post-MVP)
- Windows dual-boot or partition preservation (explicitly out of MVP scope)
Required Repository Changes
| Repo | Required work |
|---|---|
playos-refdistro | playos-installer source, Buildroot package, installer image target, disk ops; overlay factory-reset UI flow |
playos-init | Complete FactoryReset server handler (games/saves/logs erasure); installer trigger mode |
playos-runtime | playos_trusted_factory_reset() client helper |
playos-spec | Installation guide, partition layout doc |
Expected Files and Directories
playos-refdistro
src/playos-installer/
main.c # screen state machine
screens/discovery.c
screens/confirmation.c
screens/progress.c
screens/success.c
screens/error.c
disk/partition.c # libfdisk wrapper
disk/format.c # mkfs.fat (ESP), mkfs.ext4 (data/misc); system slots written from pre-built EROFS/squashfs images
disk/efi.c # EFI artifact copy, efibootmgr
br2-external/package/playos-installer/
playos-installer.mk
Config.in
br2-external/configs/playos_ally_installer_defconfig
Note: Like
src/playos-overlay/(Sprint 7),src/playos-installer/is a second deliberate in-refdistro C-source exception. When implemented, updateplayos-refdistro/AGENTS.md"What NOT to Do" to list it alongside the overlay exception.
Agent Task Breakdown
Task Status Grid
| Task ID | Task | Primary repo | Status | Notes / evidence |
|---|---|---|---|---|
| S10-T1 | Implement installer trigger mode in playos-init | playos-init | done | mount.c:421-455 parses playos.mode=install; main.c/supervisor.c spawn /usr/bin/playos-installer |
| S10-T2 | Build installer screen state machine (Raylib UI) | playos-refdistro | done | src/playos-installer/main.c DISK_DISCOVERY → CONFIRMATION → INSTALLING → SUCCESS/ERROR |
| S10-T3 | Implement disk partitioning and formatting | playos-refdistro | done | disk.c (libfdisk GPT), format.c (mkfs.fat/mkfs.ext4, squashfs slot write) |
| S10-T4 | Implement EFI artifact write and UEFI boot entry | playos-refdistro | done | efi.c writes /EFI/BOOT/BOOTX64.EFI, best-effort efibootmgr |
| S10-T5 | Implement first-boot from internal disk | playos-init | done | Existing S6 first-boot provisioning already handles empty /data (mount.c .playos-storage-version marker + seed games); no new code required |
| S10-T6 | Complete FactoryReset IPC (all five options) | playos-init, playos-runtime, playos-refdistro | done | Handler now erases games/saves/cache/config/logs; runtime trusted helper added |
| S10-T7 | Create make installer-image Buildroot target | playos-refdistro | done | playos_ally_installer_defconfig, playos-installer package, linux-installer.config, scripts/gen-installer-usb-image.sh, Makefile installer-* targets |
| S10-T8 | Installer validation (QEMU loopback + Ally) | playos-refdistro | done | QEMU loopback install SUCCESS (GPT verified); dev variant booted; physical Ally install to NVMe + reboot confirmed by user; 2026-08-19 re-install on an already-installed NVMe fixed — playos-init now skips ESP mount and pivot in installer mode, and playos-installer appends step diagnostics to /data/log/installer.log |
S10-T1 — Implement installer trigger mode in playos-init
- Parse kernel cmdline for
playos.mode=install - If absent: detect whether current root is a removable device and no
playos-datapartition exists on an internal disk; if both true, trigger installer mode - In installer mode: spawn
playos-installeras the first and only Wayland client (compositor must not spawn the shell) - Log reason for entering installer mode
Done when: playos.mode=install on the cmdline boots into the installer UI, not the shell.
S10-T2 — Build installer screen state machine (Raylib UI)
State machine: DISK_DISCOVERY → CONFIRMATION → INSTALLING → SUCCESS | ERROR
Disk discovery screen:
- Enumerate
/sys/block/for NVMe and SATA devices - Exclude the device currently hosting the root filesystem (the USB)
- Show for each candidate: model name, total size in GB, current partition count
- D-pad navigates; A selects
Confirmation screen:
WARNING: All data on <model> (<size> GB) will be erased- Hold A for 3 seconds to confirm (show countdown bar)
- B returns to disk discovery
Progress screen:
- Numbered step list with current step highlighted
- Progress bar (0–100%)
- Steps: Create GPT → Create ESP → Create system A → Create system B → Create misc → Create data → Write EFI → Populate system A → Format data → Init data → Sync
Success screen:
- "Installation complete. Remove USB and press A to restart."
- A: reboot
Error screen:
- Show failed step and error message
- Options: "Retry" (restart from disk discovery), "Shutdown"
- Never drop to a shell
Done when: all screens render, navigation works, and the full flow can be exercised in QEMU on a loopback device.
S10-T3 — Implement disk partitioning and formatting
Using libfdisk:
- Create GPT partition table on the target device
- Partition 1: EFI System Partition, FAT32, 512 MiB, label
ESP - Partition 2: system A, 4 GiB, label
playos-a(read-only EROFS/squashfs root slot) - Partition 3: system B, 4 GiB, label
playos-b(reserved empty for Sprint 11 A/B) - Partition 4:
misc, 64 MiB, labelmisc(A/B slot metadata) - Partition 5: data, ext4, remainder, label
playos-data - Write partition table to disk
Format/populate:
- ESP: call
mkfs.fat -F32 -n ESP <part1>; copyEFI/BOOT/BOOTX64.EFI - System A: write the pre-built read-only root image (EROFS, fallback squashfs) directly to
<part2>— nomkfsat install time - System B: leave empty/blank; populated by the Sprint 11 A/B update path
misc: callmkfs.ext4 -L misc <part4>(or leave raw; the slot state is tiny)- Data: call
mkfs.ext4 -L playos-data <part5>
Each step must check the return code. On any failure: transition to ERROR screen with the error code and step name.
Done when: QEMU loopback test shows the correct 5-partition GPT layout, system A boots read-only, and playos-data mounts.
S10-T4 — Implement EFI artifact write and UEFI boot entry
- Mount the ESP at
/mnt/efi - Create
/mnt/efi/EFI/BOOT/ - Copy the PlayOS EFI-stub kernel (bzImage with embedded initramfs) to
BOOTX64.EFI— no intermediate bootloader (matchesscripts/gen-ally-usb-image.sh) - Run
efibootmgr --create --disk <dev> --part 1 --label "PlayOS" --loader /EFI/BOOT/BOOTX64.EFI— log success or failure; failure is non-fatal (fallback path works) - Unmount ESP
Done when: after install, removing the USB and rebooting loads PlayOS from the NVMe EFI artifact.
S10-T5 — Implement first-boot from internal disk
On first boot from internal disk (empty /data):
playos-initdetects no.playos-storage-versionmarker- Runs the full first-boot provisioning from Sprint 6 (S6-T1/T2)
- Boots into the shell with an empty (but valid) game library
Done when: after installation and reboot, the shell shows an empty library with the correct status bar.
S10-T6 — Complete FactoryReset IPC
Extend the Sprint 6 handler (which already erases cache and config) to act on all five flags:
{
"v": 1,
"type": "FactoryReset",
"erase_games": false,
"erase_saves": false,
"erase_cache": true,
"erase_config": true,
"erase_logs": false
}
-
erase_games→ delete contents of/data/games/ -
erase_saves→ delete contents of/data/saves/(DESTRUCTIVE) -
erase_cache→ delete contents of/data/cache/ -
erase_config→ delete contents of/data/config/ -
erase_logs→ delete contents of/data/log/ -
Requires no active game; if a game is running, reply
FactoryResetErrorwith"reason": "game_running"(matchesruntime-ipc.md). -
Success replies
FactoryResetComplete; the erased directories are recreated empty. -
erase_savesis destructive: overlay must show a second confirmation before sending the IPC. -
Factory reset with
erase_games = trueanderase_saves = trueleaves the system bootable with an empty game library and a valid/datatree. -
Access: Overlay → Power menu → "Factory Reset" → option selection → confirmation.
Done when: full factory reset (all true) leaves a bootable system with an empty shell library.
S10-T7 — Create make installer-image Buildroot target
playos_ally_installer_defconfig— likeplayos_ally_defconfigbut withplayos-installerinstead ofplayos-shellas the first Wayland clientmake installer-imageproduces a bootable USB image that enters installer modemake ally-usb-imagecontinues to produce the normal system image (unchanged)- Document both targets in
README.md
Done when: make installer-image completes without errors; the produced image boots into installer mode in QEMU.
S10-T8 — Installer validation
QEMU loopback test:
make installer-image; boot in QEMU with a loopback block device as target- Verify: disk discovery shows the loopback device, confirmation screen works, partition layout correct after install, data filesystem mounts
Physical Ally test:
- Full install from USB to NVMe; remove USB; reboot; verify PlayOS starts from internal storage
- Install a game manually (
cp -rto/data/games/); reboot; verify it appears in the shell
Abort test:
- Kill installer mid-progress; verify device is in a deterministic (recoverable) state
Re-install test:
- Run installer on an already-installed Ally; verify it works
- 2026-08-19 fix: on a previously-installed NVMe, installer mode in
playos-initnow skips mounting the internalESPlabel (the disk is about to be repartitioned) and skipsplayos_pivot_to_active_slot()(so/usr/bin/playos-installerstays visible). Without this, re-install failed withInstallation Failedbecause the old ESP mount made the laterfdisk/mkfssteps fail busy. Verified end-to-end by user: re-install completed and booted from NVMe.
Done when: all four validation scenarios pass with evidence.
Implementation Guidance
Disk operations ordering
Always sync after each major step: after partition table write, after each mkfs, after EFI copy. Use fsync() on file descriptors and sync() system call before unmounting.
Never fork without exec
Use posix_spawn or execve for mkfs.fat, mkfs.ext4, efibootmgr. Do not use system().
libfdisk over shell
The installer runs as root inside the initramfs. Use libfdisk directly — do not depend on fdisk or parted binaries being present.
Verification and Evidence
| Evidence | How it is produced |
|---|---|
| Partition layout | fdisk -l <device> after QEMU loopback install |
| Filesystem types | blkid output showing FAT32 (ESP), EROFS/squashfs (system slots), ext4 (data/misc) labels |
| NVMe boot | ROG Ally boot without USB, shell visible |
| Factory reset | ls /data/games/ empty after full reset; system boots |
| Installer error screen | Write-protect target in QEMU; verify error screen appears |
Acceptance Criteria
- Installer boots from USB and shows disk discovery screen
- Only internal NVMe disks listed; USB boot device excluded
- Disk model, size, and partition count shown correctly
- Confirmation requires holding A for 3 seconds (no accidental erase)
- B on confirmation returns to disk selection
- Installation completes on clean NVMe without error
- Progress bar reaches 100% and success screen appears
- After USB removal and reboot: PlayOS boots from NVMe
- Empty game library shown on first boot from internal disk
-
File written to
/data/persists after restart - Manually installed game appears in shell after restart
- Full factory reset leaves system bootable with empty valid data partition
- Error screen shown if installation fails at any step (no terminal drop)
-
make installer-imageproduces a bootable installer USB image - CI passes (installer compiles; disk operations tested on QEMU loopback)
Handoff to Sprint 11
Sprint 11 may assume:
- The installer creates the full 5-partition layout (ESP, system A/B,
misc, data) - System A is populated with the read-only EROFS/squashfs root and boots; system B is reserved empty;
miscexists playos-initboots from the ESP EFI artifact (BOOTX64.EFI) and provisions/data/dataprovisioning is stable and tested- Sprint 11 implements A/B update/rollback logic and dm-verity on top of the existing layout; the installer does not repartition
Exit Gate
A PlayOS USB installer image successfully installs PlayOS to the ROG Ally internal NVMe. After removal of the USB drive, the Ally boots PlayOS from internal storage and shows the shell.
Previous: Sprint 9.5 | Next: Sprint 11