diff --git a/src/main/resources/assets/opencomputers/loot/openos/.osprop b/src/main/resources/assets/opencomputers/loot/openos/.osprop deleted file mode 100644 index b2dc1ceff..000000000 --- a/src/main/resources/assets/opencomputers/loot/openos/.osprop +++ /dev/null @@ -1 +0,0 @@ -return {name = "OpenOS"} diff --git a/src/main/resources/assets/opencomputers/loot/openos/.prop b/src/main/resources/assets/opencomputers/loot/openos/.prop new file mode 100644 index 000000000..3671d643d --- /dev/null +++ b/src/main/resources/assets/opencomputers/loot/openos/.prop @@ -0,0 +1 @@ +return {label = "OpenOS", reboot=true, setlabel=true, setboot=true} diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua index f805d5ef9..e5e023f90 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/cp.lua @@ -64,8 +64,15 @@ end local function recurse(fromPath, toPath, origin) local isLink, target = fs.isLink(fromPath) - if isLink and options.P then + local toIsLink, toLinkTarget = fs.isLink(toPath) + local same_path = fs.canonical(isLink and target or fromPath) == fs.canonical(toIsLink and toLinkTarget or toPath) + local toExists = fs.exists(toPath) + + if isLink and options.P and (not toExists or not same_path) then status(fromPath, toPath) + if toIsLink then + fs.remove(toPath) + end return fs.link(target, toPath) elseif fs.isDirectory(fromPath) then if not options.r then @@ -95,35 +102,34 @@ local function recurse(fromPath, toPath, origin) end return true elseif fs.exists(fromPath) then - if fs.exists(toPath) then - if fs.canonical(fromPath) == fs.canonical(toPath) then + if toExists then + if same_path then return nil, "`" .. fromPath .. "' and `" .. toPath .. "' are the same file" end - if fs.isDirectory(toPath) then - if options.i then - if not prompt("overwrite `" .. toPath .. "'?") then - return true - end - elseif options.n then - return true - else -- yes, even for -f - return nil, "cannot overwrite directory `" .. toPath .. "' with non-directory" - end - else - if options.u then - if areEqual(fromPath, toPath) then - return true - end - end - if options.i then - if not prompt("overwrite `" .. toPath .. "'?") then - return true - end - elseif options.n then - return true - end - -- else: default to overwriting + + if options.n then + return true end + + -- if target is link, we are updating the target + if toIsLink then + toPath = toLinkTarget + end + + if options.u and not fs.isDirectory(toPath) and areEqual(fromPath, toPath) then + return true + end + + if options.i then + if not prompt("overwrite `" .. toPath .. "'?") then + return true + end + end + + if fs.isDirectory(toPath) then + return nil, "cannot overwrite directory `" .. toPath .. "' with non-directory" + end + fs.remove(toPath) end status(fromPath, toPath) diff --git a/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua b/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua index 6d3b1f990..d8a00d6ab 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua +++ b/src/main/resources/assets/opencomputers/loot/openos/bin/install.lua @@ -7,12 +7,21 @@ local shell = require("shell") local tx = require("transforms") local text = require("text") -local args, options = shell.parse(...) +local lib = {} -local sources = {} -local targets = {} +lib.args, lib.options = shell.parse(...) -if options.help then +lib.sources = {} +lib.targets = {} + +lib.source_label = lib.args[1] + +lib.stdout = io.stdout +lib.stderr = io.stderr +lib.stdin = io.stdin +lib.exit = os.exit + +if lib.options.help then print([[Usage: install [OPTION]... --from=ADDR install filesystem at ADDR default: builds list of @@ -22,25 +31,57 @@ if options.help then --root=PATH same as --fromDir but target --toDir=PATH same as --root -u, --update update files interactively -The following only pertain when .osprop exists - --nolabelset do not label target - --name override label from .osprop - --noboot do not use target for boot + --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 rootfs = fs.get("/") if not rootfs then - io.stderr:write("no root filesystem, aborting\n"); - return 1 + lib.stderr:write("no root filesystem, aborting\n"); + lib.exit(1) +end + +function lib.up_deprecate(old_key, new_key) + if lib.options[new_key] == nil then + lib.options[new_key] = lib.options[old_key] + end + lib.options[old_key] = nil +end + +function lib.cleanPath(path) + if path then + local rpath = shell.resolve(path) + if fs.isDirectory(rpath) then + return fs.canonical(rpath):gsub("/+$", "") .. '/' + end + end +end + +function lib.load_options() + lib.up_deprecate('noboot', 'nosetboot') + lib.up_deprecate('nolabelset', 'nosetlabel') + lib.up_deprecate('name', 'label') + + lib.source_root = lib.cleanPath(lib.options.from) + lib.target_root = lib.cleanPath(lib.options.to) + + lib.source_dir = (lib.options.fromDir or "") .. '/.' + lib.target_dir = (lib.options.root or lib.options.toDir or "") .. "/." + + lib.update = lib.options.u or lib.options.update + + lib.source_dev = lib.source_root and fs.get(lib.source_root) + lib.target_dev = lib.target_root and fs.get(lib.target_root) end local rootAddress = rootfs.address -- if the rootfs is read only, it is probably the loot disk! -local rootException = rootAddress +lib.rootException = rootAddress if rootfs.isReadOnly() then - rootException = nil + lib.rootException = nil end -- this may be OpenOS specific, default to "" in case no /dev mount point @@ -49,72 +90,96 @@ local devfsAddress = (fs.get("/dev/") or {}).address or "" -- tmp is only valid if specified as an option local tmpAddress = computer.tmpAddress() -local fromAddress = options.from -local toAddress = options.to -local fromDir = (options.fromDir or "") .. '/.' -local root = (options.root or options.toDir or "") .. "/." -options.update = options.u or options.update - -local function cleanPath(path) - if path then - local rpath = shell.resolve(path) - if fs.isDirectory(rpath) then - return fs.canonical(rpath):gsub("/+$", "") .. '/' +function lib.load(path, env) + if fs.exists(path) then + local loader, reason = loadfile(path, "bt", setmetatable(env or {}, {__index=_G})) + if not loader then + return nil, reason end + local ok, loaded = pcall(loader) + return ok and loaded, ok or loaded end - return path end -fromAddress = cleanPath(fromAddress) -toAddress = cleanPath(toAddress) - -local function validDevice(candidate, exceptions, specified, existing) +function lib.validDevice(candidate, exceptions, specified, existing) local address = candidate.dev.address if tx.first(existing, function(e) return e.dev.address == address end) then return end - local path = candidate.path if specified then - return address:find(specified, 1, true) == 1 or specified == path + if type(specified) == "string" and address:find(specified, 1, true) == 1 or specified == candidate.dev then + return true + end else return not tx.find(exceptions, {address}) end end +function lib.relevant(candidate, path) + if not path or fs.get(path) ~= candidate.dev then + return candidate.path + end + return path +end + -- use a single for loop of all filesystems to build the list of candidates of sources and targets -for dev, path in fs.mounts() do - local candidate = {dev=dev, path=path} +function lib.load_candidates() + for dev, path in fs.mounts() do + local candidate = {dev=dev, path=path:gsub("/+$","")..'/'} - if validDevice(candidate, {devfsAddress, tmpAddress, rootException}, fromAddress, sources) then - if fromAddress or fs.list(path)() then - table.insert(sources, candidate) + if lib.validDevice(candidate, {devfsAddress, tmpAddress, lib.rootException}, lib.source_dev or lib.options.from, lib.sources) then + local root_path = lib.relevant(candidate, lib.source_root) + if (lib.options.from or fs.list(root_path)()) then -- ignore empty sources unless specified + candidate.prop = lib.load(root_path .. "/.prop") or {} + if not lib.source_label or lib.source_label:lower() == (candidate.prop.label or candidate.dev.getLabel()):lower() then + table.insert(lib.sources, candidate) + end + end + end + + -- in case candidate is valid for BOTH, we want a new table + candidate = {dev=candidate.dev, path=candidate.path} -- but not the prop + + if lib.validDevice(candidate, {devfsAddress, tmpAddress}, lib.target_dev or lib.options.to, lib.targets) then + if not dev.isReadOnly() then + table.insert(lib.targets, candidate) + elseif lib.options.to then + lib.stderr:write("Cannot install to " .. lib.options.to .. ", it is read only\n") + lib.exit(1) + return false -- in lib mode this can be hit + end end end - if validDevice(candidate, {devfsAddress, tmpAddress}, toAddress, targets) then - if not dev.isReadOnly() then - table.insert(targets, candidate) - elseif toAddress then - io.stderr:write("Cannot install to " .. toAddress .. ", it is read only\n") - return 1 + return true +end + +function lib.check_sources() + if #lib.sources == 0 then + if lib.source_label then + lib.stderr:write("No filesystem to matched given label: " .. lib.source_label .. '\n') + elseif lib.options.from then + lib.stderr:write("No such filesystem to install from: " .. lib.options.from .. '\n') + else + lib.stderr:write("Could not find and available installations\n") end + lib.exit(1) end + return true end -if fromAddress and #sources == 0 then - io.stderr:write("No such filesystem to install from: " .. fromAddress .. "\n") - return 1 -end - -if #targets == 0 then - if toAddress then - io.stderr:write("No such filesystem to install to: " .. toAddress .. "\n") - else - io.stderr:write("No writable disks found, aborting\n") +function lib.check_targets() + if #lib.targets == 0 then + if lib.options.to then + lib.stderr:write("No such filesystem to install to: " .. lib.options.to .. '\n') + else + lib.stderr:write("No writable disks found, aborting\n") + end + lib.exit(1) end - return 1 + return true end ----- For now, I am allowing source==target -- cp can handle it if the user prepares conditions correctly @@ -124,11 +189,12 @@ end -- return 1 --end -local function prompt_select(devs, direction) +function lib.prompt_select(devs, direction) + table.sort(devs, function(a, b) return a.path 1 then - print("Select the device to install " .. direction) + lib.stdout:write("Select the device to install " .. direction .. '\n') for i = 1, #devs do local src = devs[i] @@ -138,118 +204,141 @@ local function prompt_select(devs, direction) else label = src.dev.address end - print(i .. ") " .. label .. " at " .. src.path) + lib.stdout:write(i .. ") " .. label .. " at " .. src.path .. '\n') end - print("Please enter a number between 1 and " .. #devs) - io.write("Enter 'q' to cancel the installation: ") + lib.stdout:write("Please enter a number between 1 and " .. #devs .. '\n') + lib.stdout:write("Enter 'q' to cancel the installation: ") local choice while not choice do - result = io.read() + result = lib.stdin:read() if result:sub(1, 1):lower() == "q" then - os.exit() + lib.exit() + return false end local number = tonumber(result) if number and number > 0 and number <= #devs then choice = devs[number] else - io.write("Invalid input, please try again: ") + lib.stdout:write("Invalid input, please try again: ") end end end - choice.display = (choice.path == '/' and "the root filesystem") or choice.dev.getLabel() or choice.path - - if #devs == 1 then - print("Selecting " .. choice.display .. " (only option)") - end + -- normally it is helpful to call / the root filesystem + -- but if rootfs is readonly, then we know we are using rootfs as a source + -- in which case, it's label takes priority + choice.display = + not choice.dev.isReadOnly() and (choice.path == '/' and "the root filesystem") or + -- everything has props by this point, except for targets + (choice.prop or {}).label or + choice.dev.getLabel() or + choice.path return choice end -table.sort(sources, function(a, b) return a.path 0 and unicode.sub(result[1], -1) ~= "/" and not suffix:sub(1,1):find('%s') and - (#result == 1 or cmd) then + #result == 1 or searchInPath then resultSuffix = " " .. resultSuffix end diff --git a/src/main/resources/assets/opencomputers/loot/openos/usr/man/install b/src/main/resources/assets/opencomputers/loot/openos/usr/man/install index 6c669aaa6..45c3ab25a 100644 --- a/src/main/resources/assets/opencomputers/loot/openos/usr/man/install +++ b/src/main/resources/assets/opencomputers/loot/openos/usr/man/install @@ -2,20 +2,20 @@ NAME install - installs files from a source filesystem to a target filesystem SYNOPSIS - install [OPTIONS]... + install [name] [OPTIONS]... DESCRIPTION - install builds a list of candidate source and target mounted filesystems. If there are multiple candidates, the user is prompted for selections. By default, install copies all files from the source filesystem's root to the target filesystem's root path. If the source filesystem contains an .osprop, and unless command line options specify otherwise, the target filesystem's label is set and the user is prompted for reboot when install is complete. Alternatively, If a .lootprop file exists in the source filesystem, all default behavior is superceded and .lootprop is run as a script. .lootprop may copy files, set labels, prompt for reboot, etc. on its own. Developers creating their own .lootprop files for devices should respect environment variables set by install as per options it is given, such as ROOT. This manual page details the environment variables set by install when calling .lootprop. + install builds a list of candidate source and target mounted filesystems. If there are multiple candidates, the user is prompted for selections. By default, install copies all files from the source filesystem's root to the target filesystem's root path. The source filesystem can define label, boot, and reboot behavior via .prop and a fully custom install experience via .install which supercedes install running cp from source to target filesystems. Developers creating their own .install files for devices should respect environment variables set by install as per options it is given, such as the root path. This manual page details those environment variables. OPTIONS --from=ADDR - When searching for candidate source filesystems, if specified, only mounted filesystem device addresses or their mount point paths that match ADDR will be included. By default, all filesystems except the rootfs are valid sources for install. If the user is trying to install rootfs to another filesystem, --from=ADDR is required where ADDR matches the rootfs device address or --from=/ + Specifies the source filesystem or its root path. ADDR can be the device guid or a directory path. If this is a directory path, it represents a root path to install from. This option can also be used to specify source paths that would otherwise be ignored, those being devfs, tmpfs, and the rootfs. e.g. --from=/tmp . Note that if both --from and a [name] is given, install expects the source path to have a .prop defining the same name. See .prop for more details. --to=ADDR - same as --from but used when selecting target filesystem candidates. Note that the tmpfs is not a valid target filesystem by default, but must be specified explicitly if needed: i.e. --to=ADDR where ADDR matches the tmpfs device address or its mount point path, e.g. --to=/tmp . Note that install allows TO to equal FROM, also note that /bin/cp does not. But this detail may be noteworthy for .lootprop + Same as --from but specifies the target filesystem by guid or its root path. This option can also be used to specify filesystems that are otherwise ignored including tmpfs. i.e. --to=ADDR where ADDR matches the tmpfs device address or its mount point path. e.g. --to=/tmp --fromDir=PATH - Install PATH from source. PATH is relative to the root of the source filesystem. The default is . + Install PATH from source. PATH is relative to the root of the source filesystem or path given by --from. The default is . --root=PATH Same as --fromDir but for the target filesystem. @@ -26,56 +26,78 @@ OPTIONS -u, --update Indicates that install should prompt the user before modifying files. This invokes -i and -u for /bin/cp. -The following only pertain when .osprop exists in the source filesystem. All environment variables are set for .lootprop regardless of the presense of .osprop +The following can override settings defined in .prop in the source filesystem. - --nolabelset - do not set target label + --label=NAME + use NAME for label instead of any value specified by .prop, --name is deprecated - --name=NAME - use NAME for label instead of any value specified by .osprop. This option is ignored if there is no .osprop (in which case, no label is set at all) + --nosetlabel + do not set target label. --nolabelset is deprecated - --noboot - do not set target as default boot device when rebooting + --nosetboot + do not set target as default boot device when rebooting. --noboot is deprecated --noreboot do not reboot after install -.lootprop ENVIRONMENT - When .lootprop is loaded and executed, a custom table is added to the environment: ENV_.lootprop +.prop + .prop should have valid lua code that returns a table of keys and their values: e.g. "return {name='OpenOS'}" + + name=string + Declares an identifying name of the installation. This is displayed by install during source selection and also can be used on the commandline: e.g. (where {name="tape"} is given) `install tape`. If setlabel is true, this value is used for the target filesystem label. --name overrides this value. Note that install uses a case insensitive search: e.g. install TAPE works the same as install tape. + + setlabel=boolean + Determines whether the install should set the target filesystem's label. If .prop does not define a name key and the user does not define a command line --name=VALUE, setlabel has no action. --nosetlabel overrides this value + + setboot=boolean + Determines if the target filesystem should be set as the machine's default boot device. Default is false, overriden by --nosetboot + + reboot=boolean + Determines if the machine should reboot after the install completes. Overriden by --noreboot + + EXAMPLE: + return {name='OpenOS', setlabel=true, setboot=true, reboot=true} + +.install ENVIRONMENT + When .install is loaded and executed, a custom table is added to the environment: ENV_.install These are the keys of the table as populated by install - ENV_.lootprop.from: - This is the path of the selected source filesystem to install from. It should be the path to the executing .lootprop + ENV_.install.from: + This is the path of the selected source filesystem to install from. It should be the path to the executing .install example: /mnt/ABC - ENV_.lootprop.to: + ENV_.install.to: This is the path of the selected target filesystem to install to. example: / - _ENV.lootprop.fromdir + _ENV.install.fromdir This is the relative path to use in the source filesysterm as passed by command line to install. If unspecified to install it defaults to "." example: . - _ENV.lootprop.root + _ENV.install.root This is the relative path to use in the target filesystem as passed by command line to install. If unspecified to install it defaults to "." example: . - _ENV.lootprop.update + _ENV.install.update Assigned value of --update, see OPTIONS - _ENV.lootprop.nolabelset - Assigned value of --nolabelset, see OPTIONS + _ENV.install.label + Assigned value of --name or .prop's label, see OPTIONS - _ENV.lootprop.name - Assigned value of --name, see OPTIONS + _ENV.install.setlabel + Assigned value of .prop's setlabel unless --nolabelset, see OPTIONS - _ENV.lootprop.noboot - Assigned value of --noboot, see OPTIONS + _ENV.install.setboot + Assigned value of .prop's boot unless --nosetboot, see OPTIONS - --noreboot - Assigned value of --noreboot, see OPTIONS + _ENV.install.reboot + Assigned value of .prop's reboot unless --noreboot, see OPTIONS EXAMPLES install - Searches all non rootfs filesystems to install from, and all non tmpfs filesystems to install to. Prompts the user for a selection, and copies. If .osprop is defined in source, sets label and will prompt for reboot when completed. + Searches all non rootfs filesystems to install from, and all non tmpfs filesystems to install to. Prompts the user for a selection, and copies. If .prop is defined in source, sets label and will prompt for reboot when completed. + + install openos + Searches candidates source filesystems that have .prop's that define name="OpenOS" and prompts the user to confirm install to candidate target filesystems. +