cc-libs/.plans/archived/trapos-create-installer-disk-plan.md
2026-06-15 20:47:53 +02:00

153 lines
8.2 KiB
Markdown

# Plan: `trapos-create-installer-disk` — one-shot TrapOS installer floppies
## Context
Today TrapOS is installed by typing `wget run https://trapos.trapcloud.fr/install` on each
fresh computer, then manually configuring settings (`sandbox.url`, `sandbox.password`, …) per
machine. We want a **provisioning floppy**: run one program on a configured machine to "bake"
an installer disk, then on any blank machine just insert the disk and boot — it installs the
latest TrapOS online and clones *your* `.settings` onto the new computer, hands-free.
The disk's value is **hands-free install + config cloning**, not offline file shipping.
## Resolved design decisions (from grill session)
- **Install method**: autorun installs **online** via the existing `wget run` bootstrap. No
files are bundled on the disk; the disk stays tiny and never goes stale vs the registry.
- **Disk boot file**: CC only auto-runs `startup`/`startup.lua` from a floppy at boot. We put
the autorun logic **directly in `<disk>/startup.lua`** (the name "autorun" is conceptual).
- **Ordering (copy-then-install, no reboot)**: `install-trapos.lua` ends by handing off to the
live `/startup/servers.lua` shell, which never returns. So the autorun **copies `.settings`
first**, calls `settings.load()`, *then* runs the installer (whose `trapos-postinstall`
respects the already-set `sandbox.url` and never touches `sandbox.password`, so the disk's
config wins). **No changes to `install-trapos.lua`** and **no reboot** here — the
boot/reboot rework is a separate effort.
- **Sentinel**: autorun no-ops if `/trapos` exists. To reinstall: `rm /trapos`, reinsert, boot.
- **Drive selection**: pick the **first eligible** drive = present, writable floppy that is
**empty** (`#fs.list(mountPath) == 0`). Empty is preferred over non-empty. `0` empty → error
(distinguish "disk not empty" vs "insert a blank floppy"); `2+` empty → print "multiple blank
disks found, remove all but one" and stop. No interactive insert handling.
- **Missing `/.settings` at creation**: warn + still build a valid (settings-less) installer.
When `/.settings` exists, `settings.save()` first to flush in-session values, then copy.
- **Label**: `TrapOS Installer`.
- **Code shape**: injectable factory `apis/libinstallerdisk.lua` + thin CLI
`programs/trapos-create-installer-disk.lua` + `tests/installer-disk.lua`; ship in
`trapos-core`. Mirrors `apis/libccpm.lua` + `programs/ccpm.lua` + `tests/ccpm.lua`.
## Conventions to follow
2-space indent, semicolons, `local function` (per `AGENTS.md`). `require` absolute paths.
Programs support `-version`/`--version` and `-help`/`--help` via
`require('/apis/libversion')().forSelf()`. Colored output guarded by `term.isColor()` (see
`printColored` in `programs/ccpm.lua`). Factory opts default to globals (see `createCcpm(opts)`
in `apis/libccpm.lua`).
## 1. New: `apis/libinstallerdisk.lua` (testable factory)
```lua
-- createInstallerDisk(opts) -> api
-- opts.peripheral (default global `peripheral`) -- drive discovery
-- opts.settings (default global `settings`) -- save() flush
-- opts.settingsSourcePath (default '/.settings') -- computer config to clone
-- opts.installerLabel (default 'TrapOS Installer')
-- opts.installUrl (default 'https://trapos.trapcloud.fr/install')
-- (uses real global `fs`; tests sandbox with isolated real dirs, like tests/ccpm.lua)
```
Public API:
- `api.buildAutorun()` → returns the `startup.lua` source string (pure, fully unit-testable).
- `api.findTargetDrive()``ok, { name, mountPath } | err`. Iterates
`peripheral.find('drive')`/`getNames`, keeps present writable floppies (`isDiskPresent()`
and `hasData()`), filters to empty (`#fs.list(getMountPath()) == 0`), applies the
0/1/2+ rules above with the distinct error messages.
- `api.create()``ok, result | err`. Orchestrates: find drive → `setDiskLabel('TrapOS
Installer')` → if `settingsSourcePath` exists: `settings.save()` then
`fs.copy(settingsSourcePath, mount/.settings)` (delete dest first) else set
`result.settingsWarning = true` → write `buildAutorun()` to `mount/startup.lua`.
`result = { name, mountPath, label, settingsCopied, settingsWarning }`.
`buildAutorun()` returns (note: copy-then-install, locate-own-disk-by-label, no reboot):
```lua
-- TrapOS installer disk autorun (generated; do not edit)
local INSTALLER_LABEL = 'TrapOS Installer';
local INSTALL_URL = 'https://trapos.trapcloud.fr/install';
if fs.exists('/trapos') then return; end -- already installed: leave machine alone
local function installerMount()
for _, name in ipairs(peripheral.getNames()) do
if peripheral.getType(name) == 'drive' then
local drive = peripheral.wrap(name);
if drive.isDiskPresent() and drive.getDiskLabel() == INSTALLER_LABEL then
return drive.getMountPath();
end
end
end
end
local mount = installerMount();
if mount then
local src = fs.combine(mount, '.settings');
if fs.exists(src) then
fs.delete('/.settings');
fs.copy(src, '/.settings');
settings.load();
end
end
print('TrapOS Installer: installing TrapOS...');
shell.run('wget', 'run', INSTALL_URL);
```
## 2. New: `programs/trapos-create-installer-disk.lua` (thin CLI)
- Handles `version`/`-version`/`--version` (prints `v' .. createVersion().forSelf()`) and
`help`/`-help`/`--help` (usage), mirroring `programs/trapos-upgrade.lua`.
- Calls `createInstallerDisk().create()`. On error: print message in red, `return`. On success:
print a green summary (label set, `.settings` cloned / or the missing-settings warning,
which drive), plus a hint: "Insert this disk into a blank computer and reboot it."
- No confirmation prompt (target disk is required to be empty → low risk).
## 3. New: `tests/installer-disk.lua`
Follow `tests/ccpm.lua`: `local createLibTest = require('/apis/libtest')`, isolated real-fs
dirs per case, fake `peripheral` injected. Cases:
- `buildAutorun()` contains the `/trapos` sentinel, the label, the install URL, copies
`.settings` before `wget run`, and has no `os.reboot`.
- `findTargetDrive`: zero drives → "insert a blank" error; one non-empty floppy → "not empty"
error; one empty + one non-empty → picks the empty; two empty → "remove all but one" error.
- `create`: sets label, writes `startup.lua` (== `buildAutorun()`) into the fake mount dir,
copies a temp `settingsSourcePath` into `mount/.settings`; missing source → `settingsWarning`
true and no copy. Fake `settings.save` is a recording no-op so the real `/.settings` is never
touched.
Fake `peripheral`: `getNames`/`getType`/`wrap` returning fake drives whose `getMountPath()`
points at a real temp dir (e.g. `/installer-disk-test/disk-N`) so `fs.copy`/`fs.open`/`fs.list`
operate on real fs; `isDiskPresent`/`hasData` return true; `set/getDiskLabel` store a field.
## 4. Packaging
- `packages/trapos-core/ccpm.json`: append `"apis/libinstallerdisk.lua"` and
`"programs/trapos-create-installer-disk.lua"` to `files`; bump `version` `0.6.3``0.7.0`.
- `packages/index.json`: mirror `trapos-core``0.7.0`.
## Verification
1. `just check` — luacheck clean (respect `.luacheckrc` `lua51+cc`) and lychee link check.
2. `just test` (or `runtest tests/installer-disk.lua`) — suite prints `__TRAPOS_TEST_OK__`.
3. Optional headless probe: `just trapos-exec '<lua creating a periphemu drive + calling
createInstallerDisk().create()>'` to smoke-test against an emulated drive end-to-end.
4. In-game (human): run `trapos-create-installer-disk` on a configured machine → verify label
`TrapOS Installer`, `startup.lua` + `.settings` present on the floppy. Insert into a blank
computer, reboot → TrapOS installs and `.settings` is cloned. Confirm re-boot with disk
still in is a no-op (because `/trapos` exists), and that `rm /trapos` + reboot reinstalls.
## Assumptions / caveats
- Target blank computer has `shell.allow_disk_startup` enabled (CC default `true`) and HTTP
enabled with `trapcloud.fr` reachable/allowed (CC default).
- Literal order is **copy-then-install** (not "install-then-copy"); functionally the disk's
settings still win. The pre-existing "exit the OS shell re-runs `servers.lua`" wart and any
forced reboot are **out of scope** — owned by the separate boot/reboot rework.