mirror of
https://github.com/NeoFlock/neonucleus.git
synced 2025-09-25 01:23:31 +02:00
232 lines
6.7 KiB
Lua
232 lines
6.7 KiB
Lua
local computer = require("computer")
|
|
local shell = require("shell")
|
|
local fs = require("filesystem")
|
|
|
|
local args, options = shell.parse(...)
|
|
|
|
if options.help then
|
|
io.write([[Usage: install [OPTION]...
|
|
--from=ADDR install filesystem at ADDR
|
|
default: builds list of
|
|
candidates and prompts user
|
|
--to=ADDR same as --from but for target
|
|
--fromDir=PATH install PATH from source
|
|
--root=PATH same as --fromDir but target
|
|
--toDir=PATH same as --root
|
|
-u, --update update files interactively
|
|
--label override label from .prop
|
|
--nosetlabel do not label target
|
|
--nosetboot do not use target for boot
|
|
--noreboot do not reboot after install
|
|
]])
|
|
return nil -- exit success
|
|
end
|
|
|
|
local utils_path = "/lib/core/install_utils.lua"
|
|
local utils
|
|
|
|
local rootfs = fs.get("/")
|
|
if not rootfs then
|
|
io.stderr:write("no root filesystem, aborting\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
local label = args[1]
|
|
options.label = label
|
|
|
|
local source_filter = options.from
|
|
local source_filter_dev
|
|
if source_filter then
|
|
local from_path = shell.resolve(source_filter)
|
|
if fs.isDirectory(from_path) then
|
|
source_filter_dev = fs.get(from_path)
|
|
source_filter = source_filter_dev.address
|
|
options.from = from_path
|
|
end
|
|
end
|
|
|
|
local target_filter = options.to
|
|
local target_filter_dev
|
|
if target_filter then
|
|
local to_path = shell.resolve(target_filter)
|
|
if fs.isDirectory(target_filter) then
|
|
target_filter_dev = fs.get(to_path)
|
|
target_filter = target_filter_dev.address
|
|
options.to = to_path
|
|
end
|
|
end
|
|
|
|
local sources = {}
|
|
local targets = {}
|
|
|
|
-- tmpfs is not a candidate unless it is specified
|
|
|
|
local comps = require("component").list("filesystem")
|
|
local devices = {}
|
|
|
|
-- not all mounts are components, only use components
|
|
for dev, path in fs.mounts() do
|
|
if comps[dev.address] then
|
|
local known = devices[dev]
|
|
devices[dev] = known and #known < #path and known or path
|
|
end
|
|
end
|
|
|
|
local dev_dev = fs.get("/dev")
|
|
devices[dev_dev == rootfs or dev_dev] = nil
|
|
local tmpAddress = computer.tmpAddress()
|
|
|
|
for dev, path in pairs(devices) do
|
|
local address = dev.address
|
|
local install_path = dev == target_filter_dev and options.to or path
|
|
local specified = target_filter and address:find(target_filter, 1, true) == 1
|
|
|
|
if dev.isReadOnly() then
|
|
if specified then
|
|
io.stderr:write("Cannot install to " .. options.to .. ", it is read only\n")
|
|
os.exit(1)
|
|
end
|
|
elseif
|
|
specified or
|
|
not (source_filter and address:find(source_filter, 1, true) == 1) and -- specified for source
|
|
not target_filter and
|
|
address ~= tmpAddress
|
|
then
|
|
table.insert(targets, {dev = dev, path = install_path, specified = specified})
|
|
end
|
|
end
|
|
|
|
local target = targets[1]
|
|
-- if there is only 1 target, the source selection cannot include it
|
|
if #targets == 1 then
|
|
devices[targets[1].dev] = nil
|
|
end
|
|
|
|
for dev, path in pairs(devices) do
|
|
local address = dev.address
|
|
local install_path = dev == source_filter_dev and options.from or path
|
|
local specified = source_filter and address:find(source_filter, 1, true) == 1
|
|
|
|
if
|
|
fs.list(install_path)() and
|
|
(specified or
|
|
not source_filter and address ~= tmpAddress and not (address == rootfs.address and not rootfs.isReadOnly()))
|
|
then
|
|
local prop = {}
|
|
local prop_path = path .. "/.prop"
|
|
local prop_file = fs.open(prop_path)
|
|
if prop_file then
|
|
local prop_data = prop_file:read(math.maxinteger or math.huge)
|
|
prop_file:close()
|
|
local prop_load = load("return " .. prop_data)
|
|
prop = prop_load and prop_load()
|
|
if not prop then
|
|
io.stderr:write("Ignoring " .. path .. " due to malformed prop file\n")
|
|
prop = {ignore = true}
|
|
end
|
|
end
|
|
if not prop.ignore then
|
|
if not label or label:lower() == (prop.label or dev.getLabel() or ""):lower() then
|
|
table.insert(sources, {dev = dev, path = install_path, prop = prop, specified = specified})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Ask the user to select a source
|
|
local source = sources[1]
|
|
if #sources ~= 1 then
|
|
utils = loadfile(utils_path, "bt", _G)
|
|
source = utils("select", "sources", options, sources)
|
|
end
|
|
if not source then
|
|
return
|
|
end
|
|
|
|
options = {
|
|
from = source.path .. "/",
|
|
fromDir = fs.canonical(options.fromDir or source.prop.fromDir or ""),
|
|
root = fs.canonical(options.root or options.toDir or source.prop.root or ""),
|
|
update = options.update or options.u,
|
|
label = source.prop.label or label,
|
|
setlabel = not (options.nosetlabel or options.nolabelset) and source.prop.setlabel,
|
|
setboot = not (options.nosetboot or options.noboot) and source.prop.setboot,
|
|
reboot = not options.noreboot and source.prop.reboot
|
|
}
|
|
local source_display = options.label or source.dev.getLabel() or source.path
|
|
|
|
-- Remove the source from the target options
|
|
for index, entry in ipairs(targets) do
|
|
if entry.dev == source.dev then
|
|
table.remove(targets, index)
|
|
target = targets[1]
|
|
end
|
|
end
|
|
|
|
-- Ask the user to select a target
|
|
if #targets ~= 1 then
|
|
if #sources == 1 then
|
|
io.write(source_display, " selected for install\n")
|
|
end
|
|
|
|
utils = utils or loadfile(utils_path, "bt", _G)
|
|
target = utils("select", "targets", options, targets)
|
|
end
|
|
if not target then
|
|
return
|
|
end
|
|
|
|
options.to = target.path .. "/"
|
|
|
|
local function resolveFrom(path)
|
|
return fs.concat(options.from, options.fromDir) .. "/" .. path
|
|
end
|
|
|
|
local fullTargetPath = fs.concat(options.to, options.root)
|
|
local transfer_args = {
|
|
{
|
|
{resolveFrom("."), fullTargetPath},
|
|
{
|
|
cmd = "cp",
|
|
r = true, v = true, x = true, u = options.update, i = options.update,
|
|
skip = {resolveFrom(".prop")},
|
|
}
|
|
}
|
|
}
|
|
|
|
if source.prop.noclobber and #source.prop.noclobber > 0 then
|
|
local noclobber_opts = {cmd = "cp", v = true, n = true}
|
|
for _, noclobber in ipairs(source.prop.noclobber or {}) do
|
|
local noclobberFrom = resolveFrom(noclobber)
|
|
local noclobberTo = fs.concat(fullTargetPath, noclobber)
|
|
table.insert(transfer_args[1][2].skip, noclobberFrom)
|
|
table.insert(transfer_args, {{noclobberFrom, noclobberTo}, noclobber_opts})
|
|
end
|
|
end
|
|
|
|
local special_target = ""
|
|
if #targets > 1 or target_filter or source_filter then
|
|
special_target = " to " .. transfer_args[1][1][2]
|
|
end
|
|
|
|
io.write("Install " .. source_display .. special_target .. "? [Y/n] ")
|
|
if not ((io.read() or "n") .. "y"):match("^%s*[Yy]") then
|
|
io.write("Installation cancelled\n")
|
|
os.exit()
|
|
end
|
|
|
|
local installer_path = options.from .. "/.install"
|
|
if fs.exists(installer_path) then
|
|
local installer, reason = loadfile(installer_path, "bt", setmetatable({install = options}, {__index = _G}))
|
|
if not installer then
|
|
io.stderr:write("installer failed to load: " .. tostring(reason) .. "\n")
|
|
os.exit(1)
|
|
end
|
|
os.exit(installer())
|
|
end
|
|
|
|
options.cp_args = transfer_args
|
|
options.target = target
|
|
|
|
return options
|