This document explains the Purple Computer build and boot architecture.
There are two completely separate systems involved:
| USB (Live Boot) | Installed System | |
|---|---|---|
| What is it? | Full Purple Computer running from USB | Permanent OS on laptop |
| Where does it live? | USB stick | Laptop's internal disk |
| When is it used? | Immediately, at any time | Every day, after installation |
| What happens to it? | Can be reused on any computer | Stays forever |
| Does it touch the disk? | No, internal disk is never mounted | It IS the internal disk |
| Is it Ubuntu? | Yes (Ubuntu 24.04 via casper live boot) | Yes (Ubuntu 24.04 LTS) |
These are not the same system. They share the same root filesystem (built once by debootstrap), but are packaged differently: squashfs for live boot, raw disk image for install.
The USB serves two purposes:
- Live boot (default): Boot directly into Purple Computer from USB. No installation, no disk writes, no waiting. The child plays immediately.
- Install (optional): A parent can select "Install Purple Computer" from the GRUB menu to write the system to the internal disk permanently.
Parent plugs in USB, presses boot key
GRUB menu (5s auto-boot):
> "Purple Computer" (default) → live boot, no disk writes
"Install Purple Computer" → install to internal disk
"Boot from next volume" → skip USB
"UEFI Firmware Settings" → BIOS/UEFI
┌─────────────────────────────────────────────────────────┐
│ LIVE BOOT PATH (default) │
│ │
│ Kernel boots with: boot=casper quiet ... │
│ (NO purple.install=1) │
│ ↓ │
│ casper mounts OUR squashfs + overlayfs │
│ ↓ │
│ Initramfs hook: no purple.install=1 → does nothing │
│ ↓ │
│ systemd starts → getty@tty1 auto-login as 'purple' │
│ ↓ │
│ .bashrc → startx → Alacritty → Purple TUI │
│ ↓ │
│ Child plays. Internal disk never touched. │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ INSTALL PATH (parent selects from GRUB menu) │
│ │
│ Kernel boots with: boot=casper ... purple.install=1 │
│ ↓ │
│ casper mounts squashfs + overlayfs (same as live boot) │
│ ↓ │
│ Gate 1: hook finds purple.install=1 │
│ → Masks getty@tty1 (prevents auto-login/X11/Purple) │
│ → Writes purple-confirm.service to /root/etc/systemd/ │
│ → Writes scripts to /run/purple/ │
│ ↓ │
│ Gate 2: purple-confirm.service on tty1 │
│ "This will erase all data. Press ENTER to continue." │
│ ↓ │
│ install.sh writes golden image to internal disk │
│ ↓ │
│ Reboot. USB can be removed. │
└─────────────────────────────────────────────────────────┘
Appliance = does one job automatically, no configuration, no choices
The USB is an appliance (in live boot mode):
- Boot from USB
- 5-second auto-boot to Purple Computer
- No menus, no prompts, no configuration
- Child plays immediately
- USB can be removed after boot (see "USB Safe Removal" below)
The installed system is NOT an appliance:
- Normal Ubuntu 24.04 system
- Has apt, systemd, X11
- Receives updates from Ubuntu's servers
- Auto-logins to Purple TUI application
Installation requires passing two independent safety gates:
| Gate | When | What | Purpose |
|---|---|---|---|
| Gate 1 | Initramfs (casper-bottom) | Check purple.install=1 in cmdline |
Design-time arming |
| Gate 2 | Userspace (systemd) | Show confirmation, require ENTER | Runtime user consent |
Arming != Asking user. Gate 1 is set by the GRUB menu selection. Gate 2 requires explicit human action.
Key insight: The installer ONLY runs if:
purple.install=1was in kernel cmdline (Gate 1, selected from GRUB menu)- User pressed ENTER on confirmation screen (Gate 2)
In live boot mode, purple.install=1 is absent, so the hook is a no-op.
| Surface | What we do |
|---|---|
| Squashfs | Replace Ubuntu Server's squashfs with our Purple Computer squashfs |
| GRUB config | Live boot default, install as menu option |
| Initramfs | Add one hook script to /scripts/casper-bottom/ |
| ISO filesystem | Add /purple/ directory with golden image payload |
| Component | Location | Why |
|---|---|---|
| Kernel | /casper/vmlinuz |
Hardware compatibility |
| Shim/GRUB binaries | EFI partition | Secure Boot chain |
| Casper internals | Inside initramfs | Live boot plumbing |
| Boot configuration | EFI/MBR boot records | Boot on all hardware |
We never modify the kernel or casper's own scripts. The shim, GRUB, and MOK Manager binaries in the ISO's EFI partition are replaced at build time with the latest Ubuntu signed versions to avoid SBAT revocation on machines with updated firmware.
After booting from USB, the system caches the entire squashfs filesystem into RAM so the USB drive can be physically removed.
How it works:
The live boot uses casper's overlayfs: the squashfs (read-only, on USB) is the lower layer, and a tmpfs (writable, in RAM) is the upper layer. Normally this means the USB must stay inserted because reads go to the squashfs on the USB.
At boot, a background process in xinitrc reads the entire squashfs file, which populates the kernel's page cache (RAM). Once every block has been read, all future filesystem reads are served from RAM, not from the USB. The USB is no longer needed.
User-facing indicator:
The TUI title bar shows a blinking USB icon while caching is in progress. When caching completes, the icon changes to a green eject symbol (⏏), telling the parent it's safe to remove the USB drive.
Why not toram? Casper supports a toram kernel parameter that copies the squashfs into RAM before booting. This blocks the entire boot until the copy finishes (30-90 seconds on USB 2.0). Our approach lets the child start playing immediately while caching happens in the background.
Why we disable casper-md5check.service: Ubuntu's casper includes an integrity check that reads the entire squashfs at boot to verify its MD5 checksum. We mask this service because: (1) if the squashfs is corrupted, the system is already broken since casper mounts it as the root filesystem, so corrupted files would cause crashes or missing files whether or not the check ran, (2) the check only logs a warning and proceeds with the corrupted squashfs anyway, it doesn't prevent boot or repair anything, and (3) reading the full squashfs at boot adds 30-90 seconds on USB.
Note on install: The "Install Purple Computer" option in the parent menu requires the USB to still be inserted, since the golden image payload (purple/purple-os.img.zst) is on the USB and is not part of the squashfs cache. If the USB has been removed, the install cannot proceed.
The same debootstrap root filesystem becomes both:
- Squashfs (for live boot):
filesystem.squashfs - Golden image (for install):
purple-os.img.zst
debootstrap → full Purple root filesystem
├── mksquashfs → filesystem.squashfs (live boot)
└── dd + zstd → purple-os.img.zst (install to disk)
- Create disk image, partition (GPT: ESP + root)
- Debootstrap Ubuntu 24.04 minimal
- Install packages (X11, Alacritty, Python, Purple TUI, etc.)
- Configure system (auto-login, power management, etc.)
- Create squashfs from the mounted root (
mksquashfs) - Unmount, compress disk image (
zstd)
- Download Ubuntu Server 24.04 ISO
- Extract ISO contents
- Replace squashfs with our Purple Computer squashfs
- Modify initramfs (add install hook to casper-bottom)
- Add golden image payload to
/purple/ - Update GRUB config (live boot default)
- Rebuild ISO with xorriso
USB stick contains:
├── Signed boot stack (latest shim, GRUB, mmx64.efi, Ubuntu kernel)
├── Modified initramfs (with install hook)
├── casper/
│ ├── filesystem.squashfs ← Purple Computer root filesystem
│ └── filesystem.size
├── boot/grub/grub.cfg ← Live boot default menu
└── purple/
├── purple-os.img.zst ← Golden image (for install only)
├── install.sh
└── purple-confirm.sh
Internal disk contains:
├── Standard Ubuntu 24.04 LTS
├── Linux kernel (Ubuntu's linux-image-generic)
├── GRUB bootloader
├── X11 + Alacritty terminal
├── Purple TUI application
└── Everything needed to run Purple Computer
The hook script /scripts/casper-bottom/80_purple_installer:
- Checks for
purple.install=1in kernel cmdline - If NOT found: exits immediately (live boot continues normally)
- If found: checks for payload at
/root/cdrom/purple/ - If payload found: writes runtime artifacts, masks getty@tty1
- If no payload: exits, live boot continues
The systemd service purple-confirm.service:
- Only runs if
/run/purple/armedexists - Shows large, clear warning about data erasure
- Waits for explicit user input (ENTER to proceed, ESC to cancel)
- Has timeout (5 minutes) to prevent stuck systems
What the confirmation screen shows:
╔══════════════════════════════════════════╗
║ WARNING: DATA LOSS AHEAD ║
╚══════════════════════════════════════════╝
This will ERASE ALL DATA on this computer's internal drive.
Everything currently on the hard drive will be permanently deleted.
This action CANNOT be undone.
Press ENTER to install Purple Computer
Press ESC to cancel and reboot
Every failure mode results in safe state (live boot, no installation):
| Failure | Gate | Result |
|---|---|---|
No purple.install=1 |
1 | Live boot (normal) |
| No payload found | 1 | Live boot (normal) |
No /run/purple/armed |
2 | Service doesn't run |
| User presses ESC | 2 | Reboot, no install |
| Input timeout (5 min) | 2 | Reboot, no install |
| Keyboard disconnected | 2 | Show error, reboot |
| Scenario | Gate 1 | Gate 2 | Result |
|---|---|---|---|
| Live boot (default) | SKIP | N/A | Purple Computer from USB |
| Normal install | PASS | PASS (ENTER) | Installs to disk |
| User cancels | PASS | FAIL (ESC) | Reboots |
| Broken payload | FAIL | N/A | Live boot |
| USB removed mid-boot | FAIL | N/A | Live boot |
| Keyboard unplugged | PASS | FAIL (error) | Reboots |
| Input timeout | PASS | FAIL (timeout) | Reboots |
Hard-won lessons from debugging the casper-bottom hook:
| What | Write to | Why |
|---|---|---|
| Systemd units | /root/etc/systemd/system/ |
Persists on root filesystem, becomes /etc/systemd/system/ |
| Runtime scripts | /run/purple/ |
The /run tmpfs is moved into new root |
| Marker files | /run/purple/armed |
Same reason: use /run, not /root/run |
| NOT | /root/run/... |
Gets shadowed when /run tmpfs is moved on top |
Casper-bottom scripts are NOT auto-discovered. The file /scripts/casper-bottom/ORDER explicitly lists which scripts run and in what order. If your script isn't in ORDER, it will be silently ignored.
- Add verbose logging: Use
echo "[PREFIX] message" >/dev/consoleto see output during boot - Check serial console: VM serial logs capture boot messages
- Test live boot:
sudo ./test-boot.sh(default mode) - Test install:
sudo ./test-boot.sh --mode install - Interactive:
sudo ./test-boot.sh --interactiveto see the boot screen
| Term | Meaning |
|---|---|
| Golden image | Pre-built Ubuntu system, created with debootstrap, compressed as purple-os.img.zst |
| Squashfs | Compressed read-only filesystem used by casper for live boot |
| Initramfs | Early boot filesystem loaded by kernel, contains scripts that run before real root |
| Casper | Ubuntu's scripts for live boot: mounts squashfs, sets up overlayfs |
| Hook script | Our script in /scripts/casper-bottom/ that checks for arming and writes runtime artifacts |
| Live boot | Running Purple Computer directly from USB via casper + squashfs |
┌─────────────────────────────────────────────────────────────────┐
│ THE KEY INSIGHT │
├─────────────────────────────────────────────────────────────────┤
│ │
│ The USB boots into Purple Computer by default. │
│ No installation, no waiting, no disk writes. │
│ │
│ We replace Ubuntu's squashfs with our own (same root │
│ filesystem that becomes the golden image). casper handles │
│ all the live boot plumbing. │
│ │
│ Installation is optional, accessed via GRUB menu: │
│ - Gate 1 (initramfs hook): checks purple.install=1 │
│ - Gate 2 (systemd service): requires ENTER to proceed │
│ │
│ Same root filesystem, two packages: │
│ squashfs (live boot) + disk image (install) │
│ │
└─────────────────────────────────────────────────────────────────┘