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

8.2 KiB

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)

-- 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):

-- 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.30.7.0.
  • packages/index.json: mirror trapos-core0.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.