diff options
| author | Arthur LAURENT <[email protected]> | 2024-01-23 18:23:27 +0100 |
|---|---|---|
| committer | Arthur LAURENT <[email protected]> | 2024-01-26 18:20:57 +0100 |
| commit | d52291f3389b2bf3f4b90e24c4670f41605c3cc8 (patch) | |
| tree | b2a2d5eefca2ce9da55bba6dedbf2413c4f610f6 /xmake/rules/c++/modules/modules_support | |
| parent | e20c6cafdb241ad6f858556c33463da1a092abdc (diff) | |
refactor module common infrastructure
Diffstat (limited to 'xmake/rules/c++/modules/modules_support')
| -rw-r--r-- | xmake/rules/c++/modules/modules_support/builder.lua | 400 | ||||
| -rw-r--r-- | xmake/rules/c++/modules/modules_support/compiler_support.lua | 281 | ||||
| -rw-r--r-- | xmake/rules/c++/modules/modules_support/dependency_scanner.lua (renamed from xmake/rules/c++/modules/modules_support/common.lua) | 543 | ||||
| -rw-r--r-- | xmake/rules/c++/modules/modules_support/stl_headers.lua | 2 |
4 files changed, 860 insertions, 366 deletions
diff --git a/xmake/rules/c++/modules/modules_support/builder.lua b/xmake/rules/c++/modules/modules_support/builder.lua new file mode 100644 index 000000000..481415661 --- /dev/null +++ b/xmake/rules/c++/modules/modules_support/builder.lua @@ -0,0 +1,400 @@ +--!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 ruki, Arthapz +-- @file common.lua +-- + +-- imports +import("core.base.json") +import("core.base.option") +import("private.async.buildjobs") +import("core.tool.compiler") +import("core.project.config") +import("core.project.depend") +import("utils.progress") +import("compiler_support") +import("dependency_scanner") + +-- build target modules +function _build_modules(target, sourcebatch, modules, opt) + local objectfiles = dependency_scanner.sort_modules_by_dependencies(sourcebatch.objectfiles, modules) + + -- build modules + for _, objectfile in ipairs(objectfiles) do + local module = modules[objectfile] + if not module then + goto CONTINUE + end + + local name, provide, cppfile = compiler_support.get_provided_module(module) + cppfile = cppfile or module.cppfile + + local fileconfig = target:fileconfig(cppfile) + local bmifile = provide and compiler_support.get_bmi_path(provide.bmi) + local build = _should_build(target, cppfile, bmifile, objectfile, module.requires) + + -- add objectfile if module is not from external dep + if not (fileconfig and fileconfig.external) then + target:add("objectfiles", objectfile) + end + + -- needed to detect rebuild of dependencies + if provide then + compiler_support.memcache():set2(target:name(), name, build) + end + + local deps = {} + for _, dep in ipairs(table.keys(module.requires or {})) do + table.insert(deps, opt.batchjobs and target:name() .. dep or dep) + end + + opt.build_module(deps, build, module, name, provide, objectfile, cppfile, fileconfig) + + ::CONTINUE:: + end +end + +-- build target headerunits +function _build_headerunits(target, headerunits, opt) + + local outputdir = compiler_support.headerunits_cachedir(target, {mkdir = true}) + if opt.stl_headerunit then + outputdir = path.join(outputdir, "stl") + end + + for _, headerunit in ipairs(headerunits) do + local outputdir = outputdir + if opt.stl_headerunit and headerunit.name:startswith("experimental/") then + outputdir = path.join(outputdir, "experimental") + end + local bmifile = path.join(outputdir, path.filename(headerunit.name) .. compiler_support.get_bmi_extension(target)) + local key = path.normalize(headerunit.path) + local build = _should_build(target, headerunit.path, bmifile, nil, nil, {key = key, headerunit = true}) + + compiler_support.memcache():set2(target:name(), key, build) + + opt.build_headerunit(headerunit, key, bmifile, outputdir, build) + end +end + +-- should we build this module or headerunit ? +function _should_build(target, sourcefile, bmifile, objectfile, requires, opt) + + -- force rebuild a module if any of its module dependency is rebuilt + if requires then + for required, _ in pairs(requires) do + local m = get_from_target_mapper(target, required) + if m then + local rebuild = compiler_support.memcache():get2(target:name(), m.key) + if rebuild then + return true + end + end + end + end + + -- or rebuild it if the file changed for headerunit and namedmodules + if compiler_support.has_module_extension(sourcefile) or (opt and opt.headerunit) then + local dryrun = option.get("dry-run") + local compinst = compiler.load("cxx", {target = target}) + local compflags = compinst:compflags({sourcefile = sourcefile, target = target}) + + local dependfile = target:dependfile(bmifile or objectfile) + local dependinfo = target:is_rebuilt() and {} or (depend.load(dependfile) or {}) + + -- need build this object? + local depvalues = {compinst:program(), compflags} + local lastmtime = os.isfile(bmifile or objectfile) and os.mtime(dependfile) or 0 + + if dryrun or depend.is_changed(dependinfo, {lastmtime = lastmtime, values = depvalues}) then + return true + end + end + + return false +end + +-- generate meta module informations for package / other buildsystems import +-- +-- e.g +-- { +-- "defines": ["FOO=BAR"] +-- "imports": ["std", "bar"] +-- "name": "foo" +-- "file": "foo.cppm" +-- } +function _generate_meta_module_info(target, name, sourcefile, requires) + + local modulehash = compiler_support.get_modulehash(target, sourcefile) + local module_metadata = {name = name, file = path.join(modulehash, path.filename(sourcefile))} + + -- add definitions + module_metadata.defines = _builder(target).get_module_required_defines(target, sourcefile) + + -- 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 + + return module_metadata +end + +function _target_module_map_cachekey(target) + local mode = config.mode() + return target:name() .. "module_mapper" .. (mode or "") +end + +function _is_duplicated_headerunit(target, headerunit) + local mapper = get_target_module_mapper(target) + local key = hash.md5(path.normalize(headerunit.path)) + + -- for _, mapped in pairs(mapper) do + -- print("CHECK", mapped.key, key) + -- if mapped.key == key then + -- return true + -- end + -- end + + return false +end + +-- add populate job +function _init_build_for(target, batch, modules, opt) + + if opt.batchjobs then + local job_name = get_modulemap_populate_jobname(target) + return { modulemap_populatejob_name = { + name = job_name, + job = batch:addjob(job_name, function(index, total) + progress.show((index * 100) / total, "${color.build.target}<%s> populating.%s.map", target:name(), opt.type) + _builder(target).populate_module_map(target, modules) + end)}} + else + batch:show_progress(opt.progress, "${color.build.target}<%s> populating.%s.map", target:name(), opt.type) + _builder(target).populate_module_map(target, modules) + end +end + +function _builder(target) + + local cachekey = tostring(target) + local builder = compiler_support.memcache():get2("builder", cachekey) + if builder == nil then + if target:has_tool("cxx", "clang", "clangxx") then + builder = import("clang.builder", {anonymous = true}) + elseif target:has_tool("cxx", "gcc", "gxx") then + builder = import("gcc.builder", {anonymous = true}) + elseif target:has_tool("cxx", "cl") then + builder = import("msvc.builder", {anonymous = true}) + else + local _, toolname = target:tool("cxx") + raise("compiler(%s): does not support c++ module!", toolname) + end + compiler_support.memcache():set2("builder", cachekey, builder) + end + return builder +end + +function get_modulemap_populate_jobname(target) + return target:name() .. "_module_map_populate" +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) + + opt.rootjob = batchjobs:group_leave() or opt.rootjob + batchjobs:group_enter(target:name() .. "/build_modules", {rootjob = opt.rootjob}) + + local modulesjobs = {} + + _build_modules(target, sourcebatch, modules, table.join(opt, { + build_module = function(deps, build, module, name, provide, objectfile, cppfile, fileconfig) + local job_name = name and target:name() .. name or cppfile + + modulesjobs[job_name] = _builder(target).make_module_build_job(target, batchjobs, job_name, deps, {build = build, module = module, objectfile = objectfile, cppfile = cppfile}) + + if provide and fileconfig and fileconfig.public then + batchjobs:addjob(name .. "_metafile", function(index, total) + local metafilepath = compiler_support.get_metafile(target, cppfile) + depend.on_changed(function() + progress.show((index * 100) / total, "${color.build.target}<%s> generating.module.metadata %s", target:name(), name) + local metadata = _generate_meta_module_info(target, name, cppfile, module.requires) + json.savefile(metafilepath, metadata) + end, {dependfile = target:dependfile(metafilepath), files = {cppfile}, changed = target:is_rebuilt()}) + end, {rootjob = opt.rootjob}) + end + end + })) + + local tailjob = _init_build_for(target, batchjobs, modules, table.join({type = "module"}, opt)) + table.join2(modulesjobs, tailjob) + + -- build batchjobs for modules + build_batchjobs_for_modules(modulesjobs, batchjobs, opt.rootjob) +end + +-- build modules for batchcmds +function build_modules_for_batchcmds(target, batchcmds, sourcebatch, modules, opt) + + local depmtime = 0 + opt.progress = opt.progress or 0 + _init_build_for(target, batchcmds, modules, table.join({type = "module"}, opt)) + + -- build modules + _build_modules(target, sourcebatch, modules, table.join(opt, { + build_module = function(_, build, module, name, provide, objectfile, cppfile, fileconfig) + depmtime = math.max(depmtime, _builder(target).make_module_build_cmds(target, batchcmds, {build = build, module = module, cppfile = cppfile, objectfile = objectfile, progress = opt.progress})) + + if provide and fileconfig and fileconfig.public then + local metafilepath = compiler_support.get_metafile(target, cppfile) + depend.on_changed(function() + progress.show(opt.progress, "${color.build.target}<%s> generating.module.metadata %s", target:name(), name) + local metadata = _generate_meta_module_info(target, name, cppfile, module.requires) + json.savefile(metafilepath, metadata) + end, {dependfile = target:dependfile(metafilepath), files = {cppfile}, changed = target:is_rebuilt()}) + end + end + })) + + batchcmds:set_depmtime(depmtime) +end + +-- generate headerunits for batchjobs +function build_headerunits_for_batchjobs(target, batchjobs, sourcebatch, modules, opt) + + local user_headerunits, stl_headerunits = dependency_scanner.get_headerunits(target, sourcebatch, modules) + if not user_headerunits and not stl_headerunits then + return + end + -- we need new group(headerunits) + -- e.g. group(build_modules) -> group(headerunits) + opt.rootjob = batchjobs:group_leave() or opt.rootjob + batchjobs:group_enter(target:name() .. "/build_headerunits", {rootjob = opt.rootjob}) + + local build_headerunits = function(headerunits) + local modulesjobs = {} + _build_headerunits(target, headerunits, table.join(opt, { + build_headerunit = function(headerunit, key, bmifile, outputdir, build) + local job_name = target:name() .. key + local job = _builder(target).make_headerunit_build_job(target, job_name, batchjobs, headerunit, bmifile, outputdir, table.join(opt, {build = build})) + if job then + modulesjobs[job_name] = job + end + end + })) + build_batchjobs_for_modules(modulesjobs, batchjobs, opt.rootjob) + end + + -- build stl header units first as other headerunits may need them + if stl_headerunits then + opt.stl_headerunit = true + build_headerunits(stl_headerunits) + end + if user_headerunits then + opt.stl_headerunit = false + build_headerunits(user_headerunits) + end +end + +-- generate headerunits for batchcmds +function build_headerunits_for_batchcmds(target, batchcmds, sourcebatch, modules, opt) + + local user_headerunits, stl_headerunits = dependency_scanner.get_headerunits(target, sourcebatch, modules) + if not user_headerunits and not stl_headerunits then + return + end + + local build_headerunits = function(headerunits) + local depmtime = 0 + _build_headerunits(target, headerunits, table.join(opt, { + build_headerunit = function(headerunit, _, bmifile, outputdir, build) + depmtime = math.max(depmtime, _builder(target).make_headerunit_build_cmds(target, batchcmds, headerunit, bmifile, outputdir, table.join({build = build}, opt))) + end + })) + batchcmds:set_depmtime(depmtime) + end + + -- build stl header units first as other headerunits may need them + if stl_headerunits then + opt.stl_headerunit = true + build_headerunits(stl_headerunits) + end + if user_headerunits then + opt.stl_headerunit = false + build_headerunits(user_headerunits) + end +end + +-- append headerunits objectfiles to link +function append_dependency_objectfiles(target) + + local cachekey = target:name() .. "dependency_objectfiles" + local cache = compiler_support.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 + +-- get or create a target module mapper +function get_target_module_mapper(target) + + opt = opt or {} + local memcache = compiler_support.memcache() + local mapper = memcache:get2(target:name(), "module_mapper") + if not mapper then + mapper = {} + memcache:set2(target:name(), "module_mapper", mapper) + end + + return mapper +end + +-- get a module or headerunit from target mapper +function get_from_target_mapper(target, name) + local mapper = get_target_module_mapper(target) + if mapper[name] then + return mapper[name] + end +end + +-- add a module to target mapper +function add_module_to_target_mapper(target, name, sourcefile, bmifile, opt) + local mapper = get_target_module_mapper(target) + mapper[name] = {name = name, key = name, bmi = bmifile, sourcefile = sourcefile, opt = opt} +end + +-- add a headerunit to target mapper +function add_headerunit_to_target_mapper(target, headerunit, bmifile) + local mapper = get_target_module_mapper(target) + mapper[headerunit.name] = {name = headerunit.name, key = path.normalize(headerunit.path), headerunit = headerunit, bmi = bmifile} + return _is_duplicated_headerunit(target, headerunit) +end + diff --git a/xmake/rules/c++/modules/modules_support/compiler_support.lua b/xmake/rules/c++/modules/modules_support/compiler_support.lua new file mode 100644 index 000000000..5f1bc4617 --- /dev/null +++ b/xmake/rules/c++/modules/modules_support/compiler_support.lua @@ -0,0 +1,281 @@ +-- 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 ruki, Arthapz +-- @file compiler_support.lua +-- + +-- imports +import("core.base.json") +import("core.base.hashset") +import("core.cache.memcache", {alias = "_memcache"}) +import("core.cache.localcache", {alias = "_localcache"}) +import("lib.detect.find_file") +import("core.project.project") +import("core.project.config") + +function _compiler_support(target) + local cachekey = tostring(target) + local compiler_support = memcache():get2("compiler_support", cachekey) + if compiler_support == nil then + if target:has_tool("cxx", "clang", "clangxx") then + compiler_support = import("clang.compiler_support", {anonymous = true}) + elseif target:has_tool("cxx", "gcc", "gxx") then + compiler_support = import("gcc.compiler_support", {anonymous = true}) + elseif target:has_tool("cxx", "cl") then + compiler_support = import("msvc.compiler_support", {anonymous = true}) + else + local _, toolname = target:tool("cxx") + raise("compiler(%s): does not support c++ module!", toolname) + end + memcache():set2("compiler_support", cachekey, compiler_support) + end + return compiler_support +end + +-- load module support for the current target +function load(target) + _compiler_support(target).load(target) +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) + table.insert(sourcebatch.objectfiles, objectfile) + + local dependfile = target:dependfile(sourcefile or objectfile) + table.insert(sourcebatch.dependfiles, dependfile) + end +end + +-- cull sourcebatch objectfiles +function cull_objectfiles(target, sourcebatch) + + sourcebatch.sourcekind = "cxx" + sourcebatch.objectfiles = {} + for _, sourcefile in ipairs(sourcebatch.sourcefiles) do + local fileconfig = target:fileconfig(sourcefile) + if not (fileconfig and fileconfig.external) then + local objectfile = target:objectfile(sourcefile) + table.insert(sourcebatch.objectfiles, objectfile) + end + end +end + +-- get bmi extension +function get_bmi_extension(target) + return _compiler_support(target).get_bmi_extension() +end + +-- get bmi path +-- @see https://github.com/xmake-io/xmake/issues/4063 +function get_bmi_path(bmifile) + bmifile = bmifile:gsub(":", "_PARTITION_") + return path.normalize(bmifile) +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.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 + +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 = _compiler_support(target).toolchain_includedirs(target) + for _, dep in ipairs(target:orderdeps()) do + local includedirs = table.join(dep:get("sysincludedirs") or {}, dep:get("includedirs") or {}) + table.join2(headerpaths, includedirs) + end + for _, pkg in pairs(target:pkgs()) do + local includedirs = table.join(pkg:get("sysincludedirs") or {}, pkg:get("includedirs") or {}) + table.join2(headerpaths, includedirs) + end + table.join2(headerpaths, target:get("includedirs")) + local p = find_file(file, headerpaths) + assert(p, "find <%s> not found!", file) + return p +end + +-- get stdmodules +function get_stdmodules(target) + return _compiler_support(target).get_stdmodules(target) +end + +-- get memcache +function memcache() + return _memcache.cache("cxxmodules") +end + +-- get localcache +function localcache() + return _localcache.cache("cxxmodules") +end + + +-- get stl headerunits cache directory +function stlheaderunits_cachedir(target, opt) + opt = opt or {} + local stlcachedir = path.join(target:autogendir(), "rules", "bmi", "cache", "stl-headerunits") + if opt.mkdir and not os.isdir(stlcachedir) then + os.mkdir(stlcachedir) + os.mkdir(path.join(stlcachedir, "experimental")) + end + return stlcachedir +end +-- get stl modules cache directory +function stlmodules_cachedir(target, opt) + opt = opt or {} + local stlcachedir = path.join(target:autogendir(), "rules", "bmi", "cache", "stl-modules") + if opt.mkdir and not os.isdir(stlcachedir) then + os.mkdir(stlcachedir) + end + return stlcachedir +end + +-- get headerunits cache directory +function headerunits_cachedir(target, opt) + opt = opt or {} + local cachedir = path.join(target:autogendir(), "rules", "bmi", "cache", "headerunits") + if opt.mkdir and not os.isdir(cachedir) then + os.mkdir(cachedir) + end + return cachedir +end + +-- get modules cache directory +function modules_cachedir(target, opt) + opt = opt or {} + local cachedir = path.join(target:autogendir(), "rules", "bmi", "cache", "modules") + if opt.mkdir and not os.isdir(cachedir) then + os.mkdir(cachedir) + end + return cachedir +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 = module and modules_cachedir(target) or headerunits_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 + +-- get name provide info and cpp sourcefile of a module +function get_provided_module(module) + + local name, provide, cppfile + if module.provides then + -- assume there that provides is only one, until we encounter the cases + -- "Some compiler may choose to implement the :private module partition as a separate module for lookup purposes, and if so, it should be indicated as a separate provides entry." + local length = 0 + for k, v in pairs(module.provides) do + length = length + 1 + name = k + provide = v + cppfile = provide.sourcefile + if length > 1 then + raise("multiple provides are not supported now!") + end + break + end + end + + return name, provide, cppfile +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 fileconfig = target:fileconfig(sourcefile) + local install = fileconfig and fileconfig.public or false + if install then + local modulehash = get_modulehash(target, sourcefile) + local 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 + diff --git a/xmake/rules/c++/modules/modules_support/common.lua b/xmake/rules/c++/modules/modules_support/dependency_scanner.lua index d42d0f958..830263d07 100644 --- a/xmake/rules/c++/modules/modules_support/common.lua +++ b/xmake/rules/c++/modules/modules_support/dependency_scanner.lua @@ -14,99 +14,39 @@ -- -- Copyright (C) 2015-present, TBOOX Open Source Group. -- --- @author Arthapz, ruki --- @file common.lua +-- @author ruki, Arthapz +-- @file dependency_scanner.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("compiler_support") 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 +function _dependency_scanner(target) + local cachekey = tostring(target) + local dependency_scanner = compiler_support.memcache():get2("dependency_scanner", cachekey) + if dependency_scanner == nil then + if target:has_tool("cxx", "clang", "clangxx") then + dependency_scanner = import("clang.dependency_scanner", {anonymous = true}) + elseif target:has_tool("cxx", "gcc", "gxx") then + dependency_scanner = import("gcc.dependency_scanner", {anonymous = true}) + elseif target:has_tool("cxx", "cl") then + dependency_scanner = import("msvc.dependency_scanner", {anonymous = true}) + else + local _, toolname = target:tool("cxx") + raise("compiler(%s): does not support c++ module!", toolname) end + compiler_support.memcache():set2("dependency_scanner", cachekey, dependency_scanner) end - return headerunits, stl_headerunits + return dependency_scanner 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) +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 + if metadata.file and metadata.name then + return metadata.file, metadata.name, metadata end local filename = path.basename(metafile) @@ -133,130 +73,6 @@ function parse_meta_info(target, metafile) 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 --[[ { @@ -300,7 +116,7 @@ function _parse_dependencies_data(target, moduleinfos) -- 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 + if output:endswith(compiler_support.get_bmi_extension(target)) then bmifile = output break end @@ -308,11 +124,11 @@ function _parse_dependencies_data(target, moduleinfos) -- we didn't found the compiled module path, so we assume it if not bmifile then - local name = provide["logical-name"] .. bmi_extension(target) + local name = provide["logical-name"] .. compiler_support.get_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) + bmifile = path.join(compiler_support.get_outputdir(target, name), name) end end m.provides[provide["logical-name"]] = { @@ -354,6 +170,7 @@ function _parse_dependencies_data(target, moduleinfos) 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 @@ -418,7 +235,7 @@ function _topological_sort_visit(node, nodes, modules, output) if not n.tempmarked then local m2 = modules[n.objectfile] if m2 then - for name, provide in pairs(m1.provides) do + for name, _ in pairs(m1.provides) do if m2.requires and m2.requires[name] then _topological_sort_visit(n, nodes, modules, output) end @@ -448,43 +265,80 @@ function _topological_sort_get_first_unmarked_node(nodes) 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) +function _fill_needed_module(target, modules, module) + local needed_modules = {} + + for required_name, required_module in pairs(module.requires) do + table.insert(needed_modules, required_name) + table.join2(needed_modules, _fill_needed_module(target, modules, required_module)) end - return output + + return needed_modules 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 +function _get_package_modules(target, package, opt) + local package_modules + + local modulesdir = path.join(package:installdir(), "modules") + local metafiles = os.files(path.join(modulesdir, "*", "*.meta-info")) + for _, metafile in ipairs(metafiles) do + package_modules = package_modules or {} + local modulefile, name, metadata = _parse_meta_info(target, metafile) + package_modules[name] = {file = path.join(modulesdir, modulefile), metadata = metadata} + end + + return package_modules 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 = table.join(dep:get("sysincludedirs") or {}, dep:get("includedirs") or {}) - table.join2(headerpaths, includedirs) +-- get module dependencies +function get_module_dependencies(target, sourcebatch, opt) + local cachekey = target:name() .. "/" .. sourcebatch.rulename + local modules = compiler_support.memcache():get2("modules", cachekey) + if modules == nil or opt.regenerate then + modules = compiler_support.localcache():get2("modules", cachekey) + opt.progress = opt.progress or 0 + local changed = _dependency_scanner(target).generate_dependencies(target, sourcebatch, opt) + if changed or modules == nil then + local moduleinfos = compiler_support.load_moduleinfos(target, sourcebatch) + modules = _parse_dependencies_data(target, moduleinfos) + if modules then + _check_circular_dependencies(modules) + end + modules = cull_unused_modules(target, modules) + compiler_support.localcache():set2("modules", cachekey, modules) + compiler_support.localcache():save() + end + compiler_support.memcache():set2("modules", cachekey, modules) end - for _, pkg in pairs(target:pkgs()) do - local includedirs = table.join(pkg:get("sysincludedirs") or {}, pkg:get("includedirs") or {}) - table.join2(headerpaths, includedirs) + return modules +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 - table.join2(headerpaths, target:get("includedirs")) - local p = find_file(file, headerpaths) - assert(p, "find <%s> not found!", file) - return p + return headerunits, stl_headerunits end -- https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1689r5.html @@ -545,7 +399,7 @@ function fallback_generate_dependencies(target, jsonfile, sourcefile, preprocess 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 + if not module_depname and not compiler_support.has_module_extension(sourcefile) then module_depname = module_name_private end if module_depname and not module_deps_set:has(module_depname) then @@ -560,12 +414,12 @@ function fallback_generate_dependencies(target, jsonfile, sourcefile, preprocess 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) + module_dep["source-path"] = compiler_support.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) + module_dep["source-path"] = compiler_support.find_angle_header_file(target, module_depname) end module_dep["logical-name"] = module_depname table.insert(module_deps, module_dep) @@ -575,13 +429,13 @@ function fallback_generate_dependencies(target, jsonfile, sourcefile, preprocess end if module_name_export or internal then - local outputdir = get_outputdir(target, sourcefile) + local outputdir = compiler_support.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)) + provide["compiled-module-path"] = path.join(outputdir, (module_name_export or module_name_private) .. compiler_support.get_bmi_extension(target)) rule.provides = {} table.insert(rule.provides, provide) @@ -593,145 +447,104 @@ function fallback_generate_dependencies(target, jsonfile, sourcefile, preprocess 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 +-- extract packages modules dependencies +function get_all_packages_modules(target, opt) + local packages_modules --- 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 + -- parse all meta-info and append their informations to the package store + local packages = target:pkgs() or {} --- 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 + for _, deps in pairs(target:orderdeps()) do + table.join2(packages, deps:pkgs()) 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 {}) + for _, package in pairs(packages) do + local package_modules = _get_package_modules(target, package, opt) + if package_modules then + packages_modules = packages_modules or {} + table.join2(packages_modules, package_modules) + end 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 + return packages_modules +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) +-- 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 - - local modulehash = get_modulehash(target, sourcefile) - module_metadata._VENDOR_extension = { xmake = { name = name, file = path.join(modulehash, path.filename(sourcefile)) }} - return module_metadata + 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 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}) +-- get source modulefile for external target deps +function get_targetdeps_modules(target) + local sourcefiles + for _, dep in ipairs(target:orderdeps()) do + local sourcebatch = dep:sourcebatches()["c++.build.modules.builder"] + if sourcebatch and sourcebatch.sourcefiles then + for _, sourcefile in ipairs(sourcebatch.sourcefiles) do + local fileconfig = dep:fileconfig(sourcefile) + local public = (fileconfig and fileconfig.public and not fileconfig.external) or false + if public then + sourcefiles = sourcefiles or {} + table.insert(sourcefiles, sourcefile) + target:fileconfig_add(sourcefile, {external = true}) end end end end + return sourcefiles end -function get_modulehash(target, modulepath) - local key = path.directory(modulepath) .. target:name() - return hash.uuid(key):split("-", {plain = true})[1]:lower() -end +-- cull unused packages modules +-- removed named module not used in the translation units +-- when building a library we only cull external modules because we need module objectfiles to be linked inside the library +-- on an executable we cull explicitly referenced module +function cull_unused_modules(target, modules) -function get_metafile(target, modulefile) - local outputdir = get_outputdir(target, modulefile) - return path.join(outputdir, path.filename(modulefile) .. ".meta-info") -end + local cull_all_modules = target:kind() == "executable" + + local needed_modules = {} + for _, module in pairs(modules) do + local fileconfig = target:fileconfig(module.sourcefile) + local external = fileconfig and fileconfig.external + if not (cull_all_modules and external) then + goto CONTINUE + 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) + if module.provides and module.requires then + table.join2(needed_modules, _fill_needed_module(target, modules, module)) + end + + ::CONTINUE:: + end + + local culled = {} + for objectfile, module in pairs(modules) do + -- if cull_all_modules and modules.provides then + -- local name,_,_ = compiler_support.get_provided_module(module) + -- if module.requires then + -- for required, _ in pairs(module.requires) do + -- table.insert(needed_modules, required) + -- end + -- end + -- if table.find(needed_modules, name) then + -- culled[objectfile] = module + -- end + -- else + culled[objectfile] = module + -- end end - return outputdir + + return culled end diff --git a/xmake/rules/c++/modules/modules_support/stl_headers.lua b/xmake/rules/c++/modules/modules_support/stl_headers.lua index 2bc108765..4b2d6ed23 100644 --- a/xmake/rules/c++/modules/modules_support/stl_headers.lua +++ b/xmake/rules/c++/modules/modules_support/stl_headers.lua @@ -14,7 +14,7 @@ -- -- Copyright (C) 2015-present, TBOOX Open Source Group. -- --- @author Arthapz, ruki +-- @author ruki, Arthapz -- @file stl_headers.lua -- |
