master ninja.lua
  1--
  2-- utility functions
  3--
  4
  5function string.hasprefix(s, prefix)
  6	return s:sub(1, #prefix) == prefix
  7end
  8
  9function string.hassuffix(s, suffix)
 10	return s:sub(-#suffix) == suffix
 11end
 12
 13-- collects the results of an iterator into a table
 14local function collect(f, s, i)
 15	local t = {}
 16	for v in f, s, i do
 17		t[#t + 1] = v
 18	end
 19	return t
 20end
 21
 22-- collects the keys of a table into a sorted table
 23function table.keys(t, f)
 24	local keys = collect(next, t)
 25	table.sort(keys, f)
 26	return keys
 27end
 28
 29-- iterates over the sorted keys and values of a table
 30function sortedpairs(t, f)
 31	return function(s, i)
 32		local k = s[i]
 33		return k and i + 1, k, t[k]
 34	end, table.keys(t, f), 1
 35end
 36
 37-- yields string values of table or nested tables
 38local function stringsgen(t)
 39	for _, val in ipairs(t) do
 40		if type(val) == 'string' then
 41			coroutine.yield(val)
 42		else
 43			stringsgen(val)
 44		end
 45	end
 46end
 47
 48function iterstrings(x)
 49	return coroutine.wrap(stringsgen), x
 50end
 51
 52function strings(s)
 53	return collect(iterstrings(s))
 54end
 55
 56-- yields strings generated by concateting all strings in a table, for every
 57-- combination of strings in subtables
 58local function expandgen(t, i)
 59	while true do
 60		local val
 61		i, val = next(t, i)
 62		if not i then
 63			coroutine.yield(table.concat(t))
 64			break
 65		elseif type(val) == 'table' then
 66			for opt in iterstrings(val) do
 67				t[i] = opt
 68				expandgen(t, i)
 69			end
 70			t[i] = val
 71			break
 72		end
 73	end
 74end
 75
 76function expand(t)
 77	return collect(coroutine.wrap(expandgen), t)
 78end
 79
 80-- yields expanded paths from the given path specification string
 81local function pathsgen(s, i)
 82	local results = {}
 83	local first = not i
 84	while true do
 85		i = s:find('[^%s]', i)
 86		local _, j, arch = s:find('^@([^%s()]*)%s*[^%s]?', i)
 87		if arch then i = j end
 88		if not i or s:sub(i, i) == ')' then
 89			break
 90		end
 91		local parts, nparts = {}, 0
 92		local c
 93		while true do
 94			local j = s:find('[%s()]', i)
 95			if not j or j > i then
 96				nparts = nparts + 1
 97				parts[nparts] = s:sub(i, j and j - 1)
 98			end
 99			i = j
100			c = i and s:sub(i, i)
101			if c == '(' then
102				local opts, nopts = {}, 0
103				local fn = coroutine.wrap(pathsgen)
104				local opt
105				opt, i = fn(s, i + 1)
106				while opt do
107					nopts = nopts + 1
108					opts[nopts] = opt
109					opt, i = fn(s)
110				end
111				nparts = nparts + 1
112				parts[nparts] = opts
113				if not i or s:sub(i, i) ~= ')' then
114					error('unmatched (')
115				end
116				i = i + 1
117				c = s:sub(i, i)
118			else
119				break
120			end
121		end
122		if not arch or arch == config.target.platform:match('[^-]*') then
123			expandgen(parts)
124		end
125		if not c or c == ')' then
126			break
127		end
128	end
129	if first and i then
130		error('unmatched )')
131	end
132	return nil, i
133end
134
135function iterpaths(s)
136	return coroutine.wrap(pathsgen), s
137end
138
139function paths(s)
140	return collect(iterpaths(s))
141end
142
143-- yields non-empty non-comment lines in a file
144local function linesgen(file)
145	for line in io.lines(file) do
146		if #line > 0 and not line:hasprefix('#') then
147			coroutine.yield(line)
148		end
149	end
150end
151
152function iterlines(file, raw)
153	table.insert(pkg.inputs.gen, '$dir/'..file)
154	file = string.format('%s/%s/%s', basedir, pkg.gendir, file)
155	if raw then
156		return io.lines(file)
157	end
158	return coroutine.wrap(linesgen), file
159end
160
161function lines(file, raw)
162	return collect(iterlines(file, raw))
163end
164
165function load(file)
166	table.insert(pkg.inputs.gen, '$dir/'..file)
167	return dofile(string.format('%s/%s/%s', basedir, pkg.gendir, file))
168end
169
170--
171-- base constructs
172--
173
174function set(var, val, indent)
175	if type(val) == 'table' then
176		val = table.concat(val, ' ')
177	end
178	io.write(string.format('%s%s = %s\n', indent or '', var, val))
179end
180
181function subninja(file)
182	if not file:match('^[$/]') then
183		file = '$gendir/'..file
184	end
185	io.write(string.format('subninja %s\n', file))
186end
187
188function include(file)
189	io.write(string.format('include %s\n', file))
190end
191
192local function let(bindings)
193	for var, val in pairs(bindings) do
194		set(var, val, '  ')
195	end
196end
197
198function rule(name, cmd, bindings)
199	io.write(string.format('rule %s\n  command = %s\n', name, cmd))
200	if bindings then
201		let(bindings)
202	end
203end
204
205function build(rule, outputs, inputs, bindings)
206	if type(outputs) == 'table' then
207		outputs = table.concat(strings(outputs), ' ')
208	end
209	if not inputs then
210		inputs = ''
211	elseif type(inputs) == 'table' then
212		local srcs, nsrcs = {}, 0
213		for src in iterstrings(inputs) do
214			nsrcs = nsrcs + 1
215			srcs[nsrcs] = src
216			if src:hasprefix('$srcdir/') then
217				pkg.inputs.fetch[src] = true
218			end
219		end
220		inputs = table.concat(srcs, ' ')
221	elseif inputs:hasprefix('$srcdir/') then
222		pkg.inputs.fetch[inputs] = true
223	end
224	io.write(string.format('build %s: %s %s\n', outputs, rule, inputs))
225	if bindings then
226		let(bindings)
227	end
228end
229
230--
231-- higher-level rules
232--
233
234function sub(name, fn)
235	local old = io.output()
236	io.output(pkg.gendir..'/'..name)
237	fn()
238	io.output(old)
239	subninja(name)
240end
241
242function toolchain(tc)
243	set('ar', tc.ar or (tc.platform and tc.platform..'-ar') or 'ar')
244	set('as', tc.as or (tc.platform and tc.platform..'-as') or 'as')
245	set('cc', tc.cc or (tc.platform and tc.platform..'-cc') or 'cc')
246	set('ld', tc.ld or (tc.platform and tc.platform..'-ld') or 'ld')
247	set('objcopy', tc.objcopy or (tc.platform and tc.platform..'-objcopy') or 'objcopy')
248	set('mc', tc.mc or 'false')
249
250	set('cflags', tc.cflags)
251	set('ldflags', tc.ldflags)
252end
253
254function phony(name, inputs)
255	build('phony', '$gendir/'..name, inputs)
256end
257
258function cflags(flags)
259	set('cflags', '$cflags '..table.concat(flags, ' '))
260end
261
262function nasmflags(flags)
263	set('nasmflags', '$nasmflags '..table.concat(flags, ' '))
264end
265
266function compile(rule, src, deps, args)
267	local obj = src..'.o'
268	if not src:match('^[$/]') then
269		src = '$srcdir/'..src
270		obj = '$outdir/'..obj
271	end
272	if not deps and pkg.deps then
273		deps = '$gendir/deps'
274	end
275	if deps then
276		src = {src, '||', deps}
277	end
278	build(rule, obj, src, args)
279	return obj
280end
281
282function cc(src, deps, args)
283	return compile('cc', src, deps, args)
284end
285
286function objects(srcs, deps, args)
287	local objs, nobjs = {}, 0
288	local rules = {
289		c='cc',
290		s='cc',
291		S='cc',
292		cc='cc',
293		cpp='cc',
294		asm='nasm',
295	}
296	local fn
297	if type(srcs) == 'string' then
298		fn = coroutine.wrap(pathsgen)
299	else
300		fn = coroutine.wrap(stringsgen)
301	end
302	for src in fn, srcs do
303		local rule = rules[src:match('[^.]*$')]
304		local obj = src
305		if rule then
306			obj = compile(rule, src, deps, args)
307		end
308		nobjs = nobjs + 1
309		objs[nobjs] = obj
310	end
311	return objs
312end
313
314function link(out, files, args)
315	local objs = {}
316	local deps = {}
317	for _, file in ipairs(files) do
318		if not file:match('^[$/]') then
319			file = '$outdir/'..file
320		end
321		if file:hassuffix('.d') then
322			deps[#deps + 1] = file
323		else
324			objs[#objs + 1] = file
325		end
326	end
327	out = '$outdir/'..out
328	if not args then
329		args = {}
330	end
331	if next(deps) then
332		local rsp = out..'.rsp'
333		build('awk', rsp, {deps, '|', '$basedir/scripts/rsp.awk'}, {expr='-f $basedir/scripts/rsp.awk'})
334		objs[#objs + 1] = '|'
335		objs[#objs + 1] = rsp
336		args.ldlibs = '@'..rsp
337	end
338	build('link', out, objs, args)
339	return out
340end
341
342function ar(out, files)
343	out = '$outdir/'..out
344	local objs, nobjs = {}, 0
345	local deps, ndeps = {out}, 1
346	for _, file in ipairs(files) do
347		if not file:match('^[$/]') then
348			file = '$outdir/'..file
349		end
350		if file:find('%.[ad]$') then
351			ndeps = ndeps + 1
352			deps[ndeps] = file
353		else
354			nobjs = nobjs + 1
355			objs[nobjs] = file
356		end
357	end
358	build('ar', out, objs)
359	build('rsp', out..'.d', deps)
360end
361
362function lib(out, srcs, deps)
363	return ar(out, objects(srcs, deps))
364end
365
366function exe(out, srcs, deps, args)
367	return link(out, objects(srcs, deps), args)
368end
369
370function yacc(name, gram)
371	if not gram:match('^[$/]') then
372		gram = '$srcdir/'..gram
373	end
374	build('yacc', expand{'$outdir/', name, {'.tab.c', '.tab.h'}}, gram, {
375		yaccflags='-d -b $outdir/'..name,
376	})
377end
378
379function waylandproto(proto, outs, args)
380	proto = '$srcdir/'..proto
381	if outs.client then
382		build('wayland-proto', '$outdir/'..outs.client, proto, {type='client-header'})
383	end
384	if outs.server then
385		build('wayland-proto', '$outdir/'..outs.server, proto, {type='server-header'})
386	end
387	if outs.code then
388		local code = '$outdir/'..outs.code
389		build('wayland-proto', code, proto, {type='public-code'})
390		cc(code, {'pkg/wayland/headers'}, args)
391	end
392end
393
394function fetch(method)
395	local script
396	local deps = {'|', '$dir/ver', script}
397	if method == 'local' then
398		script = '$dir/fetch.sh'
399	else
400		script = '$basedir/scripts/fetch-'..method..'.sh'
401	end
402	if method ~= 'git' then
403		table.insert(deps, '||')
404		table.insert(deps, '$builddir/pkg/pax/host/pax')
405	end
406	build('fetch', '$dir/fetch', deps, {script=script})
407	if basedir ~= '.' then
408		build('phony', '$gendir/fetch', '$dir/fetch')
409	end
410	if next(pkg.inputs.fetch) then
411		build('phony', table.keys(pkg.inputs.fetch), '$dir/fetch')
412	end
413end
414
415local function findany(path, pats)
416	for _, pat in pairs(pats) do
417		if path:find(pat) then
418			return true
419		end
420	end
421	return false
422end
423
424local function specmatch(spec, path)
425	if spec.include and not findany(path, spec.include) then
426		return false
427	end
428	if spec.exclude and findany(path, spec.exclude) then
429		return false
430	end
431	return true
432end
433
434local function fs(name, path)
435	for _, spec in ipairs(config.fs) do
436		for specname in iterstrings(spec) do
437			if name == specname then
438				return specmatch(spec, path)
439			end
440		end
441	end
442	return (config.fs.include or config.fs.exclude) and specmatch(config.fs, path)
443end
444
445function gitfile(path, mode, src)
446	local out = '$builddir/root.hash/'..path
447	local perm = ('10%04o %s'):format(tonumber(mode, 8), path)
448	build('git-hash', out, {src, '|', '$basedir/scripts/hash.sh', '||', '$builddir/root.stamp'}, {
449		args=perm,
450	})
451	table.insert(pkg.inputs.index, out)
452end
453
454function file(path, mode, src)
455	if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then
456		return
457	end
458	mode = ('%04o'):format(tonumber(mode, 8))
459	pkg.fspec[path] = {
460		type='reg',
461		mode=mode,
462		source=src:gsub('^%$(%w+)', pkg, 1),
463	}
464	gitfile(path, mode, src)
465end
466
467function dir(path, mode)
468	if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then
469		return
470	end
471	pkg.fspec[path] = {
472		type='dir',
473		mode=('%04o'):format(tonumber(mode, 8)),
474	}
475end
476
477function sym(path, target)
478	if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then
479		return
480	end
481	pkg.fspec[path] = {
482		type='sym',
483		mode='0777',
484		target=target,
485	}
486	local out = '$builddir/root.hash/'..path
487	build('git-hash', out, {'|', '$basedir/scripts/hash.sh', '||', '$builddir/root.stamp'}, {
488		args=string.format('120000 %s %s', path, target),
489	})
490	table.insert(pkg.inputs.index, out)
491end
492
493function man(srcs, section)
494	for _, src in ipairs(srcs) do
495		local out = src..'.gz'
496		if not src:match('^[$/]') then
497			src = '$srcdir/'..src
498			out = '$outdir/'..out
499		end
500
501		local base = src:match('[^/]*$')
502		local ext = base:match('%.([^.]*)$')
503		if ext then base = base:sub(1, -(#ext + 2)) end
504		if section then ext = section end
505		local path = 'share/man/man'..ext..'/'..base..'.'..ext
506		if config.gzman ~= false then
507			build('gzip', out, src)
508			src = out
509			path = path..'.gz'
510		end
511		file(path, '644', src)
512	end
513end
514
515function copy(outdir, srcdir, files)
516	local outs = {}
517	for file in iterstrings(files) do
518		local out = outdir..'/'..file
519		table.insert(outs, out)
520		build('copy', out, srcdir..'/'..file)
521	end
522	return outs
523end