153 lines
8.2 KiB
Markdown
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://os.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://os.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://os.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.
|