summaryrefslogtreecommitdiffstats
path: root/scripts/map_install_src.lua
blob: f4f0ed3613e80409192de7af87f98396b4c837d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env luajit

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