--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-2020, TBOOX Open Source Group. -- -- @author ruki -- @file project.lua -- -- define module: project local project = project or {} -- load modules local os = require("base/os") local io = require("base/io") local path = require("base/path") local task = require("base/task") local utils = require("base/utils") local table = require("base/table") local global = require("base/global") local process = require("base/process") local deprecated = require("base/deprecated") local interpreter = require("base/interpreter") local rule = require("project/rule") local target = require("project/target") local config = require("project/config") local option = require("project/option") local requireinfo = require("project/requireinfo") local deprecated_project = require("project/deprecated/project") local package = require("package/package") local platform = require("platform/platform") local environment = require("platform/environment") local language = require("language/language") local sandbox_os = require("sandbox/modules/os") local sandbox_module = require("sandbox/modules/import/core/sandbox/module") -- the current os is belong to the given os? function project._api_is_os(interp, ...) -- get the current os local os = platform.os() if not os then return false end -- exists this os? for _, o in ipairs(table.join(...)) do if o and type(o) == "string" and o == os then return true end end end -- the current mode is belong to the given modes? function project._api_is_mode(interp, ...) return config.is_mode(...) end -- the current platform is belong to the given platforms? function project._api_is_plat(interp, ...) return config.is_plat(...) end -- the current platform is belong to the given architectures? function project._api_is_arch(interp, ...) return config.is_arch(...) end -- the current kind is belong to the given kinds? function project._api_is_kind(interp, ...) -- get the current kind local kind = config.get("kind") if not kind then return false end -- exists this kind? for _, k in ipairs(table.join(...)) do if k and type(k) == "string" and k == kind then return true end end end -- the current host is belong to the given hosts? function project._api_is_host(interp, ...) return os.is_host(...) end -- the current subsystem host is belong to the given hosts? function project._api_is_subhost(interp, ...) return os.is_subhost(...) end -- the current config is belong to the given config values? function project._api_is_config(interp, name, ...) return config.is_value(name, ...) end -- some configs are enabled? function project._api_has_config(interp, ...) return config.has(...) end -- some packages are enabled? function project._api_has_package(interp, ...) -- only for loading targets local requires = project._REQUIRES if requires then for _, name in ipairs(table.join(...)) do local pkg = requires[name] if pkg and pkg:enabled() then return true end end end end -- get config from the given name function project._api_get_config(interp, name) return config.get(name) end -- add module directories function project._api_add_moduledirs(interp, ...) sandbox_module.add_directories(...) end -- add plugin directories load all plugins from the given directories function project._api_add_plugindirs(interp, ...) -- get all directories local plugindirs = {} local dirs = table.join(...) for _, dir in ipairs(dirs) do table.insert(plugindirs, dir .. "/*") end -- add all plugins interp:api_builtin_includes(plugindirs) end -- add platform directories function project._api_add_platformdirs(interp, ...) platform.add_directories(...) end -- get interpreter function project.interpreter() -- the interpreter has been initialized? return it directly if project._INTERPRETER then return project._INTERPRETER end -- init interpreter local interp = interpreter.new() assert(interp) -- set root directory interp:rootdir_set(project.directory()) -- set root scope interp:rootscope_set("target") -- define apis for rule interp:api_define(rule.apis()) -- define apis for task interp:api_define(task.apis()) -- define apis for target interp:api_define(target.apis()) -- define apis for option interp:api_define(option.apis()) -- define apis for package interp:api_define(package.apis()) -- define apis for language interp:api_define(language.apis()) -- define apis for project interp:api_define { values = { -- set_xxx "set_project" , "set_modes" -- TODO deprecated , "set_description" -- add_xxx , "add_requires" , "add_repositories" } , pathes = { -- add_xxx "add_packagedirs" } , keyvalues = { "set_config" } , custom = { -- is_xxx {"is_os", project._api_is_os } , {"is_kind", project._api_is_kind } , {"is_arch", project._api_is_arch } , {"is_host", project._api_is_host } , {"is_subhost", project._api_is_subhost } , {"is_mode", project._api_is_mode } , {"is_plat", project._api_is_plat } , {"is_config", project._api_is_config } -- get_xxx , {"get_config", project._api_get_config } -- has_xxx , {"has_config", project._api_has_config } , {"has_package", project._api_has_package } -- add_xxx , {"add_moduledirs", project._api_add_moduledirs } , {"add_plugindirs", project._api_add_plugindirs } , {"add_platformdirs", project._api_add_platformdirs } } } -- register api: deprecated deprecated_project.api_register(interp) -- set filter interp:filter():register("project", function (variable) -- check assert(variable) -- hack buildir first if variable == "buildir" then return config.buildir() end -- attempt to get it directly from the configure local result = config.get(variable) if not result or type(result) ~= "string" then -- init maps local maps = { os = platform.os() , host = os.host() , subhost = os.subhost() , prefix = "$(prefix)" , tmpdir = function () return os.tmpdir() end , curdir = function () return os.curdir() end , scriptdir = function () return sandbox_os.scriptdir() end , globaldir = global.directory() , configdir = config.directory() , projectdir = project.directory() , programdir = os.programdir() } -- map it result = maps[variable] if type(result) == "function" then result = result() end -- attempt to get it from the platform tools, e.g. cc, cxx, ld .. -- because these values may not exist in config cache when call `config.get()`, we need check and get it. -- if not result then result = platform.tool(variable) end end -- ok? return result end) -- save interpreter project._INTERPRETER = interp -- ok? return interp end -- get the root project file function project.rootfile() return os.projectfile() end -- get all loaded project files with subfiles (xmake.lua) function project.allfiles() local rcfile = project.rcfile() if rcfile and os.isfile(rcfile) then return table.join(project.interpreter():scriptfiles(), rcfile) else return project.interpreter():scriptfiles() end end -- get the global rcfile: ~/.xmakerc.lua function project.rcfile() local xmakerc = project._XMAKE_RCFILE if xmakerc == nil then xmakerc = "/etc/xmakerc.lua" if not os.isfile(xmakerc) then xmakerc = "~/.xmakerc.lua" if not os.isfile(xmakerc) then xmakerc = path.join(global.directory(), "xmakerc.lua") end end project._XMAKE_RCFILE = xmakerc end return xmakerc end -- get the project directory function project.directory() return os.projectdir() end -- get the filelock of the whole project directory function project.filelock() local filelock = project._FILELOCK if filelock == nil then filelock = io.openlock(path.join(config.directory(), "project.lock")) project._FILELOCK = filelock end return filelock end -- get the project info from the given name function project.get(name) return project._INFO and project._INFO:get(name) or nil end -- load the project file function project._load(force, disable_filter) -- has already been loaded? if project._INFO and not force then return true end -- enter the project directory local oldir, errors = os.cd(os.projectdir()) if not oldir then return false, errors end -- get interpreter local interp = project.interpreter() -- load script local ok, errors = interp:load(project.rootfile(), {on_load_data = function (data) local xmakerc_file = project.rcfile() if xmakerc_file and os.isfile(xmakerc_file) then local rcdata = io.readfile(xmakerc_file) if rcdata then data = rcdata .. "\n" .. data end end return data end}) if not ok then return false, (errors or "load project file failed!") end -- load the root info of the project local rootinfo, errors = project._load_scope("root", true, not disable_filter) if not rootinfo then return false, errors end -- load the root info of the target local rootinfo_target, errors = project._load_scope("root.target", true, not disable_filter) if not rootinfo_target then return false, errors end -- save the root info for name, value in pairs(rootinfo_target:info()) do rootinfo:set("target." .. name, value) end project._INFO = rootinfo -- leave the project directory oldir, errors = os.cd(oldir) if not oldir then return false, errors end return true end -- load deps for instance: e.g. option, target and rule -- -- e.g. -- -- a.deps = b -- b.deps = c -- -- orderdeps: c -> b -> a -- function project._load_deps(instance, instances, deps, orderdeps) -- get dep instances for _, dep in ipairs(table.wrap(instance:get("deps"))) do local depinst = instances[dep] if depinst then project._load_deps(depinst, instances, deps, orderdeps) if not deps[dep] then deps[dep] = depinst table.insert(orderdeps, depinst) end end end end -- load scope from the project file function project._load_scope(scope_kind, remove_repeat, enable_filter) -- enter the project directory local oldir, errors = os.cd(os.projectdir()) if not oldir then return nil, errors end -- get interpreter local interp = project.interpreter() -- load scope local results, errors = interp:make(scope_kind, remove_repeat, enable_filter) if not results then return nil, errors end -- leave the project directory oldir, errors = os.cd(oldir) if not oldir then return nil, errors end return results end -- load tasks function project._load_tasks() -- the project file is not found? if not os.isfile(project.rootfile()) then return {}, nil end -- load the project file first and disable filter local ok, errors = project._load(true, true) if not ok then return nil, errors end -- load the tasks from the the project file local results, errors = project._load_scope("task", true, true) if not results then return nil, errors or "load project tasks failed!" end -- bind tasks for menu with an sandbox instance local ok, errors = task._bind(results, project.interpreter()) if not ok then return nil, errors end -- make task instances local tasks = {} for taskname, taskinfo in pairs(results) do tasks[taskname] = task.new(taskname, taskinfo) end return tasks end -- load rules function project._load_rules() -- load the project file first if has not been loaded? local ok, errors = project._load() if not ok then return nil, errors end -- load the rules from the the project file local results, errors = project._load_scope("rule", true, true) if not results then return nil, errors end -- make rule instances local rules = {} for rulename, ruleinfo in pairs(results) do rules[rulename] = rule.new(rulename, ruleinfo) end -- load rule deps local instances = table.join(rule.rules(), rules) for _, instance in pairs(instances) do instance._DEPS = instance._DEPS or {} instance._ORDERDEPS = instance._ORDERDEPS or {} project._load_deps(instance, instances, instance._DEPS, instance._ORDERDEPS) end return rules end -- load targets function project._load_targets() -- load all requires first and reload the project file to ensure has_package() works for targets local requires = project.requires() local ok, errors = project._load(true) if not ok then return nil, errors end -- load targets local results, errors = project._load_scope("target", true, true) if not results then return nil, errors end -- make targets local targets = {} for targetname, targetinfo in pairs(results) do local t = target.new(targetname, targetinfo, project) if t and (t:get("enabled") == nil or t:get("enabled") == true) then targets[targetname] = t end end -- load and attach target deps, rules and packages for _, t in pairs(targets) do -- load deps t._DEPS = t._DEPS or {} t._ORDERDEPS = t._ORDERDEPS or {} project._load_deps(t, targets, t._DEPS, t._ORDERDEPS) -- load rules from target and language -- -- e.g. -- -- a.deps = b -- b.deps = c -- -- orderules: c -> b -> a -- t._RULES = t._RULES or {} t._ORDERULES = t._ORDERULES or {} local rulenames = {} local extensions = {} table.join2(rulenames, t:get("rules")) for _, sourcefile in ipairs(table.wrap(t:get("files"))) do local extension = path.extension((sourcefile:gsub("|.*$", ""))) if not extensions[extension] then local lang = language.load_ex(extension) if lang and lang:rules() then table.join2(rulenames, lang:rules()) end extensions[extension] = true end end rulenames = table.unique(rulenames) for _, rulename in ipairs(rulenames) do local r = project.rule(rulename) or rule.rule(rulename) if r then t._RULES[rulename] = r for _, deprule in ipairs(r:orderdeps()) do local name = deprule:name() if not t._RULES[name] then t._RULES[name] = deprule table.insert(t._ORDERULES, deprule) end end table.insert(t._ORDERULES, r) else return nil, string.format("unknown rule(%s) in target(%s)!", rulename, t:name()) end end -- laod packages t._PACKAGES = t._PACKAGES or {} for _, packagename in ipairs(table.wrap(t:get("packages"))) do local p = requires[packagename] if p then table.insert(t._PACKAGES, p) end end end -- enter toolchains environment environment.enter("toolchains") -- do load for each target local ok = false for _, t in pairs(targets) do ok, errors = t:_load() if not ok then break end end -- leave toolchains environment environment.leave("toolchains") -- do load failed? if not ok then return nil, errors end -- ok return targets end -- load options function project._load_options(disable_filter) -- the project file is not found? if not os.isfile(project.rootfile()) then return {}, nil end -- reload the project file to ensure `if is_plat() then add_packagedirs() end` works local ok, errors = project._load(true, disable_filter) if not ok then return nil, errors end -- load the options from the the project file local results, errors = project._load_scope("option", true, not disable_filter) if not results then return nil, errors end -- load the options from the package directories, e.g. packagedir/*.pkg for _, packagedir in ipairs(table.wrap(project.get("packagedirs"))) do local packagefiles = os.files(path.join(packagedir, "*.pkg", "xmake.lua")) if packagefiles then for _, packagefile in ipairs(packagefiles) do -- load the package file local interp = option.interpreter() local ok, errors = interp:load(packagefile) if not ok then return nil, errors end -- load the package options from the the package file local packageinfos, errors = interp:make("option", true, not disable_filter) if not packageinfos then return nil, errors end -- transform includedirs and linkdirs local rootdir = path.directory(packagefile) for _, packageinfo in pairs(packageinfos) do local linkdirs = {} local includedirs = {} for _, linkdir in ipairs(table.wrap(packageinfo:get("linkdirs"))) do table.insert(linkdirs, path.is_absolute(linkdir) and linkdir or path.join(rootdir, linkdir)) end for _, includedir in ipairs(table.wrap(packageinfo:get("includedirs"))) do table.insert(includedirs, path.is_absolute(includedir) and includedir or path.join(rootdir, includedir)) end if #linkdirs > 0 then packageinfo:set("linkdirs", linkdirs) end if #includedirs > 0 then packageinfo:set("includedirs", includedirs) end end table.join2(results, packageinfos) end end end -- check options local options = {} for optionname, optioninfo in pairs(results) do -- init an option instance local instance = option.new(optionname, optioninfo) -- save it options[optionname] = instance -- mark add_defines_h_if_ok and add_undefines_h_if_ok as deprecated if instance:get("defines_h_if_ok") then deprecated.add("add_defines_h(\"%s\")", "add_defines_h_if_ok(\"%s\")", table.concat(table.wrap(instance:get("defines_h_if_ok")), "\", \"")) end if instance:get("undefines_h_if_ok") then deprecated.add("add_undefines_h(\"%s\")", "add_undefines_h_if_ok(\"%s\")", table.concat(table.wrap(instance:get("undefines_h_if_ok")), "\", \"")) end end -- load and attach options deps for _, opt in pairs(options) do opt._DEPS = opt._DEPS or {} opt._ORDERDEPS = opt._ORDERDEPS or {} project._load_deps(opt, options, opt._DEPS, opt._ORDERDEPS) end -- ok? return options end -- load requires function project._load_requires() -- parse requires local requires = {} local requires_str, requires_extra = project.requires_str() requires_extra = requires_extra or {} for _, requirestr in ipairs(table.wrap(requires_str)) do -- get the package name local packagename = requirestr:split('%s')[1] -- get alias local alias = nil local extrainfo = requires_extra[requirestr] if extrainfo then alias = extrainfo.alias end -- load it from cache first (@note will discard scripts in extrainfo) local instance = requireinfo.load(alias or packagename) if not instance then -- init a require info instance instance = table.inherit(requireinfo) -- save name and info instance._NAME = alias or packagename instance._INFO = { __requirestr = requirestr, __extrainfo = extrainfo } end -- move scripts of extrainfo (e.g. on_load ..) if extrainfo then for k, v in pairs(extrainfo) do if type(v) == "function" then instance._SCRIPTS = instance._SCRIPTS or {} instance._SCRIPTS[k] = v extrainfo[k] = nil end end -- TODO exists deprecated option? show tips if extrainfo.option then os.raise("`option = {}` is no longger supported in add_requires(), please update xmake.lua") end end -- add require info requires[alias or packagename] = instance end -- ok? return requires end -- load the packages from the the project file and disable filter, we will process filter after a while function project._load_packages() -- load the project file first if has not been loaded? local ok, errors = project._load() if not ok then return nil, errors end -- load packages return project._load_scope("package", true, false) end -- clear project cache to reload targets and options function project.clear() -- clear options status in config file first for _, opt in ipairs(table.wrap(project._OPTIONS)) do opt:clear() end -- clear targets and options project._TARGETS = nil project._OPTIONS = nil end -- get the given target function project.target(name) return project.targets()[name] end -- get the current configure for targets function project.targets() -- load targets if not project._TARGETS then local targets, errors = project._load_targets() if not targets then os.raise(errors) end project._TARGETS = targets end -- ok return project._TARGETS end -- get the given option function project.option(name) return project.options()[name] end -- get options function project.options() -- load options and enable filter if not project._OPTIONS then local options, errors = project._load_options() if not options then os.raise(errors) end project._OPTIONS = options end -- ok return project._OPTIONS end -- get the given require info function project.require(name) return project.requires()[name] end -- get requires info function project.requires() if not project._REQUIRES then local requires, errors = project._load_requires() if not requires then os.raise(errors) end project._REQUIRES = requires end return project._REQUIRES end -- get string requires function project.requires_str() if not project._REQUIRES_STR then -- reload the project file to handle `has_config()` local ok, errors = project._load(true) if not ok then os.raise(errors) end -- get raw requires local requires_str, requires_extra = project.get("requires"), project.get("__extra_requires") project._REQUIRES_STR = requires_str or false project._REQUIRES_EXTRA = requires_extra end return project._REQUIRES_STR or nil, project._REQUIRES_EXTRA end -- get the given rule function project.rule(name) return project.rules()[name] end -- get project rules function project.rules() if not project._RULES then -- load rules local rules, errors = project._load_rules() if not rules then os.raise(errors) end project._RULES = rules end return project._RULES end -- get the given task function project.task(name) return project.tasks()[name] end -- get tasks function project.tasks() if not project._TASKS then -- load tasks local tasks, errors = project._load_tasks() if not tasks then os.raise(errors) end project._TASKS = tasks end return project._TASKS end -- get packages function project.packages() if not project._PACKAGES then -- load packages local packages, errors = project._load_packages() if not packages then return nil, errors end project._PACKAGES = packages end return project._PACKAGES end -- get the mtimes function project.mtimes() return project.interpreter():mtimes() end -- get the project menu function project.menu() -- attempt to load options from the project file local options = nil local errors = nil if os.isfile(project.rootfile()) then options, errors = project._load_options(true) end -- failed? if not options then if errors then utils.error(errors) end return {} end -- arrange options by category local options_by_category = {} for _, opt in pairs(options) do -- make the category local category = "default" if opt:get("category") then category = table.unwrap(opt:get("category")) end options_by_category[category] = options_by_category[category] or {} -- append option to the current category options_by_category[category][opt:name()] = opt end -- make menu by category local menu = {} for k, opts in pairs(options_by_category) do -- insert options local first = true for name, opt in pairs(opts) do -- show menu? if opt:get("showmenu") then -- the default value local default = "auto" if opt:get("default") ~= nil then default = opt:get("default") end -- is first? if first then -- insert a separator table.insert(menu, {}) -- not first first = false end -- append it local longname = name local descriptions = opt:get("description") if descriptions then -- define menu option local menu_options = {nil, longname, "kv", default, descriptions} -- handle set_description("xx", "xx") if type(descriptions) == "table" then for i, description in ipairs(descriptions) do menu_options[4 + i] = description end end -- insert option into menu table.insert(menu, menu_options) else table.insert(menu, {nil, longname, "kv", default, nil}) end end end end -- ok? return menu end -- return module: project return project