1.6 rc version 2

advanced install features as well as simplified install for common usage
renamed .osprop to .prop
support for named installed
cp link copy fixes
fix /bin/less pgdown and space
package delay lookup no longer weak value
fixed regression in sh tab complete for first arg completion
This commit is contained in:
payonel 2016-06-10 22:52:00 -07:00
parent 3e86a32783
commit 80e25f7be7
8 changed files with 318 additions and 202 deletions

View File

@ -1 +0,0 @@
return {name = "OpenOS"}

View File

@ -0,0 +1 @@
return {label = "OpenOS", reboot=true, setlabel=true, setboot=true}

View File

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

View File

@ -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<b.path end)
local choice = devs[1]
if #devs > 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<b.path end)
table.sort(targets, function(a, b) return a.path<b.path end)
local source = prompt_select(sources, "from")
local target = prompt_select(targets, "to")
-- load .osprop (optional) settings
local osprop = nil
if fs.exists(source.path .. ".osprop") then
local osprop_data, reason = loadfile(source.path .. ".osprop", "bt", setmetatable({}, {__index=_G}))
if not osprop_data then
io.stderr:write("Failed to load .osprop: " .. tostring(reason) .. '\n')
return 1
end
osprop = osprop_data()
options.name = options.name or osprop.name
source.display = options.name or source.display
end
-- if .lootprop exists
if fs.exists(source.path .. ".lootprop") then
local env = setmetatable(
function lib.load_env()
lib.env =
{
lootprop =
{
from=source.path,
to=target.path,
fromDir=fromDir,
root=root,
update=options.update,
nolabelset=options.nolabelset,
name=options.name,
noboot=options.noboot,
noreboot=options.noreboot,
}
}, {__index=_G})
local lootprop, reason = loadfile(source.path .. ".lootprop", "bt", env)
if not lootprop then
io.stderr:write("Failed to load .lootprop: " .. tostring(reason) .. '\n')
return 1
from=lib.source_root,
to=lib.target_root,
fromDir=lib.source_dir,
root=lib.target_dir,
update=lib.options.update,
label=lib.options.label or lib.source.prop.label,
setlabel=lib.source.prop.setlabel and not lib.options.nosetlabel,
setboot=lib.source.prop.setboot and not lib.options.nosetboot,
reboot=lib.source.prop.reboot and not lib.options.noreboot,
}
end
function lib.init()
lib.load_options()
if not lib.load_candidates() then return false end
if not lib.check_sources() then return false end
if not lib.check_targets() then return false end
lib.source = lib.prompt_select(lib.sources, "from")
if not lib.source then return false end
lib.source_root = lib.source_root or lib.source.path
lib.target = lib.prompt_select(lib.targets, "to")
if not lib.target then return false end
lib.target_root = lib.target_root or lib.target.path
lib.load_env()
local reason
lib.installer, reason = lib.load(lib.source_root .. '/.install', {install=lib.env})
if not lib.installer then
if reason then
lib.stderr:write("installer failed to load: " .. tostring(reason) .. '\n')
lib.exit(1)
return false
else
lib.installer = lib.run
end
end
return lootprop()
return true
end
local cp = shell.resolve("cp", "lua")
local cp_options = "-vrx" .. (options.update and "ui" or "")
local cp_source = (source.path .. fromDir):gsub("/+","/")
local cp_dest = (target.path .. root):gsub("/+","/")
function lib.run()
local cp = shell.resolve("cp", "lua")
local cp_options = "-vrx" .. (lib.options.update and "ui" or "")
local cp_source = (lib.source_root .. lib.source_dir):gsub("/+","/")
local cp_dest = (lib.target_root .. lib.target_dir):gsub("/+","/")
io.write("Install " .. source.display .. " to " .. target.display .. "? [Y/n] ")
local choice = text.trim(io.read()):lower()
if choice == "" then
choice = "y"
end
if choice ~= "y" then
print("Installation cancelled")
return
end
local message = string.format("Installing %s [%s] to %s [%s]", source.display, cp_source, target.display, cp_dest)
local cmd = cp .. ' ' .. cp_options .. ' ' .. cp_source .. ' ' .. cp_dest
print(message)
print(cmd)
os.sleep(0.25)
local result, reason = os.execute(cmd)
if not result then
error(reason, 0)
end
if osprop then
if not options.nolabelset then
pcall(target.dev.setLabel, options.name)
io.write("Install " .. lib.source.display .. " to " .. lib.target.display .. "? [Y/n] ")
local choice = text.trim(lib.stdin:read()):lower()
if choice == "" then
choice = "y"
end
if not options.noreboot then
io.write("All done! " .. ((not options.noboot) and "Set as boot device and r" or "R") .. "eboot now? [Y/n] ")
local result = io.read()
if choice ~= "y" then
lib.stdout:write("Installation cancelled\n")
lib.exit()
return false
end
local message = string.format("Installing %s [%s] to %s [%s]", lib.source.display, cp_source, lib.target.display, cp_dest)
local cmd = cp .. ' ' .. cp_options .. ' ' .. cp_source .. ' ' .. cp_dest
lib.stdout:write(message .. '\n')
lib.stdout:write(cmd .. '\n')
os.sleep(0.25)
local result, reason = os.execute(cmd)
if not result then
error(reason, 0)
end
lib.stdout:write("Installation complete!\n")
if lib.env.setlabel then
pcall(lib.target.dev.setLabel, lib.env.label)
end
local prereboot = function()end
if lib.env.setboot then
prereboot = computer.setBootAddress
end
if lib.env.reboot then
lib.stdout:write("Reboot now? [Y/n] ")
local result = lib.stdin:read()
if not result or result == "" or result:sub(1, 1):lower() == "y" then
if not options.noboot then computer.setBootAddress(target.dev.address)end
io.write("\nRebooting now!\n")
prereboot(lib.target.dev.address)
lib.stdout:write("\nRebooting now!\n")
computer.shutdown(true)
end
end
lib.stdout:write("Returning to shell.\n")
end
io.write("Returning to shell.\n")
if lib.options.lib then
return lib
end
lib.init()
lib.run()

View File

@ -277,7 +277,7 @@ while true do
if code == keyboard.keys.q or code == keyboard.keys.d and keyboard.isControlDown() then
break
elseif code == keyboard.keys.space or code == keyboard.keys.pageDown then
num = nil
num = max_display
elseif code == keyboard.keys.pageUp then
num = -max_display
elseif code == keyboard.keys.enter or code == keyboard.keys.down then

View File

@ -64,9 +64,8 @@ local delay_tools = setmetatable({},{__mode="v"})
package.delay_data = delay_data
function delay_data.__index(tbl,key)
local lookup = delay_tools.lookup or loadfile("/lib/tools/delayLookup.lua")
delay_tools.lookup = lookup
return lookup(delay_data, tbl, key)
delay_data.lookup = delay_data.lookup or loadfile("/lib/tools/delayLookup.lua")
return delay_data.lookup(delay_data, tbl, key)
end
delay_data.__pairs = delay_data.__index -- nil key acts like pairs

View File

@ -642,7 +642,7 @@ function --[[@delayloaded-start@]] sh.internal.hintHandlerImpl(full_line, cursor
local resultSuffix = suffix
if #result > 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

View File

@ -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.