summaryrefslogtreecommitdiffstats
path: root/scripts/map_install_src.lua
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xscripts/map_install_src.lua168
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