summaryrefslogtreecommitdiff
path: root/ninja.lua
blob: e0bc3b63faa124dc21328ef36e47232682de5363 (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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
--
-- utility functions
--

function string.hasprefix(s, prefix)
	return s:sub(1, #prefix) == prefix
end

function string.hassuffix(s, suffix)
	return s:sub(-#suffix) == suffix
end

-- collects the results of an iterator into a table
local function collect(fn, s, i)
	local results, nresults = {}, 0
	for val in fn, s, i do
		nresults = nresults + 1
		results[nresults] = val
	end
	return results
end

-- collects the keys of a table into a sorted table
function table.keys(t)
	local keys = collect(next, t)
	table.sort(keys)
	return keys
end

-- yields string values of table or nested tables
local function stringsgen(t)
	for _, val in ipairs(t) do
		if type(val) == 'string' then
			coroutine.yield(val)
		else
			stringsgen(val)
		end
	end
end

function iterstrings(x)
	return coroutine.wrap(stringsgen), x
end

function strings(s)
	return collect(iterstrings(s))
end

-- yields strings generated by concateting all strings in a table, for every
-- combination of strings in subtables
local function expandgen(t, i)
	while true do
		local val
		i, val = next(t, i)
		if not i then
			coroutine.yield(table.concat(t))
			break
		elseif type(val) == 'table' then
			for opt in iterstrings(val) do
				t[i] = opt
				expandgen(t, i)
			end
			t[i] = val
			break
		end
	end
end

function expand(t)
	return collect(coroutine.wrap(expandgen), t)
end

-- yields expanded paths from the given path specification string
function pathsgen(s, i)
	local results = {}
	local first = not i
	while true do
		i = s:find('%g', i)
		if not i or s:sub(i, i) == ')' then
			break
		end
		local parts, nparts = {}, 0
		local c
		while true do
			local j = s:find('[%s()]', i)
			if not j or j > i then
				nparts = nparts + 1
				parts[nparts] = s:sub(i, j and j - 1)
			end
			i = j
			c = i and s:sub(i, i)
			if c == '(' then
				local opts, nopts = {}, 0
				local fn = coroutine.wrap(pathsgen)
				local opt
				opt, i = fn(s, i + 1)
				while opt do
					nopts = nopts + 1
					opts[nopts] = opt
					opt, i = fn(s)
				end
				nparts = nparts + 1
				parts[nparts] = opts
				if not i or s:sub(i, i) ~= ')' then
					error('unmatched (')
				end
				i = i + 1
				c = s:sub(i, i)
			else
				break
			end
		end
		expandgen(parts)
		if not c or c == ')' then
			break
		end
	end
	if first and i then
		error('unmatched )')
	end
	return nil, i
end

function iterpaths(s)
	return coroutine.wrap(pathsgen), s
end

function paths(s)
	return collect(iterpaths(s))
end

-- yields non-empty non-comment lines in a file
function linesgen(file)
	table.insert(pkg.inputs.gen, '$dir/'..file)
	for line in io.lines(pkg.dir..'/'..file) do
		if #line > 0 and not line:hasprefix('#') then
			coroutine.yield(line)
		end
	end
end

function iterlines(file)
	return coroutine.wrap(linesgen), file
end

function lines(file)
	return collect(iterlines(file))
end

--
-- base constructs
--

function set(var, val, indent)
	if type(val) == 'table' then
		val = table.concat(val, ' ')
	end
	io.write(string.format('%s%s = %s\n', indent or '', var, val))
end

function subninja(file)
	if not file:hasprefix('$') then
		file = '$dir/'..file
	end
	io.write(string.format('subninja %s\n', file))
end

function include(file)
	io.write(string.format('include %s\n', file))
end

local function let(bindings)
	for var, val in pairs(bindings) do
		set(var, val, '  ')
	end
end

function rule(name, cmd, bindings)
	io.write(string.format('rule %s\n  command = %s\n', name, cmd))
	if bindings then
		let(bindings)
	end
end

function build(rule, outputs, inputs, bindings)
	if type(outputs) == 'table' then
		outputs = table.concat(strings(outputs), ' ')
	end
	if not inputs then
		inputs = ''
	elseif type(inputs) == 'table' then
		local srcs, nsrcs = {}, 0
		for src in iterstrings(inputs) do
			nsrcs = nsrcs + 1
			srcs[nsrcs] = src
			if src:hasprefix('$srcdir/') then
				pkg.inputs.fetch[src] = true
			end
		end
		inputs = table.concat(srcs, ' ')
	elseif inputs:hasprefix('$srcdir/') then
		pkg.inputs.fetch[inputs] = true
	end
	io.write(string.format('build %s: %s %s\n', outputs, rule, inputs))
	if bindings then
		let(bindings)
	end
end

--
-- higher-level rules
--

function sub(name, fn)
	local old = io.output()
	io.output(pkg.dir..'/'..name)
	fn()
	io.output(old)
	subninja(name)
end

function toolchain(name)
	set('cflags', '$'..name..'_cflags')
	set('cxxflags', '$'..name..'_cxxflags')
	set('ldflags', '$'..name..'_ldflags')
	include('toolchain/$'..name..'_toolchain.ninja')
end

function phony(name, inputs)
	build('phony', '$dir/'..name, inputs)
end

function cflags(flags)
	set('cflags', '$cflags '..table.concat(flags, ' '))
end

function compile(rule, src, deps, args)
	local obj = src..'.o'
	if not src:hasprefix('$') then
		src = '$srcdir/'..src
		obj = '$outdir/'..obj
	end
	if not deps and pkg.deps then
		deps = '$dir/deps'
	end
	if deps then
		src = {src, '||', deps}
	end
	build(rule, obj, src, args)
	return obj
end

function cc(src, deps, args)
	return compile('cc', src, deps, args)
end

function objects(srcs, deps)
	local objs, nobjs = {}, 0
	local rules = {
		c='cc',
		s='cc',
		S='cc',
		cc='cc',
		cpp='cc',
		asm='nasm',
	}
	local fn
	if type(srcs) == 'string' then
		fn = coroutine.wrap(pathsgen)
	else
		fn = coroutine.wrap(stringsgen)
	end
	for src in fn, srcs do
		local rule = rules[src:match('[^.]*$')]
		if rule then
			src = compile(rule, src, deps)
		end
		nobjs = nobjs + 1
		objs[nobjs] = src
	end
	return objs
end

function link(out, files, args)
	local objs, nobjs = {}, 0
	local deps, ndeps = {}, 0
	for _, file in ipairs(files) do
		if not file:hasprefix('$') then
			file = '$outdir/'..file
		end
		if file:hassuffix('.d') then
			ndeps = ndeps + 1
			deps[ndeps] = file
		else
			nobjs = nobjs + 1
			objs[nobjs] = file
		end
	end
	out = '$outdir/'..out
	if not args then
		args = {}
	end
	if next(deps) then
		local rsp = out..'.rsp'
		build('awk', rsp, {deps, '|', 'scripts/rsp.awk'}, {expr='-f scripts/rsp.awk'})
		objs = {objs, '|', rsp}
		args.ldlibs = '@'..rsp
	end
	build('link', out, objs, args)
	return out
end

function ar(out, files)
	out = '$outdir/'..out
	local objs, nobjs = {}, 0
	local deps, ndeps = {out}, 1
	for _, file in ipairs(files) do
		if not file:hasprefix('$') then
			file = '$outdir/'..file
		end
		if file:find('%.[ad]$') then
			ndeps = ndeps + 1
			deps[ndeps] = file
		else
			nobjs = nobjs + 1
			objs[nobjs] = file
		end
	end
	build('ar', out, objs)
	build('lines', out..'.d', deps)
end

function lib(out, srcs, deps)
	return ar(out, objects(srcs, deps))
end

function exe(out, srcs, deps, args)
	return link(out, objects(srcs, deps), args)
end

function yacc(name, gram)
	if not gram:hasprefix('$') then
		gram = '$srcdir/'..gram
	end
	build('yacc', expand{'$outdir/', name, {'.tab.c', '.tab.h'}}, gram, {
		yaccflags='-d -b '..name,
	})
end

function waylandproto(proto, client, server, code)
	proto = '$srcdir/'..proto
	code = '$outdir/'..code
	build('waylandproto', '$outdir/'..client, proto, {type='client-header'})
	build('waylandproto', '$outdir/'..server, proto, {type='server-header'})
	build('waylandproto', code, proto, {type='code'})
	cc(code, {'pkg/wayland/headers'})
end

function fetch(method, args)
	build('fetch'..method, '$outdir/fetch.stamp', {'|', '$dir/rev'}, {args=args})
	if next(pkg.inputs.fetch) then
		build('phony', table.keys(pkg.inputs.fetch), '$outdir/fetch.stamp')
	end
end

local function findany(path, pats)
	for _, pat in pairs(pats) do
		if path:find(pat) then
			return true
		end
	end
	return false
end

local function specmatch(spec, path)
	if spec.include and not findany(path, spec.include) then
		return false
	end
	if spec.exclude and findany(path, spec.exclude) then
		return false
	end
	return true
end

local function fs(name, path)
	for _, spec in ipairs(config.fs) do
		for specname in iterstrings(spec) do
			if name == specname then
				return specmatch(spec, path)
			end
		end
	end
	return (config.fs.include or config.fs.exclude) and specmatch(config.fs, path)
end

function file(path, mode, src)
	if pkg.dir:hasprefix('pkg/') and not fs(pkg.name, path) then
		return
	end
	local out = '$builddir/root.hash/'..path
	mode = tonumber(mode, 8)
	local perm = string.format('10%04o %s', mode, path)
	build('githash', out, {src, '|', 'scripts/hash.rc', '||', '$builddir/root.stamp'}, {
		args=perm,
	})
	table.insert(pkg.inputs.index, out)
	if mode ~= 420 and mode ~= 493 then -- 0644 and 0755
		table.insert(pkg.perms, perm)
	end
end

function dir(path, mode)
	if pkg.dir:hasprefix('pkg/') and not fs(pkg.name, path) then
		return
	end
	mode = tonumber(mode, 8)
	table.insert(pkg.perms, string.format('04%04o %s', mode, path))
end

function sym(path, target)
	if pkg.dir:hasprefix('pkg/') and not fs(pkg.name, path) then
		return
	end
	local out = '$builddir/root.hash/'..path
	build('githash', out, {'|', 'scripts/hash.rc', '||', '$builddir/root.stamp'}, {
		args=string.format('120000 %s %s', path, target),
	})
	table.insert(pkg.inputs.index, out)
end

function man(srcs, section)
	for _, src in ipairs(srcs) do
		if not src:hasprefix('$') then
			src = '$srcdir/'..src
		end
		local i = src:find('/', 1, true)
		local gz = '$outdir'..src:sub(i)..'.gz'
		build('gzip', gz, src)
		local srcsection = section or src:match('[^.]*$')
		file('share/man/man'..srcsection..'/'..gz:match('[^/]*$'), '644', gz)
	end
end

function copy(outdir, srcdir, files)
	local outs = {}
	for i, file in ipairs(files) do
		local out = outdir..'/'..file
		outs[i] = out
		build('copy', out, srcdir..'/'..file)
	end
	return outs
end