916 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			916 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
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'",
 | 
						|
				filepart, 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
 | 
						|
 |