feat(installer): create provisioning disk
This commit is contained in:
parent
41977eaf13
commit
6ecb5a83d6
124
apis/libinstallerdisk.lua
Normal file
124
apis/libinstallerdisk.lua
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
local DEFAULT_INSTALLER_LABEL = 'TrapOS Installer';
|
||||||
|
local DEFAULT_INSTALL_URL = 'https://trapos.trapcloud.fr/install';
|
||||||
|
local DEFAULT_SETTINGS_SOURCE_PATH = '/.settings';
|
||||||
|
|
||||||
|
local function writeFile(path, body)
|
||||||
|
local file = fs.open(path, 'w');
|
||||||
|
if not file then return false; end
|
||||||
|
file.write(body);
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function createInstallerDisk(opts)
|
||||||
|
opts = opts or {};
|
||||||
|
local peripheralApi = opts.peripheral or peripheral;
|
||||||
|
local settingsApi = opts.settings or settings;
|
||||||
|
local settingsSourcePath = opts.settingsSourcePath or DEFAULT_SETTINGS_SOURCE_PATH;
|
||||||
|
local installerLabel = opts.installerLabel or DEFAULT_INSTALLER_LABEL;
|
||||||
|
local installUrl = opts.installUrl or DEFAULT_INSTALL_URL;
|
||||||
|
|
||||||
|
local api = {};
|
||||||
|
|
||||||
|
function api.buildAutorun()
|
||||||
|
return '-- TrapOS installer disk autorun (generated; do not edit)\n'
|
||||||
|
.. "local INSTALLER_LABEL = '" .. installerLabel .. "';\n"
|
||||||
|
.. "local INSTALL_URL = '" .. installUrl .. "';\n"
|
||||||
|
.. '\n'
|
||||||
|
.. "if fs.exists('/trapos') then return; end\n"
|
||||||
|
.. '\n'
|
||||||
|
.. 'local function installerMount()\n'
|
||||||
|
.. ' for _, name in ipairs(peripheral.getNames()) do\n'
|
||||||
|
.. " if peripheral.getType(name) == 'drive' then\n"
|
||||||
|
.. ' local drive = peripheral.wrap(name);\n'
|
||||||
|
.. ' if drive.isDiskPresent() and drive.getDiskLabel() == INSTALLER_LABEL then\n'
|
||||||
|
.. ' return drive.getMountPath();\n'
|
||||||
|
.. ' end\n'
|
||||||
|
.. ' end\n'
|
||||||
|
.. ' end\n'
|
||||||
|
.. 'end\n'
|
||||||
|
.. '\n'
|
||||||
|
.. 'local mount = installerMount();\n'
|
||||||
|
.. 'if mount then\n'
|
||||||
|
.. " local src = fs.combine(mount, '.settings');\n"
|
||||||
|
.. ' if fs.exists(src) then\n'
|
||||||
|
.. " fs.delete('/.settings');\n"
|
||||||
|
.. " fs.copy(src, '/.settings');\n"
|
||||||
|
.. ' settings.load();\n'
|
||||||
|
.. ' end\n'
|
||||||
|
.. 'end\n'
|
||||||
|
.. '\n'
|
||||||
|
.. "print('TrapOS Installer: installing TrapOS...');\n"
|
||||||
|
.. "shell.run('wget', 'run', INSTALL_URL);\n";
|
||||||
|
end
|
||||||
|
|
||||||
|
function api.findTargetDrive()
|
||||||
|
local names = peripheralApi.getNames and peripheralApi.getNames() or {};
|
||||||
|
local presentCount = 0;
|
||||||
|
local nonEmptyCount = 0;
|
||||||
|
local empty = {};
|
||||||
|
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
if peripheralApi.getType(name) == 'drive' then
|
||||||
|
local drive = peripheralApi.wrap(name);
|
||||||
|
if drive.isDiskPresent() and drive.hasData() then
|
||||||
|
presentCount = presentCount + 1;
|
||||||
|
local mountPath = drive.getMountPath();
|
||||||
|
if mountPath and #fs.list(mountPath) == 0 then
|
||||||
|
empty[#empty + 1] = { name = name, mountPath = mountPath, drive = drive };
|
||||||
|
else
|
||||||
|
nonEmptyCount = nonEmptyCount + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #empty > 1 then
|
||||||
|
return false, 'multiple blank disks found, remove all but one';
|
||||||
|
end
|
||||||
|
if #empty == 1 then
|
||||||
|
return true, empty[1];
|
||||||
|
end
|
||||||
|
if presentCount > 0 or nonEmptyCount > 0 then
|
||||||
|
return false, 'disk not empty; insert a blank floppy';
|
||||||
|
end
|
||||||
|
return false, 'insert a blank floppy';
|
||||||
|
end
|
||||||
|
|
||||||
|
function api.create()
|
||||||
|
local ok, target = api.findTargetDrive();
|
||||||
|
if not ok then return false, target; end
|
||||||
|
|
||||||
|
target.drive.setDiskLabel(installerLabel);
|
||||||
|
|
||||||
|
local result = {
|
||||||
|
name = target.name,
|
||||||
|
mountPath = target.mountPath,
|
||||||
|
label = installerLabel,
|
||||||
|
settingsCopied = false,
|
||||||
|
settingsWarning = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if fs.exists(settingsSourcePath) then
|
||||||
|
settingsApi.save();
|
||||||
|
local dest = fs.combine(target.mountPath, '.settings');
|
||||||
|
fs.delete(dest);
|
||||||
|
fs.copy(settingsSourcePath, dest);
|
||||||
|
result.settingsCopied = true;
|
||||||
|
else
|
||||||
|
result.settingsWarning = true;
|
||||||
|
end
|
||||||
|
|
||||||
|
local startupPath = fs.combine(target.mountPath, 'startup.lua');
|
||||||
|
fs.delete(startupPath);
|
||||||
|
if not writeFile(startupPath, api.buildAutorun()) then
|
||||||
|
return false, 'failed to write ' .. startupPath;
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, result;
|
||||||
|
end
|
||||||
|
|
||||||
|
return api;
|
||||||
|
end
|
||||||
|
|
||||||
|
return createInstallerDisk;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"packages": {
|
"packages": {
|
||||||
"trapos-core": "0.6.3",
|
"trapos-core": "0.7.0",
|
||||||
"trapos-test": "0.2.1",
|
"trapos-test": "0.2.1",
|
||||||
"trapos-boot": "0.4.0",
|
"trapos-boot": "0.4.0",
|
||||||
"trapos-net": "0.3.0",
|
"trapos-net": "0.3.0",
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos-core",
|
"name": "trapos-core",
|
||||||
"version": "0.6.3",
|
"version": "0.7.0",
|
||||||
"description": "TrapOS base: package manager, event loop, upgrade and event tools",
|
"description": "TrapOS base: package manager, event loop, upgrade and event tools",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"files": [
|
"files": [
|
||||||
"apis/eventloop.lua",
|
"apis/eventloop.lua",
|
||||||
"apis/libccpm.lua",
|
"apis/libccpm.lua",
|
||||||
|
"apis/libinstallerdisk.lua",
|
||||||
"apis/libversion.lua",
|
"apis/libversion.lua",
|
||||||
"programs/ccpm.lua",
|
"programs/ccpm.lua",
|
||||||
|
"programs/trapos-create-installer-disk.lua",
|
||||||
"programs/trapos-upgrade.lua",
|
"programs/trapos-upgrade.lua",
|
||||||
"programs/trapos-postinstall.lua",
|
"programs/trapos-postinstall.lua",
|
||||||
"programs/events.lua"
|
"programs/events.lua"
|
||||||
|
|||||||
54
programs/trapos-create-installer-disk.lua
Normal file
54
programs/trapos-create-installer-disk.lua
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
local createInstallerDisk = require('/apis/libinstallerdisk');
|
||||||
|
local createVersion = require('/apis/libversion');
|
||||||
|
|
||||||
|
local function printUsage()
|
||||||
|
print('trapos-create-installer-disk usage:');
|
||||||
|
print();
|
||||||
|
print('\t\ttrapos-create-installer-disk');
|
||||||
|
print('\t\ttrapos-create-installer-disk version');
|
||||||
|
print('\t\ttrapos-create-installer-disk help');
|
||||||
|
end
|
||||||
|
|
||||||
|
local function printColored(msg, color)
|
||||||
|
if term.isColor and term.isColor() then
|
||||||
|
local previous = term.getTextColor();
|
||||||
|
term.setTextColor(color);
|
||||||
|
print(msg);
|
||||||
|
term.setTextColor(previous);
|
||||||
|
else
|
||||||
|
print(msg);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local args = table.pack(...);
|
||||||
|
local command = args[1];
|
||||||
|
|
||||||
|
if command == 'version' or command == '-version' or command == '--version' then
|
||||||
|
print('v' .. createVersion().forSelf());
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
if command == 'help' or command == '-help' or command == '--help' then
|
||||||
|
printUsage();
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
if command ~= nil and command ~= '' then
|
||||||
|
printUsage();
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, result = createInstallerDisk().create();
|
||||||
|
if not ok then
|
||||||
|
printColored(result, colors.red);
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
printColored('=> TrapOS installer disk created on ' .. result.name .. '.', colors.lime);
|
||||||
|
print('Label: ' .. result.label);
|
||||||
|
if result.settingsCopied then
|
||||||
|
print('Cloned /.settings to ' .. fs.combine(result.mountPath, '.settings'));
|
||||||
|
elseif result.settingsWarning then
|
||||||
|
printColored('Warning: /.settings not found; installer disk has no cloned settings.', colors.orange);
|
||||||
|
end
|
||||||
|
print('Insert this disk into a blank computer and reboot it.');
|
||||||
184
tests/installer-disk.lua
Normal file
184
tests/installer-disk.lua
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
local createLibTest = require('/apis/libtest');
|
||||||
|
local createInstallerDisk = require('/apis/libinstallerdisk');
|
||||||
|
|
||||||
|
local testlib = createLibTest({ ... });
|
||||||
|
|
||||||
|
local counter = 0;
|
||||||
|
|
||||||
|
local function freshDir()
|
||||||
|
counter = counter + 1;
|
||||||
|
local path = '/installer-disk-test/case-' .. counter;
|
||||||
|
fs.delete(path);
|
||||||
|
fs.makeDir(path);
|
||||||
|
return path;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readAll(path)
|
||||||
|
local file = fs.open(path, 'r');
|
||||||
|
local body = file.readAll();
|
||||||
|
file.close();
|
||||||
|
return body;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function writeFile(path, body)
|
||||||
|
local file = fs.open(path, 'w');
|
||||||
|
file.write(body);
|
||||||
|
file.close();
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fakePeripheral(drives)
|
||||||
|
local names = {};
|
||||||
|
local byName = {};
|
||||||
|
for i, drive in ipairs(drives) do
|
||||||
|
drive.name = drive.name or ('drive_' .. i);
|
||||||
|
drive.present = drive.present ~= false;
|
||||||
|
drive.data = drive.data ~= false;
|
||||||
|
names[#names + 1] = drive.name;
|
||||||
|
byName[drive.name] = drive;
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
getNames = function()
|
||||||
|
return names;
|
||||||
|
end,
|
||||||
|
getType = function(name)
|
||||||
|
if byName[name] then return 'drive'; end
|
||||||
|
return nil;
|
||||||
|
end,
|
||||||
|
wrap = function(name)
|
||||||
|
local drive = byName[name];
|
||||||
|
return {
|
||||||
|
isDiskPresent = function()
|
||||||
|
return drive.present;
|
||||||
|
end,
|
||||||
|
hasData = function()
|
||||||
|
return drive.data;
|
||||||
|
end,
|
||||||
|
getMountPath = function()
|
||||||
|
return drive.mountPath;
|
||||||
|
end,
|
||||||
|
setDiskLabel = function(label)
|
||||||
|
drive.label = label;
|
||||||
|
end,
|
||||||
|
getDiskLabel = function()
|
||||||
|
return drive.label;
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fakeSettings()
|
||||||
|
local api = { saveCount = 0 };
|
||||||
|
function api.save()
|
||||||
|
api.saveCount = api.saveCount + 1;
|
||||||
|
end
|
||||||
|
return api;
|
||||||
|
end
|
||||||
|
|
||||||
|
testlib.test('buildAutorun copies settings before wget and no reboot', function()
|
||||||
|
local installer = createInstallerDisk();
|
||||||
|
local body = installer.buildAutorun();
|
||||||
|
local sentinel = string.find(body, "fs.exists('/trapos')", 1, true);
|
||||||
|
local label = string.find(body, 'TrapOS Installer', 1, true);
|
||||||
|
local url = string.find(body, 'https://trapos.trapcloud.fr/install', 1, true);
|
||||||
|
local copy = string.find(body, "fs.copy(src, '/.settings')", 1, true);
|
||||||
|
local wget = string.find(body, "shell.run('wget', 'run', INSTALL_URL)", 1, true);
|
||||||
|
|
||||||
|
testlib.assertTrue(sentinel);
|
||||||
|
testlib.assertTrue(label);
|
||||||
|
testlib.assertTrue(url);
|
||||||
|
testlib.assertTrue(copy);
|
||||||
|
testlib.assertTrue(wget);
|
||||||
|
testlib.assertTrue(copy < wget);
|
||||||
|
testlib.assertTrue(not string.find(body, 'os.reboot', 1, true));
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('findTargetDrive asks for blank disk with zero drives', function()
|
||||||
|
local installer = createInstallerDisk({ peripheral = fakePeripheral({}) });
|
||||||
|
local ok, err = installer.findTargetDrive();
|
||||||
|
testlib.assertTrue(not ok);
|
||||||
|
testlib.assertTrue(string.find(err, 'insert a blank', 1, true));
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('findTargetDrive rejects a non-empty floppy', function()
|
||||||
|
local disk = freshDir();
|
||||||
|
writeFile(fs.combine(disk, 'existing'), 'x');
|
||||||
|
local installer = createInstallerDisk({
|
||||||
|
peripheral = fakePeripheral({ { mountPath = disk } }),
|
||||||
|
});
|
||||||
|
local ok, err = installer.findTargetDrive();
|
||||||
|
testlib.assertTrue(not ok);
|
||||||
|
testlib.assertTrue(string.find(err, 'not empty', 1, true));
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('findTargetDrive picks one empty over one non-empty', function()
|
||||||
|
local nonEmpty = freshDir();
|
||||||
|
local empty = freshDir();
|
||||||
|
writeFile(fs.combine(nonEmpty, 'existing'), 'x');
|
||||||
|
local installer = createInstallerDisk({
|
||||||
|
peripheral = fakePeripheral({
|
||||||
|
{ name = 'left', mountPath = nonEmpty },
|
||||||
|
{ name = 'right', mountPath = empty },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
local ok, result = installer.findTargetDrive();
|
||||||
|
testlib.assertTrue(ok, tostring(result));
|
||||||
|
testlib.assertEquals(result.name, 'right');
|
||||||
|
testlib.assertEquals(result.mountPath, empty);
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('findTargetDrive rejects multiple empty disks', function()
|
||||||
|
local installer = createInstallerDisk({
|
||||||
|
peripheral = fakePeripheral({
|
||||||
|
{ mountPath = freshDir() },
|
||||||
|
{ mountPath = freshDir() },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
local ok, err = installer.findTargetDrive();
|
||||||
|
testlib.assertTrue(not ok);
|
||||||
|
testlib.assertTrue(string.find(err, 'remove all but one', 1, true));
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('create labels disk, copies settings, writes startup', function()
|
||||||
|
local disk = freshDir();
|
||||||
|
local settingsPath = fs.combine(freshDir(), '.settings');
|
||||||
|
local drive = { name = 'top', mountPath = disk };
|
||||||
|
local settingsApi = fakeSettings();
|
||||||
|
writeFile(settingsPath, 'sandbox.url=https://example.test');
|
||||||
|
|
||||||
|
local installer = createInstallerDisk({
|
||||||
|
peripheral = fakePeripheral({ drive }),
|
||||||
|
settings = settingsApi,
|
||||||
|
settingsSourcePath = settingsPath,
|
||||||
|
});
|
||||||
|
local ok, result = installer.create();
|
||||||
|
|
||||||
|
testlib.assertTrue(ok, tostring(result));
|
||||||
|
testlib.assertEquals(drive.label, 'TrapOS Installer');
|
||||||
|
testlib.assertEquals(settingsApi.saveCount, 1);
|
||||||
|
testlib.assertEquals(readAll(fs.combine(disk, 'startup.lua')), installer.buildAutorun());
|
||||||
|
testlib.assertEquals(readAll(fs.combine(disk, '.settings')), 'sandbox.url=https://example.test');
|
||||||
|
testlib.assertTrue(result.settingsCopied);
|
||||||
|
testlib.assertTrue(not result.settingsWarning);
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('create warns when settings source is missing', function()
|
||||||
|
local disk = freshDir();
|
||||||
|
local settingsApi = fakeSettings();
|
||||||
|
local installer = createInstallerDisk({
|
||||||
|
peripheral = fakePeripheral({ { mountPath = disk } }),
|
||||||
|
settings = settingsApi,
|
||||||
|
settingsSourcePath = fs.combine(freshDir(), '.settings'),
|
||||||
|
});
|
||||||
|
local ok, result = installer.create();
|
||||||
|
|
||||||
|
testlib.assertTrue(ok, tostring(result));
|
||||||
|
testlib.assertEquals(settingsApi.saveCount, 0);
|
||||||
|
testlib.assertTrue(not fs.exists(fs.combine(disk, '.settings')));
|
||||||
|
testlib.assertTrue(not result.settingsCopied);
|
||||||
|
testlib.assertTrue(result.settingsWarning);
|
||||||
|
testlib.assertEquals(readAll(fs.combine(disk, 'startup.lua')), installer.buildAutorun());
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.run();
|
||||||
Loading…
Reference in New Issue
Block a user