--!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, TBOOX Open Source Group. -- -- @author Arthapz, ruki -- @file common.lua -- -- imports import("core.base.json") import("core.base.bytes") import("core.base.hashset") import("core.project.config") import("core.tool.compiler") import("core.cache.memcache", {alias = "_memcache"}) import("core.cache.localcache", {alias = "_localcache"}) import("core.project.project") import("lib.detect.find_file") import("private.async.buildjobs") import("stl_headers") -- get memcache function memcache() return _memcache.cache("cxxmodules") end -- get localcache function localcache() return _localcache.cache("cxxmodules") end -- get stl modules cache directory function stlmodules_cachedir(target, opt) opt = opt or {} local stlcachedir = path.join(config.buildir(), "stlmodules", "cache", target:arch(), config.mode() or "release") if opt.mkdir and not os.isdir(stlcachedir) then os.mkdir(stlcachedir) os.mkdir(path.join(stlcachedir, "experimental")) end return stlcachedir end -- get modules cache directory function modules_cachedir(target, opt) opt = opt or {} local cachedir = path.join(target:autogendir(), "rules", "modules", "cache") if opt.mkdir and not os.isdir(cachedir) then os.mkdir(cachedir) end return cachedir end -- get headerunits info function get_headerunits(target, sourcebatch, modules) local headerunits local stl_headerunits for _, objectfile in ipairs(sourcebatch.objectfiles) do local m = modules[objectfile] if m then for name, r in pairs(m.requires) do if r.method ~= "by-name" then local unittype = r.method == "include-angle" and ":angle" or ":quote" if stl_headers.is_stl_header(name) then stl_headerunits = stl_headerunits or {} if not table.find_if(stl_headerunits, function(i, v) return v.name == name end) then table.insert(stl_headerunits, {name = name, path = r.path, type = unittype, unique = r.unique}) end else headerunits = headerunits or {} if not table.find_if(headerunits, function(i, v) return v.name == name end) then table.insert(headerunits, {name = name, path = r.path, type = unittype, unique = r.unique}) end end end end end end return headerunits, stl_headerunits end -- patch sourcebatch function patch_sourcebatch(target, sourcebatch) sourcebatch.sourcekind = "cxx" sourcebatch.objectfiles = {} sourcebatch.dependfiles = {} for _, sourcefile in ipairs(sourcebatch.sourcefiles) do local objectfile = target:objectfile(sourcefile) local dependfile = target:dependfile(objectfile) table.insert(sourcebatch.objectfiles, objectfile) table.insert(sourcebatch.dependfiles, dependfile) end end function parse_meta_info(target, metafile) local metadata = json.loadfile(metafile) if metadata._VENDOR_extension.xmake then return metadata._VENDOR_extension.xmake.file, metadata._VENDOR_extension.xmake.name, metadata end local filename = path.basename(metafile) local metadir = path.directory(metafile) for _, ext in ipairs({".mpp", ".mxx", ".cppm", ".ixx"}) do if os.isfile(path.join(metadir, filename .. ext)) then filename = filename .. ext break end end local sourcecode = io.readfile(path.join(path.directory(metafile), filename)) sourcecode = sourcecode:gsub("//.-\n", "\n") sourcecode = sourcecode:gsub("/%*.-%*/", "") local name for _, line in ipairs(sourcecode:split("\n", {plain = true})) do name = line:match("export%s+module%s+(.+)%s*;") or line:match("export%s+__preprocessed_module%s+(.+)%s*;") if name then break end end return filename, name, metadata end -- extract packages modules dependencies function get_all_package_modules(target, modules, opt) local package_modules -- parse all meta-info and append their informations to the package store for _, package in pairs(target:pkgs()) do package_modules = package_modules or {} local modulesdir = path.join(package:installdir(), "modules") local metafiles = os.files(path.join(modulesdir, "*", "*.meta-info")) for _, metafile in ipairs(metafiles) do local modulefile, name, metadata = parse_meta_info(target, metafile) package_modules[name] = { file = path.join(modulesdir, modulefile), metadata = metadata } end end return package_modules end -- cull unused modules function cull_unused_modules(target, modules, package_modules_data) local needed_modules = {} -- append all target dependencies for _, module in pairs(modules) do if module.requires then for required, _ in pairs(module.requires) do table.insert(needed_modules, required) end end end -- append all package dependencies local culled local module_names = table.keys(package_modules_data) for _, name in ipairs(module_names) do culled = culled or {} if table.find(needed_modules, name) and package_modules_data[name] and not culled[name] then culled[name] = package_modules_data[name] if culled[name].metadata.imports then table.join2(needed_modules, culled[name].metadata.imports) table.join2(module_names, culled[name].metadata.imports) end end end return culled end -- get modules support function modules_support(target) local cachekey = tostring(target) local module_builder = memcache():get2("modules_support", cachekey) if module_builder == nil then if target:has_tool("cxx", "clang", "clangxx") then module_builder = import("clang", {anonymous = true}) elseif target:has_tool("cxx", "gcc", "gxx") then module_builder = import("gcc", {anonymous = true}) elseif target:has_tool("cxx", "cl") then module_builder = import("msvc", {anonymous = true}) else local _, toolname = target:tool("cxx") raise("compiler(%s): does not support c++ module!", toolname) end memcache():set2("modules_support", cachekey, module_builder) end return module_builder end -- get bmi extension function bmi_extension(target) return modules_support(target).get_bmi_extension() end -- has module extension? e.g. *.mpp, ... function has_module_extension(sourcefile) local modulexts = _g.modulexts if modulexts == nil then modulexts = hashset.of(".mpp", ".mxx", ".cppm", ".ixx") _g.modulexts = modulexts end local extension = path.extension(sourcefile) return modulexts:has(extension:lower()) end -- this target contains module files? function contains_modules(target) -- we can not use `"c++.build.modules.builder"`, because it contains sourcekind/cxx. local target_with_modules = target:sourcebatches()["c++.build.modules"] and true or false if not target_with_modules then target_with_modules = target:policy("build.c++.modules") end if not target_with_modules then for _, dep in ipairs(target:orderdeps()) do local sourcebatches = dep:sourcebatches() if sourcebatches["c++.build.modules"] then target_with_modules = true break end end end return target_with_modules end -- load module infos function load_moduleinfos(target, sourcebatch) local moduleinfos for _, sourcefile in ipairs(sourcebatch.sourcefiles) do local dependfile = target:dependfile(sourcefile) if os.isfile(dependfile) then local data = io.load(dependfile) if data then moduleinfos = moduleinfos or {} local moduleinfo = json.decode(data.moduleinfo) moduleinfo.sourcefile = sourcefile if moduleinfo then table.insert(moduleinfos, moduleinfo) end end end end return moduleinfos end -- parse module dependency data --[[ { "build/.objs/stl_headerunit/linux/x86_64/release/src/hello.mpp.o" = { requires = { iostream = { method = "include-angle", unique = true, path = "/usr/include/c++/11/iostream" } }, provides = { hello = { bmi = "build/.gens/stl_headerunit/linux/x86_64/release/rules/modules/cache/hello.gcm", sourcefile = "src/hello.mpp" } } }, "build/.objs/stl_headerunit/linux/x86_64/release/src/main.cpp.o" = { requires = { hello = { method = "by-name", unique = false, path = "build/.gens/stl_headerunit/linux/x86_64/release/rules/modules/cache/hello.gcm" } } } }]] function _parse_dependencies_data(target, moduleinfos) local modules for _, moduleinfo in ipairs(moduleinfos) do assert(moduleinfo.version <= 1) for _, rule in ipairs(moduleinfo.rules) do modules = modules or {} local m = {} if rule.provides then for _, provide in ipairs(rule.provides) do m.provides = m.provides or {} assert(provide["logical-name"]) local bmifile = provide["compiled-module-path"] -- try to find the compiled module path in outputs filed (MSVC doesn't generate compiled-module-path) if not bmifile then for _, output in ipairs(rule.outputs) do if output:endswith(".ifc") or output:endswith(".pcm") or output:endswith(".bmi") then bmifile = output break end end -- we didn't found the compiled module path, so we assume it if not bmifile then local name = provide["logical-name"] .. bmi_extension(target) -- partition ":" character is invalid path character on windows -- @see https://github.com/xmake-io/xmake/issues/2954 name = name:replace(":", "-") bmifile = path.join(get_outputdir(target, name), name) end end m.provides[provide["logical-name"]] = { bmi = bmifile, sourcefile = moduleinfo.sourcefile, interface = provide["is-interface"] } end else m.cppfile = moduleinfo.sourcefile end assert(rule["primary-output"]) modules[path.translate(rule["primary-output"])] = m end end for _, moduleinfo in ipairs(moduleinfos) do for _, rule in ipairs(moduleinfo.rules) do local m = modules[path.translate(rule["primary-output"])] for _, r in ipairs(rule.requires) do m.requires = m.requires or {} local p = r["source-path"] if not p then for _, dependency in pairs(modules) do if dependency.provides and dependency.provides[r["logical-name"]] then p = dependency.provides[r["logical-name"]].bmi break end end end m.requires[r["logical-name"]] = { method = r["lookup-method"] or "by-name", path = p and path.translate(p) or nil, unique = r["unique-on-source-path"] or false } end end end return modules end -- check circular dependencies for the given module function _check_circular_dependencies_of_module(name, moduledeps, modulesources, depspath) for _, dep in ipairs(moduledeps[name]) do local depinfo = moduledeps[dep] if depinfo then local depspath_sub if depspath then for idx, name in ipairs(depspath) do if name == dep then local circular_deps = table.slice(depspath, idx) table.insert(circular_deps, dep) local sourceinfo = "" for _, circular_depname in ipairs(circular_deps) do local sourcefile = modulesources[circular_depname] if sourcefile then sourceinfo = sourceinfo .. ("\n -> module(%s) in %s"):format(circular_depname, sourcefile) end end os.raise("circular modules dependency(%s) detected!%s", table.concat(circular_deps, ", "), sourceinfo) end end depspath_sub = table.join(depspath, dep) end _check_circular_dependencies_of_module(dep, moduledeps, modulesources, depspath_sub) end end end -- check circular dependencies -- @see https://github.com/xmake-io/xmake/issues/3031 function _check_circular_dependencies(modules) local moduledeps = {} local modulesources = {} for _, mod in pairs(modules) do if mod then if mod.provides and mod.requires then for name, provide in pairs(mod.provides) do modulesources[name] = provide.sourcefile local deps = moduledeps[name] if deps then table.join2(deps, mod.requires) else moduledeps[name] = table.keys(mod.requires) end end end end end for name, _ in pairs(moduledeps) do _check_circular_dependencies_of_module(name, moduledeps, modulesources, {name}) end end function _topological_sort_visit(node, nodes, modules, output) if node.marked then return end assert(not node.tempmarked) node.tempmarked = true local m1 = modules[node.objectfile] for _, n in ipairs(nodes) do if not n.tempmarked then local m2 = modules[n.objectfile] if m2 then for name, provide in pairs(m1.provides) do if m2.requires and m2.requires[name] then _topological_sort_visit(n, nodes, modules, output) end end end end end node.tempmarked = false node.marked = true table.insert(output, 1, node.objectfile) end function _topological_sort_has_node_without_mark(nodes) for _, node in ipairs(nodes) do if not node.marked then return true end end return false end function _topological_sort_get_first_unmarked_node(nodes) for _, node in ipairs(nodes) do if not node.marked and not node.tempmarked then return node end end end -- topological sort function sort_modules_by_dependencies(objectfiles, modules) local output = {} local nodes = {} for _, objectfile in ipairs(objectfiles) do local m = modules[objectfile] if m then table.insert(nodes, {marked = false, tempmarked = false, objectfile = objectfile}) end end while _topological_sort_has_node_without_mark(nodes) do local node = _topological_sort_get_first_unmarked_node(nodes) _topological_sort_visit(node, nodes, modules, output) end return output end function find_quote_header_file(target, sourcefile, file) local p = path.join(path.directory(path.absolute(sourcefile, project.directory())), file) assert(os.isfile(p)) return p end function find_angle_header_file(target, file) local headerpaths = modules_support(target).toolchain_includedirs(target) for _, dep in ipairs(target:orderdeps()) do local includedirs = dep:get("sysincludedirs") or dep:get("includedirs") if includedirs then table.join2(headerpaths, includedirs) end end for _, pkg in pairs(target:pkgs()) do local includedirs = pkg:get("sysincludedirs") or pkg:get("includedirs") if includedirs then table.join2(headerpaths, includedirs) end end table.join2(headerpaths, target:get("includedirs")) local p = find_file(file, headerpaths) assert(p, "find <%s> not found!", file) return p end -- https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1689r5.html --[[ { "version": 1, "revision": 0, "rules": [ { "primary-output": "use-header.mpp.o", "requires": [ { "logical-name": "", "source-path": "/path/to/found/header.hpp", "unique-on-source-path": true, "lookup-method": "include-angle" } ] }, { "primary-output": "header.hpp.bmi", "provides": [ { "logical-name": "header.hpp", "source-path": "/path/to/found/header.hpp", "unique-on-source-path": true, } ] } ] }]] function fallback_generate_dependencies(target, jsonfile, sourcefile, preprocess_file) local output = {version = 1, revision = 0, rules = {}} local rule = {outputs = {jsonfile}} rule["primary-output"] = target:objectfile(sourcefile) local module_name_export local module_name_private local module_deps = {} local module_deps_set = hashset.new() local sourcecode = preprocess_file(sourcefile) or io.readfile(sourcefile) local internal = false sourcecode = sourcecode:gsub("//.-\n", "\n") sourcecode = sourcecode:gsub("/%*.-%*/", "") for _, line in ipairs(sourcecode:split("\n", {plain = true})) do if not module_name_export then module_name_export = line:match("export%s+module%s+(.+)%s*;") or line:match("export%s+__preprocessed_module%s+(.+)%s*;") end if not module_name_private then module_name_private = line:match("module%s+(.+)%s*;") or line:match("__preprocessed_module%s+(.+)%s*;") if module_name_private then internal = module_name_private:find(":") end end local module_depname = line:match("import%s+(.+)%s*;") -- we need to parse module interface dep in cxx/impl_unit.cpp, e.g. hello.mpp and hello_impl.cpp -- @see https://github.com/xmake-io/xmake/pull/2664#issuecomment-1213167314 if not module_depname and not has_module_extension(sourcefile) then module_depname = module_name_private end if module_depname and not module_deps_set:has(module_depname) then local module_dep = {} -- partition? import :xxx; if module_depname:startswith(":") then local module_name = (module_name_export or module_name_private or "") module_name = module_name:split(":")[1] module_dep["unique-on-source-path"] = true module_depname = module_name .. module_depname elseif module_depname:startswith("\"") then module_depname = module_depname:sub(2, -2) module_dep["lookup-method"] = "include-quote" module_dep["unique-on-source-path"] = true module_dep["source-path"] = find_quote_header_file(target, sourcefile, module_depname) elseif module_depname:startswith("<") then module_depname = module_depname:sub(2, -2) module_dep["lookup-method"] = "include-angle" module_dep["unique-on-source-path"] = true module_dep["source-path"] = find_angle_header_file(target, module_depname) end module_dep["logical-name"] = module_depname table.insert(module_deps, module_dep) module_deps_set:insert(module_depname) end end if module_name_export or internal then local outputdir = get_outputdir(target, sourcefile) local provide = {} provide["logical-name"] = module_name_export or module_name_private provide["source-path"] = sourcefile provide["is-interface"] = not internal provide["compiled-module-path"] = path.join(outputdir, (module_name_export or module_name_private) .. bmi_extension(target)) rule.provides = {} table.insert(rule.provides, provide) end rule.requires = module_deps table.insert(output.rules, rule) local jsondata = json.encode(output) io.writefile(jsonfile, jsondata) end -- get module dependencies function get_module_dependencies(target, sourcebatch, opt) local cachekey = target:name() .. "/" .. sourcebatch.rulename local modules = memcache():get2("modules", cachekey) if modules == nil or opt.regenerate then modules = localcache():get2("modules", cachekey) opt.progress = opt.progress or 0 local changed = modules_support(target).generate_dependencies(target, sourcebatch, opt) if changed or modules == nil then local moduleinfos = load_moduleinfos(target, sourcebatch) modules = _parse_dependencies_data(target, moduleinfos) modules = table.join(modules or {}, modules_support(target).get_stdmodules(target)) if modules then _check_circular_dependencies(modules) end localcache():set2("modules", cachekey, modules) localcache():save() end memcache():set2("modules", cachekey, modules) end return modules end -- generate headerunits for batchcmds function generate_headerunits_for_batchcmds(target, batchcmds, sourcebatch, modules, opt) local user_headerunits, stl_headerunits = get_headerunits(target, sourcebatch, modules) -- build stl header units as other headerunits may need them if stl_headerunits then modules_support(target).generate_stl_headerunits_for_batchcmds(target, batchcmds, stl_headerunits, opt) end if user_headerunits then modules_support(target).generate_user_headerunits_for_batchcmds(target, batchcmds, user_headerunits, opt) end end -- build batchjobs for modules function build_batchjobs_for_modules(modules, batchjobs, rootjob) return buildjobs(modules, batchjobs, rootjob) end -- build modules for batchjobs function build_modules_for_batchjobs(target, batchjobs, sourcebatch, modules, opt) local objectfiles = sort_modules_by_dependencies(sourcebatch.objectfiles, modules) modules_support(target).build_modules_for_batchjobs(target, batchjobs, objectfiles, modules, opt) end -- build modules for batchcmds function build_modules_for_batchcmds(target, batchcmds, sourcebatch, modules, opt) local objectfiles = sort_modules_by_dependencies(sourcebatch.objectfiles, modules) modules_support(target).build_modules_for_batchcmds(target, batchcmds, objectfiles, modules, opt) end -- append headerunits objectfiles to link function append_dependency_objectfiles(target) local cachekey = target:name() .. "dependency_objectfiles" local cache = localcache():get(cachekey) if cache then if target:is_binary() then target:add("ldflags", cache, {force = true, expand = false}) elseif target:is_static() then target:add("arflags", cache, {force = true, expand = false}) elseif target:is_shared() then target:add("shflags", cache, {force = true, expand = false}) end end end -- generate meta module informations for package / other buildsystems import -- based on https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2473r1.pdf -- -- e.g -- { -- "include_paths": ["foo/", "bar/"] -- "definitions": ["FOO=BAR"] -- "imports": ["std", "bar"] -- "_VENDOR_extension": {} -- } function generate_meta_module_info(target, name, sourcefile, requires) local module_metadata = {} -- add include paths module_metadata.include_paths = table.wrap(target:get("includedirs")) or {} for _, deps in ipairs(target:orderdeps()) do table.join2(module_metadata.include_paths, deps:get("includedirs") or {}) end -- add definitions module_metadata.definitions = table.wrap(target:get("defines")) or {} for _, deps in ipairs(target:orderdeps()) do table.join2(module_metadata.definitions, deps:get("defines") or {}) end -- add imports if requires then for name, _ in pairs(requires) do module_metadata.imports = module_metadata.imports or {} table.append(module_metadata.imports, name) end end local modulehash = get_modulehash(target, sourcefile) module_metadata._VENDOR_extension = { xmake = { name = name, file = path.join(modulehash, path.filename(sourcefile)) }} return module_metadata end function install_module_target(target) local sourcebatch = target:sourcebatches()["c++.build.modules.install"] if sourcebatch and sourcebatch.sourcefiles then for _, sourcefile in ipairs(sourcebatch.sourcefiles) do local install = (fileconfig and not fileconfig.install) and false or true if install then local modulehash = get_modulehash(target, sourcefile) prefixdir = path.join("modules", modulehash) target:add("installfiles", sourcefile, {prefixdir = prefixdir}) local metafile = get_metafile(target, sourcefile) if os.exists(metafile) then target:add("installfiles", metafile, {prefixdir = prefixdir}) end end end end end function get_modulehash(target, modulepath) local key = path.directory(modulepath) .. target:name() return hash.uuid(key):split("-", {plain = true})[1]:lower() end function get_metafile(target, modulefile) local outputdir = get_outputdir(target, modulefile) return path.join(outputdir, path.filename(modulefile) .. ".meta-info") end function get_outputdir(target, module) local cachedir = modules_cachedir(target) local modulehash = get_modulehash(target, module.path or module) local outputdir = path.join(cachedir, modulehash) if not os.exists(outputdir) then os.mkdir(outputdir) end return outputdir end