diff options
Diffstat (limited to '')
-rwxr-xr-x | scripts/map_install_src.lua | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/scripts/map_install_src.lua b/scripts/map_install_src.lua new file mode 100755 index 0000000..ffc9a30 --- /dev/null +++ b/scripts/map_install_src.lua @@ -0,0 +1,168 @@ +#!/usr/bin/env luajit +-- SPDX-License-Identifier: GPL-3.0-or-later + +-- parse install commands from stdin +-- input: PREFIX=... make install --dry-run --always-make +-- output: <install path> <source path> +-- (or sed commands if --sed was specified) + +output = 'list' +if #arg > 1 or arg[1] == '-h' or arg[1] == '--help' then + print(string.format([[ +Read install commands and map install paths to paths in source directory. + +Usage: +$ PREFIX=... make install --dry-run --always-make | %s + +Example output: +/kresd/git/.local/lib/kdns_modules/policy.lua modules/policy/policy.lua + +Option --sed will produce output suitable as input suitable for sed.]], + arg[0])) + os.exit(1) +elseif #arg == 0 then + output = 'list' +elseif arg[1] == '--sed' then + output = 'sed' +else + print('Invalid arguments. See --help.') + os.exit(2) +end + +-- remove double // from paths and remove trailing / +function normalize_path(path) + assert(path) + repeat + path, changes = path:gsub('//', '/') + until changes == 0 + return path:gsub('/$', '') +end + +function is_opt(word) + return word:match('^-') +end + +-- opts requiring additional argument to be skipped +local ignored_opts_with_arg = { + ['--backup'] = true, + ['-g'] = true, + ['--group'] = true, + ['-m'] = true, + ['--mode'] = true, + ['-o'] = true, + ['--owner'] = true, + ['--strip-program'] = true, + ['--suffix'] = true, +} + +-- state machine junctions caused by --opts +-- returns: new state (expect, mode) and target name if any +function parse_opts(word, expect, mode) + if word == '--' then + return 'names', mode, nil -- no options anymore + elseif word == '-d' or word == '--directory' then + return 'opt_or_name', 'newdir', nil + elseif word == '-t' or word == '--target-directory' then + return 'targetdir', mode, nil + elseif word:match('^--target-directory=') then + return 'opt_or_name', mode, string.sub(word, 20) + elseif ignored_opts_with_arg[word] then + return 'ignore', mode, nil -- ignore next word + else + return expect, mode, nil -- unhandled opt + end +end + + +-- cmd: complete install command line: install -m 0644 -t dest src1 src2 +-- dirs: names known to be directories: name => true +-- returns: updated dirs +function process_cmd(cmd, dirs) + -- print('# ' .. cmd) + sanity_check(cmd) + local expect = 'install' + local mode = 'copy' -- copy or newdir + local target -- last argument or argument for install -t + local names = {} -- non-option arguments + + for word in cmd:gmatch('%S+') do + if expect == 'install' then -- parsing 'install' + assert(word == 'install') + expect = 'opt_or_name' + elseif expect == 'opt_or_name' then + if is_opt(word) then + expect, mode, newtarget = parse_opts(word, expect, mode) + target = newtarget or target + else + if mode == 'copy' then + table.insert(names, word) + elseif mode == 'newdir' then + local path = normalize_path(word) + dirs[path] = true + else + assert(false, 'bad mode') + end + end + elseif expect == 'targetdir' then + local path = normalize_path(word) + dirs[path] = true + target = word + expect = 'opt_or_name' + elseif expect == 'names' then + table.insert(names, word) + elseif expect == 'ignore' then + expect = 'opt_or_name' + else + assert(false, 'bad expect') + end + end + if mode == 'newdir' then + -- no mapping to print, this cmd just created directory + return dirs + end + + if not target then -- last argument is the target + target = table.remove(names) + end + assert(target, 'fatal: no target in install cmd') + target = normalize_path(target) + + for _, name in pairs(names) do + basename = string.gsub(name, "(.*/)(.*)", "%2") + if not dirs[target] then + print('fatal: target directory "' .. target .. '" was not created yet!') + os.exit(2) + end + -- mapping installed name -> source name + if output == 'list' then + print(target .. '/' .. basename, name) + elseif output == 'sed' then + print(string.format([[s`%s`%s`g]], + target .. '/' .. basename, name)) + else + assert(false, 'unsupported output') + end + end + return dirs +end + +function sanity_check(cmd) + -- shell quotation is not supported + assert(not cmd:match('"'), 'quotes " are not supported') + assert(not cmd:match("'"), "quotes ' are not supported") + assert(not cmd:match('\\'), "escapes like \\ are not supported") + assert(cmd:match('^install%s'), 'not an install command') +end + +-- remember directories created by install -d so we can expand relative paths +local dirs = {} +while true do + local cmd = io.read("*line") + if not cmd then + break + end + local isinstall = cmd:match('^install%s') + if isinstall then + dirs = process_cmd(cmd, dirs) + end +end |