123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916 |
- local posix = require("posix")
- -- Targets:
- --
- -- {
- -- fullname = full name of target
- -- dir = target's build directory
- -- outs = target's object files
- -- is = { set of rule types which made the target }
- -- }
- local emitter = {}
- local rules = {}
- local targets = {}
- local buildfiles = {}
- local globals
- local cwd = "."
- local vars = {}
- local parente = {}
- local loadingstack = {}
- -- Forward references
- local loadtarget
- local function print(...)
- local function print_no_nl(list)
- for _, s in ipairs(list) do
- if (type(s) == "table") then
- io.stderr:write("{")
- for k, v in pairs(s) do
- print_no_nl({k})
- io.stderr:write("=")
- print_no_nl({v})
- io.stderr:write(" ")
- end
- io.stderr:write("}")
- else
- io.stderr:write(tostring(s))
- end
- end
- end
- print_no_nl({...})
- io.stderr:write("\n")
- end
- local function assertString(s, i)
- if (type(s) ~= "string") then
- error(string.format("parameter %d must be a string", i))
- end
- end
- local function concat(...)
- local r = {}
- local function process(list)
- for _, t in ipairs(list) do
- if (type(t) == "table") and not t.is then
- process(t)
- else
- r[#r+1] = t
- end
- end
- end
- process({...})
- return r
- end
- -- Test table membership (crudely).
- local function contains(needle, haystack)
- for _, k in ipairs(haystack) do
- if (k == needle) then
- return true
- end
- end
- return false
- end
- local function inherit(high, low)
- local o = {}
- setmetatable(o, {
- __index = function(self, k)
- local x = high[k]
- if x then
- return x
- end
- return low[k]
- end
- })
- for k, v in pairs(high) do
- local _, _, kk = k:find("^%+(.*)$")
- if kk then
- o[kk] = concat(low[kk], v)
- end
- end
- return o
- end
- local function asstring(o)
- local t = type(o)
- if (t == "nil") then
- return ""
- elseif (t == "string") then
- return o
- elseif (t == "number") then
- return o
- elseif (t == "table") then
- if o.is then
- return asstring(o.outs)
- else
- local s = {}
- for _, v in pairs(o) do
- s[#s+1] = asstring(v)
- end
- return table.concat(s, " ")
- end
- else
- error(string.format("can't turn values of type '%s' into strings", t))
- end
- end
- local function concatpath(...)
- local p = table.concat({...}, "/")
- return (p:gsub("/+", "/"):gsub("^%./", ""):gsub("/%./", "/"))
- end
- -- Returns a list of the targets within the given collection; the keys of any
- -- keyed items are lost. Lists and wildcards are expanded.
- local function targetsof(...)
- local o = {}
- local function process(items)
- for _, item in pairs(items) do
- if (type(item) == "table") then
- if item.is then
- -- This is a target.
- o[#o+1] = item
- else
- -- This is a list.
- process(item)
- end
- elseif (type(item) == "string") then
- -- Filename!
- if item:find("^%+") then
- item = cwd..item
- elseif item:find("^%./") then
- item = concatpath(cwd, item)
- end
- o[#o+1] = loadtarget(item)
- else
- error(string.format("member of target list is not a string or a target"))
- end
- end
- end
- process({...})
- return o
- end
- local function filenamesof(...)
- local targets = targetsof(...)
- local f = {}
- for _, r in ipairs(targets) do
- if (type(r) == "table") and r.is then
- if r.outs then
- for _, o in ipairs(r.outs) do
- f[#f+1] = o
- end
- end
- elseif (type(r) == "string") then
- f[#f+1] = r
- else
- error(string.format("list of targets contains a %s which isn't a target",
- type(r)))
- end
- end
- return f
- end
- local function targetnamesof(...)
- local targets = targetsof(...)
- local f
- for _, r in pairs(targets) do
- if (type(r) == "table") and r.is then
- f[#f+1] = r.fullname
- elseif (type(r) == "string") then
- f[#f+1] = r
- else
- error(string.format("list of targets contains a %s which isn't a target",
- type(r)))
- end
- end
- return f
- end
- local function dotocollection(files, callback)
- if (#files == 1) and (type(files[1]) == "string") then
- return callback(files[1])
- end
- local o = {}
- local function process(files)
- for _, s in ipairs(files) do
- if (type(s) == "table") then
- if s.is then
- error("passed target to a filename manipulation function")
- else
- process(s)
- end
- else
- local b = callback(s)
- if (b ~= "") then
- o[#o+1] = b
- end
- end
- end
- end
- process(files)
- return o
- end
- local function abspath(...)
- return dotocollection({...},
- function(filename)
- assertString(filename, 1)
- if not filename:find("^[/$]") then
- filename = concatpath(posix.getcwd(), filename)
- end
- return filename
- end
- )
- end
- local function basename(...)
- return dotocollection({...},
- function(filename)
- assertString(filename, 1)
- local _, _, b = filename:find("^.*/([^/]*)$")
- if not b then
- return filename
- end
- return b
- end
- )
- end
- local function dirname(...)
- return dotocollection({...},
- function(filename)
- assertString(filename, 1)
- local _, _, b = filename:find("^(.*)/[^/]*$")
- if not b then
- return ""
- end
- return b
- end
- )
- end
- local function replace(files, pattern, repl)
- return dotocollection({files},
- function(filename)
- return filename:gsub(pattern, repl)
- end
- )
- end
- local function fpairs(...)
- return ipairs(filenamesof(...))
- end
- local function matching(collection, pattern)
- local o = {}
- dotocollection(collection,
- function(filename)
- if filename:find(pattern) then
- o[#o+1] = filename
- end
- end
- )
- return o
- end
-
- -- Selects all targets containing at least one output file that matches
- -- the pattern (or all, if the pattern is nil).
- local function selectof(targets, pattern)
- local targets = targetsof(targets)
- local o = {}
- for k, v in pairs(targets) do
- if v.is and v.outs then
- local matches = false
- for _, f in pairs(v.outs) do
- if f:find(pattern) then
- matches = true
- break
- end
- end
- if matches then
- o[#o+1] = v
- end
- end
- end
- return o
- end
- local function uniquify(...)
- local s = {}
- return dotocollection({...},
- function(filename)
- if not s[filename] then
- s[filename] = true
- return filename
- end
- end
- )
- end
- local function startswith(needle, haystack)
- return haystack:sub(1, #needle) == needle
- end
- local function emit(...)
- local n = select("#", ...)
- local args = {...}
- for i=1, n do
- local s = asstring(args[i])
- io.stdout:write(s)
- if not s:find("\n$") then
- io.stdout:write(" ")
- end
- end
- end
- local function templateexpand(list, vars)
- vars = inherit(vars, globals)
- local o = {}
- for _, s in ipairs(list) do
- o[#o+1] = s:gsub("%%%b{}",
- function(expr)
- expr = expr:sub(3, -2)
- local chunk, e = loadstring("return ("..expr..")", expr)
- if e then
- error(string.format("error evaluating expression: %s", e))
- end
- setfenv(chunk, vars)
- local value = chunk()
- if (value == nil) then
- error(string.format("template expression '%s' expands to nil (probably an undefined variable)", expr))
- end
- return asstring(value)
- end
- )
- end
- return o
- end
-
- local function loadbuildfile(filename)
- if contains(filename, loadingstack) then
- error(string.format("build file cycle; '%s' refers to itself indirectly; stack is: %s %s",
- filename, asstring(loadingstack), filename))
- end
- loadingstack[#loadingstack+1] = filename
- if not buildfiles[filename] then
- buildfiles[filename] = true
- local fp, data, chunk, e
- io.stderr:write("loading ", filename, "\n")
- fp, e = io.open(filename)
- if not e then
- data, e = fp:read("*a")
- fp:close()
- if not e then
- local thisglobals = {}
- thisglobals._G = thisglobals
- setmetatable(thisglobals, {__index = globals})
- chunk, e = loadstring(data, "@"..filename)
- if not e then
- setfenv(chunk, thisglobals)
- end
- end
- end
- if e then
- error(string.format("couldn't load '%s': %s", filename, e))
- end
- local oldcwd = cwd
- cwd = dirname(filename)
- chunk()
- cwd = oldcwd
- end
- loadingstack[#loadingstack] = nil
- end
- local function loadbuildfilefor(filepart, targetpart)
- local normalname = concatpath(filepart, "/build.lua")
- if posix.access(normalname, "r") then
- loadbuildfile(normalname)
- return
- end
- local extendedname = concatpath(filepart, "/build-"..targetpart..".lua")
- if posix.access(extendedname, "r") then
- loadbuildfile(extendedname)
- return
- end
- error(string.format("could not access either '%s' or '%s'", normalname, extendedname))
- end
- loadtarget = function(targetname)
- if targets[targetname] then
- return targets[targetname]
- end
- local target
- if not targetname:find("%+") then
- local files
- if targetname:find("[?*]") then
- files = posix.glob(targetname)
- if not files then
- files = {}
- end
- else
- files = {targetname}
- end
- target = {
- outs = files,
- is = {
- __implicitfile = true
- }
- }
- targets[targetname] = target
- else
- local _, _, filepart, targetpart = targetname:find("^([^+]*)%+([%w-_]+)$")
- if not filepart or not targetpart then
- error(string.format("malformed target name '%s'", targetname))
- end
- if (filepart == "") then
- filepart = cwd
- end
- loadbuildfilefor(filepart, targetpart)
- target = targets[targetname]
- if not target then
- error(string.format("build file '%s' contains no target '%s'",
- filename, targetpart))
- end
- end
- return target
- end
- local typeconverters = {
- targets = function(propname, i)
- if (type(i) == "string") then
- i = {i}
- elseif (type(i) ~= "table") then
- error(string.format("property '%s' must be a target list", propname))
- end
- local m = {}
- for k, v in pairs(i) do
- local ts = targetsof(v)
- if (type(k) == "number") then
- for _, t in ipairs(ts) do
- m[#m+1] = t
- end
- else
- if (#ts ~= 1) then
- error(string.format("named target '%s' can only be assigned from a single target", k))
- else
- m[k] = ts[1]
- end
- end
- end
- return m
- end,
- strings = function(propname, i)
- if (type(i) == "string") then
- i = {i}
- elseif (type(i) ~= "table") then
- error(string.format("property '%s' must be a string list", propname))
- end
- return concat(i)
- end,
- boolean = function(propname, i)
- if (type(i) ~= "boolean") then
- error(string.format("property '%s' must be a boolean", propname))
- end
- return i
- end,
- string = function(propname, i)
- if (type(i) ~= "string") then
- error(string.format("property '%s' must be a string", propname))
- end
- return i
- end,
- table = function(propname, i)
- if (type(i) ~= "table") then
- error(string.format("property '%s' must be a table", propname))
- end
- return i
- end,
- object = function(propname, i)
- return i
- end,
- }
-
- local function definerule(rulename, types, cb)
- if rulename and rules[rulename] then
- error(string.format("rule '%s' is already defined", rulename))
- end
- types.name = { type="string" }
- types.cwd = { type="string", optional=true }
- types.vars = { type="table", default={} }
- for propname, typespec in pairs(types) do
- if not typeconverters[typespec.type] then
- error(string.format("property '%s' has unrecognised type '%s'",
- propname, typespec.type))
- end
- end
- local rulecwd = cwd
- local rule = function(e)
- local definedprops = {}
- for propname, _ in pairs(e) do
- definedprops[propname] = true
- end
- local args = {}
- for propname, typespec in pairs(types) do
- if not e[propname] then
- if not typespec.optional and (typespec.default == nil) then
- error(string.format("missing mandatory property '%s'", propname))
- end
- args[propname] = typespec.default
- else
- args[propname] = typeconverters[typespec.type](propname, e[propname])
- definedprops[propname] = nil
- end
- end
- local propname, _ = next(definedprops)
- if propname then
- error(string.format("don't know what to do with property '%s'", propname))
- end
- if not args.cwd then
- args.cwd = cwd
- end
- args.fullname = args.cwd.."+"..args.name
- local oldparente = parente
- parente = args
- args.vars = inherit(args.vars, oldparente.vars)
- local result = cb(args) or {}
- parente = oldparente
- result.is = result.is or {}
- if rulename then
- result.is[rulename] = true
- end
- result.fullname = args.fullname
- if targets[arg.fullname] and (targets[arg.fullname] ~= result) then
- error(string.format("target '%s' is already defined", args.fullname))
- end
- targets[result.fullname] = result
- return result
- end
- if rulename then
- if rules[rulename] then
- error(string.format("rule '%s' is already defined", rulename))
- end
- rules[rulename] = rule
- end
- return rule
- end
- -----------------------------------------------------------------------------
- -- DEFAULT RULES --
- -----------------------------------------------------------------------------
- local function install_make_emitter()
- emit("hide = @\n")
- function emitter:var(name, value)
- -- Don't let emit insert spaces.
- emit(name.."="..value.."\n")
- end
- function emitter:rule(name, ins, outs)
- if (#outs == 0) then
- local n = name.."-IMAGINARY-OUT"
- emit(".INTERMEDIATE:", n, "\n")
- outs = {n}
- end
- local impl = name.."-IMPL"
- emit(".INTERMEDIATE:", name, "\n")
- emit(".INTERMEDIATE:", impl, "\n")
- for i = 1, #outs do
- emit(name..":", outs[i], "\n")
- end
- for i = 1, #outs do
- emit(outs[i]..":", impl, ";\n")
- end
- for i = 1, #ins do
- emit(impl..":", ins[i], "\n")
- end
- emit(impl..":", "\n")
- local dirs = uniquify(dirname(outs))
- if (#dirs > 0) then
- emit("\t@mkdir -p", dirs, "\n")
- end
- end
- function emitter:phony(name, ins, outs)
- emit(".PHONY:", name, "\n")
- self:rule(name, ins, outs)
- end
- function emitter:label(...)
- local s = table.concat({...}, " ")
- emit("\t@echo", s, "\n")
- end
- function emitter:exec(commands)
- for _, s in ipairs(commands) do
- emit("\t$(hide)", s, "\n")
- end
- end
- function emitter:endrule()
- emit("\n")
- end
- end
- local function install_ninja_emitter()
- emit("rule build\n")
- emit(" command = $command\n")
- emit("\n")
- local function unmake(collection)
- return dotocollection({collection},
- function(s)
- return s:gsub("%$%b()",
- function(expr)
- return "${"..expr:sub(3, -2).."}"
- end
- )
- end
- )
- end
- function emitter:var(name, value)
- -- Don't let emit insert spaces.
- emit(name.."="..unmake(value).."\n")
- end
- function emitter:rule(name, ins, outs)
- if (#outs == 0) then
- emit("build", name, ": phony", unmake(ins), "\n")
- else
- emit("build", name, ": phony", unmake(outs), "\n")
- emit("build", unmake(outs), ": build", unmake(ins), "\n")
- end
- end
- function emitter:label(...)
- end
- function emitter:exec(commands)
- emit(" command =", table.concat(unmake(commands), " && "), "\n")
- end
- function emitter:endrule()
- emit("\n")
- end
- end
- definerule("simplerule",
- {
- ins = { type="targets" },
- outs = { type="strings" },
- deps = { type="targets", default={} },
- label = { type="string", optional=true },
- commands = { type="strings" },
- vars = { type="table", default={} },
- },
- function (e)
- emitter:rule(e.fullname, filenamesof(e.ins, e.deps), e.outs)
- emitter:label(e.fullname, " ", e.label or "")
- local vars = inherit(e.vars, {
- ins = filenamesof(e.ins),
- outs = filenamesof(e.outs)
- })
- emitter:exec(templateexpand(e.commands, vars))
- emitter:endrule()
- return {
- outs = e.outs
- }
- end
- )
- definerule("installable",
- {
- map = { type="targets", default={} },
- },
- function (e)
- local deps = {}
- local commands = {}
- local srcs = {}
- local outs = {}
- local dests = {}
- for dest, src in pairs(e.map) do
- if src.is.installable then
- if (type(dest) ~= "number") then
- error("can't specify a destination filename when installing an installable")
- end
- deps[#deps+1] = src.fullname
- outs = concat(outs, filenamesof(src))
- elseif (type(dest) == "number") then
- error("only references to other installables can be missing a destination")
- else
- local f = filenamesof(src)
- if (#f ~= 1) then
- error("installable can only cope with targets emitting single files")
- end
- deps[#deps+1] = f
- dests[#dests+1] = dest
- outs[#outs+1] = dest
- commands[#commands+1] = "cp "..f[1].." "..dest
- end
- end
- emitter:rule(e.fullname, deps, dests)
- emitter:label(e.fullname, " ", e.label or "")
- if (#commands > 0) then
- emitter:exec(commands)
- end
- emitter:endrule()
- return {
- outs = outs
- }
- end
- )
- -----------------------------------------------------------------------------
- -- MAIN PROGRAM --
- -----------------------------------------------------------------------------
- local function parse_arguments(argmap, arg)
- local i = 1
- local files = {}
- local function unrecognisedarg(arg)
- argmap[" unrecognised"](arg)
- end
- while (i <= #arg) do
- local o = arg[i]
- local op
- if (o:byte(1) == 45) then
- -- This is an option.
- if (o:byte(2) == 45) then
- -- ...with a -- prefix.
- o = o:sub(3)
- local fn = argmap[o]
- if not fn then
- unrecognisedarg("--"..o)
- end
- i = i + fn(arg[i+1], arg[i+2])
- else
- -- ...without a -- prefix.
- local od = o:sub(2, 2)
- local fn = argmap[od]
- if not fn then
- unrecognisedarg("-"..od)
- end
- op = o:sub(3)
- if (op == "") then
- i = i + fn(arg[i+1], arg[i+2])
- else
- fn(op)
- end
- end
- else
- files[#files+1] = o
- end
- i = i + 1
- end
- argmap[" files"](files)
- end
- globals = {
- posix = posix,
- abspath = abspath,
- asstring = asstring,
- basename = basename,
- concat = concat,
- concatpath = concatpath,
- cwd = function() return cwd end,
- definerule = definerule,
- dirname = dirname,
- emit = emit,
- filenamesof = filenamesof,
- fpairs = fpairs,
- include = loadbuildfile,
- inherit = inherit,
- print = print,
- replace = replace,
- matching = matching,
- selectof = selectof,
- startswith = startswith,
- uniquify = uniquify,
- vars = vars,
- }
- setmetatable(globals,
- {
- __index = function(self, k)
- local rule = rules[k]
- if rule then
- return rule
- else
- return _G[k]
- end
- end
- }
- )
- vars.cflags = {}
- parente.vars = vars
- setmetatable(_G,
- {
- __index = function(self, k)
- local value = rawget(_G, k)
- if not value then
- error(string.format("access of undefined variable '%s'", k))
- end
- return value
- end
- }
- )
-
- do
- local emitter_type = install_make_emitter
- parse_arguments(
- {
- ["make"] = function()
- emitter_type = install_make_emitter
- return 0
- end,
- ["ninja"] = function()
- emitter_type = install_ninja_emitter
- return 0
- end,
- [" unrecognised"] = function(arg)
- error(string.format("unrecognised argument '%s'", arg))
- end,
- [" files"] = function(files)
- emitter_type()
- for _, f in ipairs(files) do
- local _, _, name, value = f:find("^([%w_]+)=(.*)$")
- if name then
- emitter:var(name, value)
- end
- end
-
- for _, f in ipairs(files) do
- if not f:find("=") then
- loadbuildfile(f)
- end
- end
- end
- },
- {...}
- )
- end
|