diff --git a/plans/dual-boot.md b/plans/dual-boot.md new file mode 100644 index 0000000..15b9f45 --- /dev/null +++ b/plans/dual-boot.md @@ -0,0 +1,158 @@ +# Dual-Boot Installation Support for Omarchy ISO + +## Context + +Currently, the Omarchy installer wipes the entire selected disk (`"wipe": true`) and creates two partitions: a 2GB FAT32 ESP and a LUKS-encrypted Btrfs root. This means users must dedicate an entire disk to Omarchy. Adding dual-boot support lets users install alongside an existing OS (typically Windows) while keeping the Omarchy partition encrypted. + +## Core Approach: `pre_mounted_config` + +**Do our own partitioning, then hand off to archinstall for package installation only.** + +archinstall's `manual_partitioning` mode has a long history of bugs with existing partitions and LUKS (archinstall issues #998, #2072, #2444). Instead, we: + +1. Detect existing partitions and free space ourselves (with `sgdisk`/`parted`) +2. Create the LUKS partition and Btrfs subvolumes ourselves (with `cryptsetup`/`mkfs.btrfs`) +3. Mount everything at `/mnt/archinstall` +4. Tell archinstall to use `"config_type": "pre_mounted_config"` — it installs packages into our pre-mounted tree and does zero disk operations +5. Handle encryption config (crypttab, mkinitcpio hooks, bootloader cmdline) in post-install + +The existing full-disk path stays completely unchanged. Dual-boot is a parallel code path. + +## Implementation Guardrails + +- **Run target changes in chroot** — all target-system commands (`mkinitcpio -P`, `limine-scan`, bootloader config updates) must run via `arch-chroot "$INSTALL_ROOT" ...` +- **Edit target files under `$INSTALL_ROOT` only** — write `mkinitcpio` hook config, `limine.conf`, `crypttab`, and related files in `$INSTALL_ROOT/etc` and `$INSTALL_ROOT/boot` (never in the live environment root) +- **Settle kernel partition state** — after creating the new partition, run `partprobe "$DISK"` and `udevadm settle` before `cryptsetup` to avoid device-node races +- **Deterministic fstab** — always generate `/etc/fstab` for dual-boot with overwrite semantics (e.g., `genfstab -U "$INSTALL_ROOT" > "$INSTALL_ROOT/etc/fstab"`) to avoid duplicate entries on retries +- **Hook compatibility** — when updating mkinitcpio hooks, add `encrypt` for busybox initramfs or `sd-encrypt` for systemd initramfs; never assume one universally +- **Safe rollback on failure** — on any dual-boot setup failure: unmount mounts, close LUKS mapping, and surface recovery guidance; optionally delete the just-created partition only if it was created in this run + +## Scope Constraints + +- **UEFI/GPT only** — dual-boot on MBR/BIOS is too fragile; those users get full-disk only +- **No auto-shrinking** — users must free up space from their existing OS first (e.g., Windows Disk Management). Too risky to shrink partitions automatically +- **Minimum 20GB free space** required for Omarchy + +## User Flow + +``` +Select disk → Detect layout → + + If disk has existing OS + ESP + ≥20GB free space: + "How would you like to install?" + → "Install alongside Windows (use 85GB free space)" + → "Erase entire disk" + + If disk is empty or no free space: + Existing "Confirm overwriting" flow (unchanged) +``` + +## File Changes + +### 1. `configs/airootfs/root/configurator` + +**Add after disk selection (line 233), before config generation:** + +- `detect_disk_layout()` — examines selected disk for: + - GPT partition table (required for dual-boot) + - Existing ESP partition (via `lsblk` PARTTYPE matching EFI GUID) + - Free unallocated space (via `sgdisk -F`/`-E` or `parted print free`) + - OS detection for installer UX warnings/prompts (not boot correctness): mount ESP read-only, check common vendor paths like `EFI/Microsoft`, `EFI/systemd`, `EFI/ubuntu`, `EFI/fedora`, `EFI/GRUB` + +- `install_mode_form()` — shows gum choose with dual-boot vs erase options when conditions are met. Sets `DUAL_BOOT=true/false`. + +**Generate different JSON when `DUAL_BOOT=true`:** + +```json +{ + "disk_config": { + "config_type": "pre_mounted_config", + "mountpoint": "/mnt/archinstall" + } +} +``` + +No `device_modifications`, no `disk_encryption`, no `btrfs_options` — we handle all of that ourselves. Everything else (hostname, kernels, packages, etc.) stays the same. + +**Write `install_mode.sh`** alongside the other config files: + +```bash +DUAL_BOOT=true +DISK=/dev/nvme0n1 +ESP_PARTITION=/dev/nvme0n1p1 +``` + +### 2. `configs/airootfs/root/.automated_script.sh` + +**Source `install_mode.sh`** in `run_configurator()` if it exists. + +**Add `setup_dual_boot_partitions()`** — runs before archinstall when `DUAL_BOOT=true`: + +1. Select the largest contiguous free region and run `sgdisk` to create new partition there (type 8309 = Linux LUKS) +2. `partprobe "$DISK"` + `udevadm settle` so new partition node is present +3. `cryptsetup luksFormat` + `luksOpen` with user's password +4. `mkfs.btrfs` on `/dev/mapper/omarchy_root` +5. Create Btrfs subvolumes: `@`, `@home`, `@log`, `@pkg` +6. Mount subvolumes at `/mnt/archinstall` with `compress=zstd` +7. Mount existing ESP (untouched) at `/mnt/archinstall/boot` +8. Generate target `/etc/fstab` from mounted filesystems before post-install steps + +**Add `configure_dual_boot_post_install()`** — runs after archinstall: + +1. Write `/etc/crypttab` entry using LUKS UUID/PARTUUID (stable identifier), e.g. `omarchy_root UUID= none luks,discard` +2. Update mkinitcpio hooks before `filesystems`: `encrypt` (busybox) or `sd-encrypt` (systemd) +3. Regenerate initramfs with `arch-chroot "$INSTALL_ROOT" mkinitcpio -P` +4. Configure Limine kernel cmdline to match initramfs mode: + - busybox `encrypt`: `cryptdevice=UUID=:omarchy_root root=/dev/mapper/omarchy_root` + - systemd `sd-encrypt`: `rd.luks.name==omarchy_root root=/dev/mapper/omarchy_root` (or equivalent `rd.luks.uuid=` form) +5. Run `arch-chroot "$INSTALL_ROOT" limine-scan` to detect other OS boot entries (Windows, etc.) +6. Preserve current firmware default boot order unless user explicitly chooses to make Limine first (capture and restore `BootOrder` if `efibootmgr` side effects reorder entries) + +**Use `$INSTALL_ROOT` variable** — defaults to `/mnt` for full-disk, `/mnt/archinstall` for dual-boot. Post-install steps (offline mirror bind-mount, sudoers, omarchy copy) reference this instead of hardcoded `/mnt`. + +### 3. `builder/build-iso.sh` + `builder/archinstall.packages` + +Add `parted` to packages available in the live ISO (needed for free space detection). + +## Bootloader Coexistence + +Limine installs to `ESP/EFI/arch-limine/` and registers via `efibootmgr`. This sits alongside: +- Windows Boot Manager at `ESP/EFI/Microsoft/Boot/bootmgfw.efi` +- Any other bootloader in its own ESP subdirectory + +`limine-scan` auto-detects other UEFI boot entries and adds them to `limine.conf`. Users can choose OS at boot. + +**Boot order policy**: creating a new EFI entry must not silently change the current default boot target. Keep existing firmware boot order by default and only promote Limine when user opts in. + +Implementation note: when registering Limine with `efibootmgr`, do not pass reorder flags by default; if firmware still changes order, explicitly restore prior `BootOrder` unless the user opted to promote Limine. + +**ESP size note**: Windows typically creates a 100-260MB ESP. This is enough for Limine + kernel + initramfs, but Snapper boot snapshots (which copy kernels to ESP) will be limited or disabled. + +**ESP free-space threshold**: +- If ESP has `<100MB` free at install time: show a strong warning (do not block install) +- If ESP has `<64MB` free: hide dual-boot option and require full-disk install (or user frees space first) + +**BitLocker warning**: If Windows is detected, show a warning about having the BitLocker recovery key ready, since modifying ESP/UEFI entries can trigger recovery. + +## Edge Cases + +| Scenario | Behavior | +|----------|----------| +| Disk is empty | Full-disk install only (current behavior) | +| Existing OS but no free space | Full-disk install only, with note to free space first | +| Free space < 20GB | Dual-boot option not shown | +| MBR/BIOS disk | Dual-boot option not shown | +| ESP < 100MB free after mount | Warn user; continue allowed | +| ESP < 64MB free after mount | Dual-boot option not shown | +| Multiple free regions | Use largest contiguous region | +| Partitioning/setup failure mid-run | Cleanup mounts/LUKS and abort safely | + +## Verification + +1. **Regression**: Full-disk install in VM with empty disk — must work identically to today +2. **Dual-boot with Windows**: VM with Windows installed + 40GB free space — detect Windows, install alongside, both OSes boot +3. **Dual-boot with Linux**: VM with another Linux + free space — similar test +4. **No free space**: VM with fully partitioned disk — dual-boot option should not appear +5. **Encryption**: After dual-boot install, verify LUKS unlock prompt at boot and encrypted root +6. **Initramfs mode**: Validate both busybox and systemd hook paths boot correctly (`encrypt` vs `sd-encrypt`) +7. **Boot order**: Verify existing default UEFI entry remains unchanged unless user opted to promote Limine diff --git a/plans/oem-install.md b/plans/oem-install.md new file mode 100644 index 0000000..af4b36a --- /dev/null +++ b/plans/oem-install.md @@ -0,0 +1,210 @@ +# OEM Install + First-Boot Setup Plan + +## Goal + +Create a second ISO flavor for OEM preloading that is installed to internal storage at factory time, then boots into customer setup on first power-on (no external media required), and ends in a fully encrypted Omarchy install. + +## Product Requirements + +- Factory can preload one image to internal disk. +- Customer first boot launches setup flow automatically (already works: `.automated_script.sh` runs configurator on tty1). +- Customer chooses credentials/settings. +- Final installed OS uses full-disk encryption (LUKS) with customer password (already works: configurator collects password and passes it to archinstall as `encryption_password`). +- Existing consumer ISO behavior remains unchanged. + +--- + +## Chosen Architecture + +### Bootstrapping model +- OEM devices ship with an OEM bootstrap ISO image written to internal disk. +- On first boot, `copytoram=y` kernel arg loads the live environment into RAM, freeing the internal disk. +- Bootstrap environment launches existing configurator + installer. +- Installer performs a fresh install to internal disk with LUKS encryption enabled. +- Bootstrap environment is overwritten during install. + +### Recovery model: zero-recovery +- No permanent recovery partition. +- Simplest and strongest security story. +- Recovery requires external reinstall media. + +--- + +## Non-Goals + +- No on-device recovery workflow. +- No changes to normal installer UX outside explicit OEM mode checks and warnings. +- No factory credential provisioning. + +--- + +## Repo Implementation Plan + +## 1) Add OEM build mode + +### Files +- `bin/omarchy-iso-make` +- `builder/build-iso.sh` + +### Changes +- Add CLI flag `--oem` to `bin/omarchy-iso-make`. +- Pass `OMARCHY_MODE=oem` into Docker build env (default `normal`). +- In `builder/build-iso.sh`, write mode marker `/root/omarchy_mode` containing `oem` or `normal` (same pattern as `omarchy_mirror`). +- When `OMARCHY_MODE=oem`, inject `copytoram=y` into all bootloader entry points before `mkarchiso` runs: + - `grub/grub.cfg`: append to `linux` cmdline entries. + - `efiboot/loader/entries/01-archiso-x86_64-linux.conf`: append to `options` line. + - `syslinux/archiso_sys-linux.cfg`: append to `APPEND` lines. +- When `OMARCHY_MODE=oem`, override `iso_application` in `profiledef.sh` to `"Omarchy OEM Installer"`. +- Rename output ISO with `-oem` suffix (in addition to existing `-$OMARCHY_INSTALLER_REF` suffix). + +### Hardening +- Add post-injection build assertion that fails build if any expected boot config does not contain `copytoram=y`. +- Make injection idempotent (do not duplicate `copytoram=y` if already present). + +### Acceptance criteria +- `./bin/omarchy-iso-make --oem` builds successfully. +- OEM ISO boot entries contain `copytoram=y` in all supported boot paths. +- Normal build output and behavior remain unchanged. + +### Suggested verification commands +- `bsdtar -xOf out/.iso loader/entries/01-archiso-x86_64-linux.conf | rg copytoram=y` +- `bsdtar -xOf out/.iso boot/grub/grub.cfg | rg copytoram=y` +- `bsdtar -xOf out/.iso boot/syslinux/archiso_sys-linux.cfg | rg copytoram=y` + +--- + +## 2) Add OEM safety guardrails + +### Why +OEM image boots from internal disk and later wipes that same disk. Live environment must be fully in RAM before install begins. + +### File +- `configs/airootfs/root/.automated_script.sh` + +### Changes +- Before `run_configurator`, if `/root/omarchy_mode` is `oem`: + - Verify `/proc/cmdline` contains `copytoram=y`; abort with clear actionable error if missing. + - Verify memory safety using `MemAvailable` (not total memory): + - Minimum threshold: `max(6 GiB, 1.5 x ISO size + 1 GiB)`. + - Abort with clear error if below threshold. + - Verify runtime state indicates live root is in RAM (for example, expected archiso runtime mounts present and boot media not actively mounted as install target path). + +### Acceptance criteria +- OEM mode aborts safely if `copytoram=y` missing from cmdline. +- OEM mode aborts safely if available memory below threshold. +- OEM mode aborts safely if runtime state does not confirm RAM-backed live environment. + +### Error message requirements +- State exactly what failed. +- State why install is blocked. +- Provide next step (reboot using proper OEM media or use external recovery media). + +--- + +## 3) OEM disk warning + +### Why +With `copytoram=y`, archiso copies squashfs to RAM and releases boot device. Existing configurator call `findmnt -no SOURCE /run/archiso/bootmnt` may return empty, so exclusion filter is skipped and all physical disks are shown, including former boot disk. No disk selection logic change is required. + +OEM mode still needs an unmistakable warning before destructive install proceeds. + +### File +- `configs/airootfs/root/configurator` + +### Changes +- Read mode from `/root/omarchy_mode`. +- If mode is `oem`, add OEM-specific warning text before existing destructive confirmation. +- Upgrade confirmation in OEM mode to explicit typed confirmation (for example: `YES, WIPE DISK`) while preserving existing normal-mode flow. + +### Acceptance criteria +- Normal ISO behavior is unchanged. +- OEM ISO shows OEM-specific warning before disk wipe confirmation. +- OEM typed confirmation is required before destructive action. +- In VM with `copytoram=y`, former boot disk appears in disk list without exclusion-logic changes. + +--- + +## 4) Documentation + +### File +- `README.md` + +### Changes +- Document `--oem` build flag and resulting artifact naming. +- Document factory workflow: writing OEM ISO to internal disk and expected first-boot UX. +- Document failure and recovery behavior: + - Missing `copytoram=y` or low memory causes safe abort. + - Interrupted/failed install requires external reinstall media. +- Add short operator checklist for manufacturing line validation. + +--- + +## 5) Validation + +### VM test matrix +1. Build OEM ISO. +2. Write OEM ISO to virtual disk (not external media emulation). +3. Boot from virtual disk and verify: + - `/proc/cmdline` contains `copytoram=y`. + - Guardrail checks pass in healthy case. +4. Complete setup flow on first boot. +5. Install to same disk. +6. Reboot into installed OS. +7. Verify encryption: + - `lsblk` shows crypt mapping. + - `cryptsetup status` reports active LUKS device. + - Boot requires unlock passphrase. +8. Validate OEM warning + typed confirmation appears and gates destructive action. + +### Boot path coverage +- UEFI + systemd-boot path. +- UEFI + GRUB path (if supported in current profile). +- BIOS/syslinux path (if still shipped). + +### Hardware test matrix +- UEFI test on at least two hardware families (Intel + AMD). +- Confirm no external media required. +- Confirm first boot starts setup automatically. + +### Negative tests +- OEM boot without `copytoram=y` -> safe abort with clear error. +- Low `MemAvailable` -> safe abort with clear error. +- Runtime state not RAM-backed -> safe abort with clear error. +- Power interruption before installer starts -> deterministic reboot behavior. +- Power interruption during install -> recovery requires external media (documented). + +--- + +## 6) Security hardening checklist + +- No factory credentials exist; customer sets all passwords at first boot. +- Clear shell history and transient setup artifacts at end of install. +- Verify no secrets in logs shipped to customer. +- Regenerate host identity on first installed boot as needed: + - `machine-id` + - SSH host keys (if installed/enabled) +- Keep package signature verification and keyring handling intact. + +--- + +## Rollout Plan + +1. Implement OEM build flag, mode marker, boot config injection, and build assertions. +2. Implement copytoram, memory, and runtime-state guardrails in `.automated_script.sh`. +3. Add OEM warning text and typed destructive confirmation in configurator. +4. Add docs and operator checklist. +5. Run VM matrix, including negative tests. +6. Run hardware matrix. +7. Cut first OEM preproduction release for manufacturing pilot. + +--- + +## Definition of Done + +- `--oem` build exists and is documented. +- Device preloaded with OEM image boots directly to setup. +- Setup installs to internal disk without external media. +- Final installed system is LUKS-encrypted with customer-owned passphrase. +- OEM safety guardrails fail closed with actionable errors. +- Existing non-OEM ISO behavior remains unchanged. +- VM and hardware QA matrix pass.