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