2014-09-14 08:36:58 +02:00

915 lines
25 KiB
Lua

module(..., package.seeall)
local unitgen = require "tundra.unitgen"
local util = require "tundra.util"
local path = require "tundra.path"
local depgraph = require "tundra.depgraph"
local buildfile = require "tundra.buildfile"
local native = require "tundra.native"
local ide_backend = nil
local current = nil
local _nodegen = { }
_nodegen.__index = _nodegen
local function syntax_error(msg, ...)
error { Class = 'syntax error', Message = string.format(msg, ...) }
end
local function validate_boolean(name, value)
if type(value) == "boolean" then
return value
end
syntax_error("%s: expected boolean value, got %q", name, type(value))
end
local function validate_string(name, value)
if type(value) == "string" then
return value
end
syntax_error("%s: expected string value, got %q", name, type(value))
end
local function validate_pass(name, value)
if type(value) == "string" then
return value
else
syntax_error("%s: expected pass name, got %q", name, type(value))
end
end
local function validate_table(name, value)
-- A single string can be converted into a table value very easily
local t = type(value)
if t == "table" then
return value
elseif t == "string" then
return { value }
else
syntax_error("%s: expected table value, got %q", name, t)
end
end
local function validate_config(name, value)
if type(value) == "table" or type(value) == "string" then
return value
end
syntax_error("%s: expected config, got %q", name, type(value))
end
local validators = {
["string"] = validate_string,
["pass"] = validate_pass,
["table"] = validate_table,
["filter_table"] = validate_table,
["source_list"] = validate_table,
["boolean"] = validate_boolean,
["config"] = validate_config,
}
function _nodegen:validate()
local decl = self.Decl
for name, detail in pairs(assert(self.Blueprint)) do
local val = decl[name]
if not val then
if detail.Required then
syntax_error("%s: missing argument: '%s'", self.Keyword, name)
end
-- ok, optional value
else
local validator = validators[detail.Type]
decl[name] = validator(name, val)
end
end
for name, detail in pairs(decl) do
if not self.Blueprint[name] then
syntax_error("%s: unsupported argument: '%s'", self.Keyword, name)
end
end
end
function _nodegen:customize_env(env, raw_data)
-- available for subclasses
end
function _nodegen:configure_env(env, deps)
local build_id = env:get('BUILD_ID')
local propagate_blocks = {}
local decl = self.Decl
for _, dep_obj in util.nil_ipairs(deps) do
local data = dep_obj.Decl.Propagate
if data then
propagate_blocks[#propagate_blocks + 1] = data
end
end
local function push_bindings(env_key, data)
if data then
for _, item in util.nil_ipairs(flatten_list(build_id, data)) do
env:append(env_key, item)
end
end
end
local function replace_bindings(env_key, data)
if data then
local first = true
for _, item in util.nil_ipairs(flatten_list(build_id, data)) do
if first then
env:replace(env_key, item)
first = false
else
env:append(env_key, item)
end
end
end
end
-- Push Libs, Defines and so in into the environment of this unit.
-- These are named for convenience but are aliases for syntax niceness.
for decl_key, env_key in util.nil_pairs(self.DeclToEnvMappings) do
-- First pick settings from our own unit.
push_bindings(env_key, decl[decl_key])
for _, data in ipairs(propagate_blocks) do
push_bindings(env_key, data[decl_key])
end
end
-- Push Env blocks as is
for k, v in util.nil_pairs(decl.Env) do
push_bindings(k, v)
end
for k, v in util.nil_pairs(decl.ReplaceEnv) do
replace_bindings(k, v)
end
for _, block in util.nil_ipairs(propagate_blocks) do
for k, v in util.nil_pairs(block.Env) do
push_bindings(k, v)
end
for k, v in util.nil_pairs(block.ReplaceEnv) do
replace_bindings(k, v)
end
end
end
local function resolve_sources(env, items, accum, base_dir)
local ignored_exts = util.make_lookup_table(env:get_list("IGNORED_AUTOEXTS", {}))
for _, item in util.nil_ipairs(items) do
local type_name = type(item)
assert(type_name ~= "function")
if type_name == "userdata" then
accum[#accum + 1] = item
elseif type_name == "table" then
if depgraph.is_node(item) then
accum[#accum + 1] = item
elseif getmetatable(item) then
accum[#accum + 1] = item:get_dag(env)
else
resolve_sources(env, item, accum, item.SourceDir or base_dir)
end
else
assert(type_name == "string")
local ext = path.get_extension(item)
if not ignored_exts[ext] then
if not base_dir or path.is_absolute(item) then
accum[#accum + 1] = item
else
local p = path.join(base_dir, item)
accum[#accum + 1] = p
end
end
end
end
return accum
end
-- Analyze source list, returning list of input files and list of dependencies.
--
-- This is so you can pass a mix of actions producing files and regular
-- filenames as inputs to the next step in the chain and the output files of
-- such nodes will be used automatically.
--
-- list - list of source files and nodes that produce source files
-- suffixes - acceptable source suffixes to pick up from nodes in source list
local function analyze_sources(env, pass, list, suffixes)
if not list then
return nil
end
list = util.flatten(list)
local deps = {}
local function implicit_make(source_file)
local t = type(source_file)
if t == "table" then
return source_file
end
assert(t == "string")
local make = env:get_implicit_make_fn(source_file)
if make then
return make(env, pass, source_file)
else
return nil
end
end
local function transform(output, fn)
if type(fn) ~= "string" then
error(util.tostring(fn) .. " is not a string", 2)
end
local t = implicit_make(fn)
if t then
deps[#deps + 1] = t
t:insert_output_files(output, suffixes)
else
output[#output + 1] = fn
end
end
local files = {}
for _, src in ipairs(list) do
if depgraph.is_node(src) then
deps[#deps + 1] = src
src:insert_output_files(files, suffixes)
elseif type(src) == "table" then
error("non-DAG node in source list at this point")
else
files[#files + 1] = src
end
end
while true do
local result = {}
local old_dep_count = #deps
for _, src in ipairs(files) do
transform(result, src)
end
files = result
if #deps == old_dep_count then
--print("scan", util.tostring(list), util.tostring(suffixes), util.tostring(result))
return result, deps
end
end
end
local function x_identity(self, name, info, value, env, out_deps)
return value
end
local function x_source_list(self, name, info, value, env, out_deps)
local build_id = env:get('BUILD_ID')
local source_files
if build_id then
source_files = filter_structure(build_id, value)
else
source_files = value
end
local sources = resolve_sources(env, source_files, {}, self.Decl.SourceDir)
local source_exts = env:get_list(info.ExtensionKey)
local inputs, ideps = analyze_sources(env, resolve_pass(self.Decl.Pass), sources, source_exts)
if ideps then
util.append_table(out_deps, ideps)
end
return inputs
end
local function x_filter_table(self, name, info, value, env, out_deps)
local build_id = env:get('BUILD_ID')
return flatten_list(build_id, value)
end
local function find_named_node(name_or_dag)
if type(name_or_dag) == "table" then
return name_or_dag:get_dag(current.default_env)
elseif type(name_or_dag) == "string" then
local generator = current.units[name_or_dag]
if not generator then
errorf("unknown node specified: %q", tostring(name_or_dag))
end
return generator:get_dag(current.default_env)
else
errorf("illegal node specified: %q", tostring(name_or_dag))
end
end
-- Special resolver for dependencies in a nested (config-filtered) list.
local function resolve_dependencies(decl, raw_deps, env)
if not raw_deps then
return {}
end
local build_id = env:get('BUILD_ID')
local deps = flatten_list(build_id, raw_deps)
return util.map_in_place(deps, function (i)
if type(i) == "string" then
local n = current.units[i]
if not n then
errorf("%s: Unknown 'Depends' target %q", decl.Name, i)
end
return n
elseif type(i) == "table" and getmetatable(i) and i.Decl then
return i
else
errorf("bad 'Depends' value of type %q", type(i))
end
end)
end
local function x_pass(self, name, info, value, env, out_deps)
return resolve_pass(value)
end
local decl_transformers = {
-- the x_identity data types have already been checked at script time through validate_xxx
["string"] = x_identity,
["table"] = x_identity,
["config"] = x_identity,
["boolean"] = x_identity,
["pass"] = x_pass,
["source_list"] = x_source_list,
["filter_table"] = x_filter_table,
}
-- Create input data for the generator's DAG creation function based on the
-- blueprint passed in when the generator was registered. This is done here
-- centrally rather than in all the different node generators to reduce code
-- duplication and keep the generators miminal. If you need to do something
-- special, you can override create_input_data() in your subclass.
function _nodegen:create_input_data(env)
local decl = self.Decl
local data = {}
local deps = {}
for name, detail in pairs(assert(self.Blueprint)) do
local val = decl[name]
if val then
local xform = decl_transformers[detail.Type]
data[name] = xform(self, name, detail, val, env, deps)
end
end
return data, deps
end
function get_pass(self, name)
if not name then
return nil
end
end
local pattern_cache = {}
local function get_cached_pattern(p)
local v = pattern_cache[p]
if not v then
local comp = '[%w_]+'
local sub_pattern = p:gsub('*', '[%%w_]+')
local platform, tool, variant, subvariant = unitgen.match_build_id(sub_pattern, comp)
v = string.format('^%s%%-%s%%-%s%%-%s$', platform, tool, variant, subvariant)
pattern_cache[p] = v
end
return v
end
local function config_matches(pattern, build_id)
local ptype = type(pattern)
if ptype == "nil" then
return true
elseif ptype == "string" then
local fpattern = get_cached_pattern(pattern)
return build_id:match(fpattern)
elseif ptype == "table" then
for _, pattern_item in ipairs(pattern) do
if config_matches(pattern_item, build_id) then
return true
end
end
return false
else
error("bad 'Config' pattern type: " .. ptype)
end
end
local function make_unit_env(unit)
-- Select an environment for this unit based on its SubConfig tag
-- to support cross compilation.
local env
local subconfig = unit.Decl.SubConfig or current.default_subconfig
if subconfig and current.base_envs then
env = current.base_envs[subconfig]
if Options.VeryVerbose then
if env then
printf("%s: using subconfig %s (%s)", unit.Decl.Name, subconfig, env:get('BUILD_ID'))
else
if current.default_subconfig then
errorf("%s: couldn't find a subconfig env", unit.Decl.Name)
else
printf("%s: no subconfig %s found; using default env", unit.Decl.Name, subconfig)
end
end
end
end
if not env then
env = current.default_env
end
return env:clone()
end
local anon_count = 1
function _nodegen:get_dag(parent_env)
local build_id = parent_env:get('BUILD_ID')
local dag = self.DagCache[build_id]
if not dag then
if build_id:len() > 0 and not config_matches(self.Decl.Config, build_id) then
-- Unit has been filtered out via Config attribute.
-- Create a fresh dummy node for it.
local name
if not self.Decl.Name then
name = string.format("Dummy node %d", anon_count)
else
name = string.format("Dummy node %d for %s", anon_count, self.Decl.Name)
end
anon_count = anon_count + 1
dag = depgraph.make_node {
Env = parent_env,
Pass = resolve_pass(self.Decl.Pass),
Label = name,
}
else
local unit_env = make_unit_env(self)
if self.Decl.Name then
unit_env:set('UNIT_PREFIX', '__' .. self.Decl.Name)
end
-- Before accessing the unit's dependencies, resolve them via filtering.
local deps = resolve_dependencies(self.Decl, self.Decl.Depends, unit_env)
self:configure_env(unit_env, deps)
self:customize_env(unit_env, self.Decl, deps)
local input_data, input_deps = self:create_input_data(unit_env, parent_env)
-- Copy over dependencies which have been pre-resolved
input_data.Depends = deps
for _, dep in util.nil_ipairs(deps) do
input_deps[#input_deps + 1] = dep:get_dag(parent_env)
end
dag = self:create_dag(unit_env, input_data, input_deps, parent_env)
if not dag then
error("create_dag didn't generate a result node")
end
end
self.DagCache[build_id] = dag
end
return dag
end
local _generator = {
Evaluators = {},
}
_generator.__index = _generator
local function new_generator(s)
s = s or {}
s.units = {}
return setmetatable(s, _generator)
end
local function create_unit_map(state, raw_nodes)
-- Build name=>decl mapping
for _, unit in ipairs(raw_nodes) do
assert(unit.Decl)
local name = unit.Decl.Name
if name and type(name) == "string" then
if state.units[name] then
errorf("duplicate unit name: %s", name)
end
state.units[name] = unit
end
end
end
function _generate_dag(args)
local envs = assert(args.Envs)
local raw_nodes = assert(args.Declarations)
local state = new_generator {
base_envs = envs,
root_env = envs["__default"], -- the outmost config's env in a cross-compilation scenario
config = assert(args.Config),
variant = assert(args.Variant),
passes = assert(args.Passes),
}
current = state
create_unit_map(state, raw_nodes)
local subconfigs = state.config.SubConfigs
-- Pick a default environment which is used for
-- 1. Nodes without a SubConfig declaration
-- 2. Nodes with a missing SubConfig declaration
-- 3. All nodes if there are no SubConfigs set for the current config
if subconfigs then
state.default_subconfig = assert(state.config.DefaultSubConfig)
state.default_env = assert(envs[state.default_subconfig], "unknown DefaultSubConfig specified")
else
state.default_env = assert(envs["__default"])
end
local always_lut = util.make_lookup_table(args.AlwaysNodes)
local default_lut = util.make_lookup_table(args.DefaultNodes)
local always_nodes = util.map(args.AlwaysNodes, find_named_node)
local default_nodes = util.map(args.DefaultNodes, find_named_node)
local named_nodes = {}
for name, _ in pairs(state.units) do
named_nodes[name] = find_named_node(name)
end
current = nil
return { always_nodes, default_nodes, named_nodes }
end
function generate_dag(args)
local success, result = xpcall(function () return _generate_dag(args) end, buildfile.syntax_error_catcher)
if success then
return result[1], result[2], result[3]
else
croak("%s", result)
end
end
function resolve_pass(name)
assert(current)
if name then
local p = current.passes[name]
if not p then
syntax_error("%q is not a valid pass name", name)
end
return p
else
return nil
end
end
function get_target(data, suffix, prefix)
local target = data.Target
if not target then
assert(data.Name)
target = "$(OBJECTDIR)/" .. (prefix or "") .. data.Name .. (suffix or "")
end
return target
end
function get_evaluator(name)
return _generator.Evaluators[name]
end
function is_evaluator(name)
if _generator.Evaluators[name] then return true else return false end
end
local common_blueprint = {
Propagate = {
Help = "Declarations to propagate to dependent units",
Type = "filter_table",
},
Depends = {
Help = "Dependencies for this node",
Type = "table", -- handled specially
},
Env = {
Help = "Data to append to the environment for the unit",
Type = "filter_table",
},
ReplaceEnv = {
Help = "Data to replace in the environment for the unit",
Type = "filter_table",
},
Pass = {
Help = "Specify build pass",
Type = "pass",
},
SourceDir = {
Help = "Specify base directory for source files",
Type = "string",
},
Config = {
Help = "Specify configuration this unit will build in",
Type = "config",
},
SubConfig = {
Help = "Specify sub-configuration this unit will build in",
Type = "config",
},
__DagNodes = {
Help = "Internal node to keep track of DAG nodes generated so far",
Type = "table",
}
}
function create_eval_subclass(meta_tbl, base)
base = base or _nodegen
setmetatable(meta_tbl, base)
meta_tbl.__index = meta_tbl
return meta_tbl
end
function add_evaluator(name, meta_tbl, blueprint)
assert(type(name) == "string")
assert(type(meta_tbl) == "table")
assert(type(blueprint) == "table")
-- Set up this metatable as a subclass of _nodegen unless it is already
-- configured.
if not getmetatable(meta_tbl) then
setmetatable(meta_tbl, _nodegen)
meta_tbl.__index = meta_tbl
end
-- Install common blueprint items.
for name, val in pairs(common_blueprint) do
if not blueprint[name] then
blueprint[name] = val
end
end
-- Expand environment shortcuts into options.
for decl_key, env_key in util.nil_pairs(meta_tbl.DeclToEnvMappings) do
blueprint[decl_key] = {
Type = "filter_table",
Help = "Shortcut for environment key " .. env_key,
}
end
for name, val in pairs(blueprint) do
local type_ = assert(val.Type)
if not validators[type_] then
errorf("unsupported blueprint type %q", type_)
end
if val.Type == "source_list" and not val.ExtensionKey then
errorf("%s: source_list must provide ExtensionKey", name)
end
end
-- Record blueprint for use when validating user constructs.
meta_tbl.Keyword = name
meta_tbl.Blueprint = blueprint
-- Store this evaluator under the keyword that will trigger it.
_generator.Evaluators[name] = meta_tbl
end
-- Called when processing build scripts, keywords is something previously
-- registered as an evaluator here.
function evaluate(eval_keyword, data)
local meta_tbl = assert(_generator.Evaluators[eval_keyword])
-- Give the evaluator change to fix up the data before we validate it.
data = meta_tbl:preprocess_data(data)
local object = setmetatable({
DagCache = {}, -- maps BUILD_ID -> dag node
Decl = data
}, meta_tbl)
-- Expose the dag cache to the raw input data so the IDE generator can find it later
data.__DagNodes = object.DagCache
object.__index = object
-- Validate data according to Blueprint settings
object:validate()
return object
end
-- Given a list of strings or nested lists, flatten the structure to a single
-- list of strings while applying configuration filters. Configuration filters
-- match against the current build identifier like this:
--
-- { "a", "b", { "nixfile1", "nixfile2"; Config = "unix-*-*" }, "bar", { "debugfile"; Config = "*-*-debug" }, }
--
-- If 'exclusive' is set, then:
-- If 'build_id' is set, only values _with_ a 'Config' filter are included.
-- If 'build_id' is nil, only values _without_ a 'Config' filter are included.
function flatten_list(build_id, list, exclusive)
if not list then return nil end
local filter_defined = build_id ~= nil
-- Helper function to apply filtering recursively and append results to an
-- accumulator table.
local function iter(node, accum, filtered)
local node_type = type(node)
if node_type == "table" and not getmetatable(node) then
if node.Config then filtered = true end
if not filter_defined or config_matches(node.Config, build_id) then
for _, item in ipairs(node) do
iter(item, accum, filtered)
end
end
elseif not exclusive or (filtered == filter_defined) then
accum[#accum + 1] = node
end
end
local results = {}
iter(list, results, false)
return results
end
-- Conceptually similar to flatten_list(), but retains table structure.
-- Use to keep source tables as they are passed in, to retain nested SourceDir attributes.
local empty_leaf = {} -- constant
function filter_structure(build_id, data, exclusive)
if type(data) == "table" then
if getmetatable(data) then
return data -- it's already a DAG node; use as-is
end
local filtered = data.Config and true or false
if not data.Config or config_matches(data.Config, build_id) then
local result = {}
for k, item in pairs(data) do
if type(k) == "number" then
-- Filter array elements.
result[#result + 1] = filter_structure(build_id, item, filtered)
elseif k ~= "Config" then
-- Copy key-value data through.
result[k] = item
end
end
return result
else
return empty_leaf
end
else
return data
end
end
-- Processes an "Env" table. For each value, the corresponding variable in
-- 'env' is appended to if its "Config" filter matches 'build_id'. If
-- 'build_id' is nil, filtered values are skipped.
function append_filtered_env_vars(env, values_to_append, build_id, exclusive)
for key, val in util.pairs(values_to_append) do
if type(val) == "table" then
local list = flatten_list(build_id, val, exclusive)
for _, subvalue in ipairs(list) do
env:append(key, subvalue)
end
elseif not (exclusive and build_id) then
env:append(key, val)
end
end
end
-- Like append_filtered_env_vars(), but replaces existing variables instead
-- of appending to them.
function replace_filtered_env_vars(env, values_to_replace, build_id, exclusive)
for key, val in util.pairs(values_to_replace) do
if type(val) == "table" then
local list = flatten_list(build_id, val, exclusive)
if #list > 0 then
env:replace(key, list)
end
elseif not (exclusive and build_id) then
env:replace(key, val)
end
end
end
function generate_ide_files(config_tuples, default_names, raw_nodes, env, hints, ide_script)
local state = new_generator { default_env = env }
assert(state.default_env)
create_unit_map(state, raw_nodes)
local backend_fn = assert(ide_backend)
backend_fn(state, config_tuples, raw_nodes, env, default_names, hints, ide_script)
end
function set_ide_backend(backend_fn)
ide_backend = backend_fn
end
-- Expose the DefRule helper which is used to register builder syntax in a
-- simplified way.
function _G.DefRule(ruledef)
local name = assert(ruledef.Name, "Missing Name string in DefRule")
local setup_fn = assert(ruledef.Setup, "Missing Setup function in DefRule " .. name)
local cmd = assert(ruledef.Command, "Missing Command string in DefRule " .. name)
local blueprint = assert(ruledef.Blueprint, "Missing Blueprint in DefRule " .. name)
local mt = create_eval_subclass {}
local annot = ruledef.Annotation
if not annot then
annot = name .. " $(<)"
end
local preproc = ruledef.Preprocess
local function verify_table(v, tag)
if not v then
errorf("No %s returned from DefRule %s", tag, name)
end
if type(v) ~= "table" then
errorf("%s returned from DefRule %s is not a table", tag, name)
end
end
local function make_node(input_files, output_files, env, data, deps, scanner)
return depgraph.make_node {
Env = env,
Label = annot,
Action = cmd,
Pass = data.Pass or resolve_pass(ruledef.Pass),
InputFiles = input_files,
OutputFiles = output_files,
ImplicitInputs = ruledef.ImplicitInputs,
Scanner = scanner,
Dependencies = deps,
}
end
if ruledef.ConfigInvariant then
local cache = {}
function mt:create_dag(env, data, deps)
local setup_data = setup_fn(env, data)
local input_files = setup_data.InputFiles
local output_files = setup_data.OutputFiles
verify_table(input_files, "InputFiles")
verify_table(output_files, "OutputFiles")
local mashup = { }
for _, input in util.nil_ipairs(input_files) do
mashup[#mashup + 1] = input
end
mashup[#mashup + 1] = "@@"
for _, output in util.nil_ipairs(output_files) do
mashup[#mashup + 1] = output
end
mashup[#mashup + 1] = "@@"
for _, implicit_input in util.nil_ipairs(setup_data.ImplicitInputs) do
mashup[#mashup + 1] = implicit_input
end
local key = native.digest_guid(table.concat(mashup, ';'))
local key = util.tostring(key)
if cache[key] then
return cache[key]
else
local node = make_node(input_files, output_files, env, data, deps, setup_data.Scanner)
cache[key] = node
return node
end
end
else
function mt:create_dag(env, data, deps)
local setup_data = setup_fn(env, data)
verify_table(setup_data.InputFiles, "InputFiles")
verify_table(setup_data.OutputFiles, "OutputFiles")
return make_node(setup_data.InputFiles, setup_data.OutputFiles, env, data, deps, setup_data.Scanner)
end
end
if preproc then
function mt:preprocess_data(raw_data)
return preproc(raw_data)
end
end
add_evaluator(name, mt, blueprint)
end
function _nodegen:preprocess_data(data)
return data
end