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