--!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-present, Xmake Open Source Community. -- -- @author ruki -- @file configfiles.lua -- -- imports import("core.base.option") import("core.base.semver") import("core.project.config") import("core.project.depend") import("core.project.project") import("core.platform.platform") import("lib.detect.find_tool") -- get all configuration files function _get_configfiles() local configfiles = {} for _, target in table.orderpairs(project.targets()) do if target:is_enabled() then -- get configuration files for target local srcfiles, dstfiles, fileinfos = target:configfiles() for idx, srcfile in ipairs(srcfiles) do -- get destinate file and file info local dstfile = dstfiles[idx] local fileinfo = fileinfos[idx] -- get source info local srcinfo = configfiles[dstfile] if not srcinfo then srcinfo = {} configfiles[dstfile] = srcinfo end -- save source file if srcinfo.srcfile then assert(path.absolute(srcinfo.srcfile) == path.absolute(srcfile), "file(%s) and file(%s) are writing a same file(%s)", srcfile, srcinfo.srcfile, dstfile) else srcinfo.srcfile = srcfile srcinfo.fileinfo = fileinfo end -- we use first target to get dependfile path -- @see https://github.com/xmake-io/xmake/issues/3321 if not srcinfo.dependfile then srcinfo.dependfile = target:dependfile(srcfile) end -- always update? local always_update = target:policy("build.always_update_configfiles") if always_update == nil then always_update = project.policy("build.always_update_configfiles") end srcinfo.always_update = always_update -- save targets srcinfo.targets = srcinfo.targets or {} table.insert(srcinfo.targets, target) -- save preprocessors local preprocessor = fileinfo and fileinfo.preprocessor if preprocessor then srcinfo.preprocessors = srcinfo.preprocessors or {} table.insert(srcinfo.preprocessors, preprocessor) end end end end return configfiles end -- get the builtin variables function _get_builtinvars_target(target) -- get version variables local builtinvars = {} local version, version_build = target:version() if version then builtinvars.VERSION = version try {function () local v = semver.new(version) if v then builtinvars.VERSION_MAJOR = v:major() builtinvars.VERSION_MINOR = v:minor() builtinvars.VERSION_ALTER = v:patch() end end} if version_build then builtinvars.VERSION_BUILD = version_build end end return builtinvars end -- get the git builtin variables function _get_builtinvars_git(builtinvars) local cmds = { GIT_TAG = {"describe", "--tags"}, GIT_TAG_LONG = {"describe", "--tags", "--long"}, GIT_BRANCH = {"rev-parse", "--abbrev-ref", "HEAD"}, GIT_COMMIT = {"rev-parse", "--short", "HEAD"}, GIT_COMMIT_LONG = {"rev-parse", "HEAD"}, GIT_COMMIT_DATE = {"log", "-1", "--date=format:%Y%m%d%H%M%S", "--format=%ad"} } for name, argv in pairs(cmds) do builtinvars[name] = function () local result local git = find_tool("git") if git then result = try {function () return os.iorunv(git.program, argv) end} end if not result then result = "none" end return result:trim() end end end -- get the global builtin variables function _get_builtinvars_global() local builtinvars = _g.builtinvars_global if builtinvars == nil then builtinvars = { arch = config.get("arch") or os.arch() , plat = config.get("plat") or os.host() , host = os.host() , mode = config.get("mode") or "release" , debug = is_mode("debug") and 1 or 0 , os = platform.os() } local builtinvars_upper = {} for name, value in pairs(builtinvars) do builtinvars_upper[name:upper()] = type(value) == "string" and value:upper() or value end table.join2(builtinvars, builtinvars_upper) _get_builtinvars_git(builtinvars) _g.builtinvars_global = builtinvars end return builtinvars end function _preprocess_define_value(name, value, opt) opt = opt or {} local extraconf = opt.extraconf if value == nil then value = ("/* #undef %s */"):format(name) elseif type(value) == "boolean" then if value then value = ("#define %s 1"):format(name) else value = ("/* #define %s 0 */"):format(name) end elseif type(value) == "number" then value = ("#define %s %d"):format(name, value) elseif type(value) == "string" then local quote = true local escape = false if extraconf then -- disable to wrap quote, @see https://github.com/xmake-io/xmake/issues/1694 if extraconf.quote == false then quote = false end -- escape path separator when with quote, @see https://github.com/xmake-io/xmake/issues/1872 if quote and extraconf.escape then escape = true end end if quote then if escape then value = value:gsub("\\", "\\\\") end value = ("#define %s \"%s\""):format(name, value) else value = ("#define %s %s"):format(name, value) end else raise("unknown variable(%s) type: %s", name, type(value)) end return value end function _preprocess_default_value(name, value, opt) opt = opt or {} local default = table.unpack(opt.argv or {}) assert(default ~= nil, "please set default value for variable(%s)", variable) if value == nil then value = default else value = tostring(value) end return value end function _preprocess_define_export_value(name, value, opt) value = ([[#ifdef %s_STATIC # define %s_EXPORT #else # if defined(_WIN32) # define %s_EXPORT __declspec(dllexport) # elif defined(__GNUC__) && ((__GNUC__ >= 4) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 3)) # define %s_EXPORT __attribute__((visibility("default"))) # else # define %s_EXPORT # endif #endif ]]):format(name, name, name, name, name) return value end -- get variable value function _get_variable_value(variables, name, opt) opt = opt or {} local preprocessor_name = opt.preprocessor_name local preprocessor_argv = opt.preprocessor_argv local configfile = opt.configfile local value = variables[name] local extraconf = variables["__extraconf_" .. name] if preprocessor_name then local preprocessed = false if opt.preprocessors then for _, preprocessor in ipairs(opt.preprocessors) do value = preprocessor(preprocessor_name, name, value, {argv = preprocessor_argv, extraconf = extraconf}) if value ~= nil then preprocessed = true break end end end if not preprocessed then local preprocessors = _g._preprocessors if preprocessors == nil then preprocessors = { define = _preprocess_define_value, default = _preprocess_default_value, define_export = _preprocess_define_export_value } _g._preprocessors = preprocessors end local preprocessor = preprocessors[preprocessor_name] if preprocessor == nil then raise("unknown variable keyword, ${%s %s}", preprocessor_name, name) end value = preprocessor(name, value, {argv = preprocessor_argv, extraconf = extraconf}) end assert(value ~= nil, "cannot get variable(%s %s) in %s.", preprocessor_name, name, configfile) else assert(value ~= nil, "cannot get variable(%s) in %s.", name, configfile) end dprint(" > replace %s -> %s", name, value) if type(value) == "table" then dprint("invalid variable value", value) end return value end -- generate the configuration file function _generate_configfile(srcfile, dstfile, fileinfo, targets, preprocessors) -- trace if option.get("verbose") then cprint("${dim}generating %s to %s ..", srcfile, dstfile) end -- only copy it? if fileinfo.onlycopy then if os.mtime(srcfile) > os.mtime(dstfile) then os.cp(srcfile, dstfile) end else -- generate to the temporary file first local dstfile_tmp = os.tmpfile(srcfile) os.tryrm(dstfile_tmp) os.cp(srcfile, dstfile_tmp) -- get all variables local variables = fileinfo.variables or {} for _, target in ipairs(targets) do -- get variables from the target for name, value in pairs(target:get("configvar")) do local value = value if variables[name] == nil then value = table.unwrap(value) variables[name] = value variables["__extraconf_" .. name] = target:extraconf("configvar." .. name, value) end end -- get variables from the target.options for _, opt in ipairs(target:orderopts()) do for name, value in pairs(opt:get("configvar")) do if variables[name] == nil then variables[name] = table.unwrap(value) variables["__extraconf_" .. name] = opt:extraconf("configvar." .. name, value) end end end -- get the builtin variables from the target for name, value in pairs(_get_builtinvars_target(target)) do local value = value if type(value) == "function" then value = value() end if variables[name] == nil then variables[name] = value end end end -- get the global builtin variables for name, value in pairs(_get_builtinvars_global()) do local value = value if type(value) == "function" then value = value() end if variables[name] == nil then variables[name] = value end end -- replace all variables local pattern = fileinfo.pattern or "%${([^\n]-)}" io.gsub(dstfile_tmp, "(" .. pattern .. ")", function(_, variable) variable = variable:trim() local preprocessor_argv local preprocessor_name local parts = variable:split("%s") if #parts > 1 then preprocessor_name = parts[1] variable = parts[2] preprocessor_argv = table.slice(parts, 3) end return _get_variable_value(variables, variable, {preprocessor_name = preprocessor_name, preprocessor_argv = preprocessor_argv, configfile = srcfile, preprocessors = preprocessors}) end) -- update file if the content is changed if os.isfile(dstfile_tmp) then os.cp(dstfile_tmp, dstfile, {copy_if_different = true}) end end -- trace cprint("generating %s ... ${color.success}${text.success}", srcfile) end -- the main entry function function main(opt) -- enter project directory opt = opt or {} local oldir = os.cd(project.directory()) -- generate all configuration files local configfiles = _get_configfiles() for dstfile, srcinfo in pairs(configfiles) do depend.on_changed(function () _generate_configfile(srcinfo.srcfile, dstfile, srcinfo.fileinfo, srcinfo.targets, srcinfo.preprocessors) end, {files = srcinfo.srcfile, lastmtime = os.mtime(dstfile), dependfile = srcinfo.dependfile, changed = opt.force or srcinfo.always_update}) end -- leave project directory os.cd(oldir) end