-- -- 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(f, s, i) local t = {} for v in f, s, i do t[#t + 1] = v end return t end -- collects the keys of a table into a sorted table function table.keys(t, f) local keys = collect(next, t) table.sort(keys, f) return keys end -- iterates over the sorted keys and values of a table function sortedpairs(t, f) return function(s, i) local k = s[i] return k and i + 1, k, t[k] end, table.keys(t, f), 1 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 local function pathsgen(s, i) local results = {} local first = not i while true do i = s:find('[^%s]', i) local _, j, arch = s:find('^@([^%s()]*)%s*[^%s]?', i) if arch then i = j end 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 if not arch or arch == config.target.platform:match('[^-]*') then expandgen(parts) end 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 local function linesgen(file) for line in io.lines(file) do if #line > 0 and not line:hasprefix('#') then coroutine.yield(line) end end end function iterlines(file, raw) table.insert(pkg.inputs.gen, '$dir/'..file) file = string.format('%s/%s/%s', basedir, pkg.gendir, file) if raw then return io.lines(file) end return coroutine.wrap(linesgen), file end function lines(file, raw) return collect(iterlines(file, raw)) end function load(file) table.insert(pkg.inputs.gen, '$dir/'..file) return dofile(string.format('%s/%s/%s', basedir, pkg.gendir, 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:match('^[$/]') then file = '$gendir/'..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.gendir..'/'..name) fn() io.output(old) subninja(name) end function toolchain(tc) set('ar', tc.ar or (tc.platform and tc.platform..'-ar') or 'ar') set('as', tc.as or (tc.platform and tc.platform..'-as') or 'as') set('cc', tc.cc or (tc.platform and tc.platform..'-cc') or 'cc') set('ld', tc.ld or (tc.platform and tc.platform..'-ld') or 'ld') set('objcopy', tc.objcopy or (tc.platform and tc.platform..'-objcopy') or 'objcopy') set('mc', tc.mc or 'false') set('cflags', tc.cflags) set('ldflags', tc.ldflags) end function phony(name, inputs) build('phony', '$gendir/'..name, inputs) end function cflags(flags) set('cflags', '$cflags '..table.concat(flags, ' ')) end function nasmflags(flags) set('nasmflags', '$nasmflags '..table.concat(flags, ' ')) end function compile(rule, src, deps, args) local obj = src..'.o' if not src:match('^[$/]') then src = '$srcdir/'..src obj = '$outdir/'..obj end if not deps and pkg.deps then deps = '$gendir/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, args) 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, args) end nobjs = nobjs + 1 objs[nobjs] = src end return objs end function link(out, files, args) local objs = {} local deps = {} for _, file in ipairs(files) do if not file:match('^[$/]') then file = '$outdir/'..file end if file:hassuffix('.d') then deps[#deps + 1] = file else objs[#objs + 1] = file end end out = '$outdir/'..out if not args then args = {} end if next(deps) then local rsp = out..'.rsp' build('awk', rsp, {deps, '|', '$basedir/scripts/rsp.awk'}, {expr='-f $basedir/scripts/rsp.awk'}) objs[#objs + 1] = '|' objs[#objs + 1] = 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:match('^[$/]') 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('rsp', 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:match('^[$/]') then gram = '$srcdir/'..gram end build('yacc', expand{'$outdir/', name, {'.tab.c', '.tab.h'}}, gram, { yaccflags='-d -b $outdir/'..name, }) end function waylandproto(proto, outs, args) proto = '$srcdir/'..proto if outs.client then build('wayland-proto', '$outdir/'..outs.client, proto, {type='client-header'}) end if outs.server then build('wayland-proto', '$outdir/'..outs.server, proto, {type='server-header'}) end if outs.code then local code = '$outdir/'..outs.code build('wayland-proto', code, proto, {type='public-code'}) cc(code, {'pkg/wayland/headers'}, args) end end function fetch(method) local script local deps = {'|', '$dir/ver', script} if method == 'local' then script = '$dir/fetch.sh' else script = '$basedir/scripts/fetch-'..method..'.sh' end if method ~= 'git' then table.insert(deps, '$builddir/pkg/pax/host/pax') end build('fetch', '$dir/fetch', deps, {script=script}) if basedir ~= '.' then build('phony', '$gendir/fetch', '$dir/fetch') end if next(pkg.inputs.fetch) then build('phony', table.keys(pkg.inputs.fetch), '$dir/fetch') 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 gitfile(path, mode, src) local out = '$builddir/root.hash/'..path local perm = ('10%04o %s'):format(tonumber(mode, 8), path) build('git-hash', out, {src, '|', '$basedir/scripts/hash.sh', '||', '$builddir/root.stamp'}, { args=perm, }) table.insert(pkg.inputs.index, out) end function file(path, mode, src) if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then return end mode = ('%04o'):format(tonumber(mode, 8)) pkg.fspec[path] = { type='reg', mode=mode, source=src:gsub('^%$(%w+)', pkg, 1), } gitfile(path, mode, src) end function dir(path, mode) if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then return end pkg.fspec[path] = { type='dir', mode=('%04o'):format(tonumber(mode, 8)), } end function sym(path, target) if pkg.gendir:hasprefix('pkg/') and not fs(pkg.name, path) then return end pkg.fspec[path] = { type='sym', mode='0777', target=target, } local out = '$builddir/root.hash/'..path build('git-hash', out, {'|', '$basedir/scripts/hash.sh', '||', '$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 local out = src..'.gz' if not src:match('^[$/]') then src = '$srcdir/'..src out = '$outdir/'..out end local base = src:match('[^/]*$') local ext = base:match('%.([^.]*)$') if ext then base = base:sub(1, -(#ext + 2)) end if section then ext = section end local path = 'share/man/man'..ext..'/'..base..'.'..ext if config.gzman ~= false then build('gzip', out, src) src = out path = path..'.gz' end file(path, '644', src) end end function copy(outdir, srcdir, files) local outs = {} for file in iterstrings(files) do local out = outdir..'/'..file table.insert(outs, out) build('copy', out, srcdir..'/'..file) end return outs end