summaryrefslogtreecommitdiff
path: root/xmake/rules/c++/modules/builder.lua
diff options
context:
space:
mode:
authorruki <[email protected]>2025-04-21 09:45:41 +0800
committerGitHub <[email protected]>2025-04-21 09:45:41 +0800
commit5d999522fe625c54a572fcdd28bc82f2850a3d5a (patch)
tree4d1d5852b9ab35542e12ee4cd534ae4d90116d02 /xmake/rules/c++/modules/builder.lua
parentb82b7f253f29154093f665ba5096834e8d31bbf8 (diff)
parenta1fefbea336ad2294b853059d32850877f54bb90 (diff)
Merge pull request #6338 from Arthapz/simplify-module-dir-tree
(c++ modules support) Simplify module support directory tree
Diffstat (limited to 'xmake/rules/c++/modules/builder.lua')
-rw-r--r--xmake/rules/c++/modules/builder.lua618
1 files changed, 618 insertions, 0 deletions
diff --git a/xmake/rules/c++/modules/builder.lua b/xmake/rules/c++/modules/builder.lua
new file mode 100644
index 000000000..1f3eec988
--- /dev/null
+++ b/xmake/rules/c++/modules/builder.lua
@@ -0,0 +1,618 @@
+--!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("core.base.hashset")
+import("async.runjobs")
+import("private.async.buildjobs")
+import("core.tool.compiler")
+import("core.project.config")
+import("core.project.depend")
+import("utils.progress")
+import("support")
+import("scanner")
+
+-- build target modules
+function _build_modules(target, sourcebatch, modules, opt)
+ local objectfiles = sourcebatch.objectfiles
+ for _, objectfile in ipairs(objectfiles) do
+ local module = modules[objectfile]
+ if not module then
+ goto continue
+ end
+
+ local name, _, cppfile = support.get_provided_module(module)
+ cppfile = cppfile or module.cppfile
+
+ local deps = {}
+ for name, req in pairs(module.requires or {}) do
+ -- we need to use the full path as dep name if requre item is headerunit
+ local dep = name
+ if req.method:startswith("include-") and req.path then
+ dep = req.path
+ end
+ dep = path.normalize(dep)
+ local depname = target:fullname() .. "/module/" .. dep
+ table.insert(deps, depname)
+ end
+ opt.build_module(deps, module, name, objectfile, cppfile)
+
+ ::continue::
+ end
+end
+
+-- build target headerunits
+function _build_headerunits(target, headerunits, opt)
+ local outputdir = 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) .. support.get_bmi_extension(target))
+ local key = path.normalize(headerunit.path)
+ local build = should_build(target, headerunit.path, bmifile, {key = key, headerunit = true})
+ if build then
+ mark_build(target, key)
+ end
+ opt.build_headerunit(headerunit, key, bmifile, outputdir, build)
+ end
+end
+
+-- check if flags are compatible for module reuse
+function _are_flags_compatible(target, other, cppfile)
+ local compinst1 = target:compiler("cxx")
+ local flags1 = compinst1:compflags({sourcefile = cppfile, target = target})
+
+ local compinst2 = other:compiler("cxx")
+ local flags2 = compinst2:compflags({sourcefile = cppfile, target = other})
+
+ -- strip unrelevent flags
+ flags1 = support.strip_flags(target, flags1)
+ flags2 = support.strip_flags(target, flags2)
+
+ if #flags1 ~= #flags2 then
+ return false
+ end
+
+ table.sort(flags1)
+ table.sort(flags2)
+
+ for i = 1, #flags1 do
+ if flags1[i] ~= flags2[i] then
+ return false
+ end
+ end
+ return true
+end
+
+-- try to reuse modules from other target
+function _try_reuse_modules(target, modules)
+ for _, module in pairs(modules) do
+ local name, provide, cppfile = support.get_provided_module(module)
+ if not provide then
+ goto continue
+ end
+
+ cppfile = cppfile or module.cppfile
+
+ local fileconfig = target:fileconfig(cppfile)
+ local public = fileconfig and (fileconfig.public or fileconfig.external)
+ if not public then
+ goto continue
+ end
+
+ for _, dep in ipairs(target:orderdeps()) do
+ if not _are_flags_compatible(target, dep, cppfile) then
+ goto nextdep
+ end
+ local mapped = get_from_target_mapper(dep, name)
+ if mapped then
+ support.memcache():set2(target:fullname() .. name, "reuse", true)
+ add_module_to_target_mapper(target, mapped.name, mapped.sourcefile, mapped.bmi, table.join(mapped.opt or {}, {target = dep}))
+ break
+ end
+ ::nextdep::
+ end
+
+ ::continue::
+ end
+ return modules
+end
+
+-- should we build this module or headerunit ?
+function should_build(target, sourcefile, bmifile, opt)
+ opt = opt or {}
+ local objectfile = opt.objectfile
+ 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 {})
+ local depvalues = {compinst:program(), compflags}
+
+ -- force rebuild a module if any of its module dependency is rebuilt
+ local requires = opt.requires
+ if requires then
+ for required, _ in table.orderpairs(requires) do
+ local m = get_from_target_mapper(target, required)
+ if m then
+ local rebuild = (m.opt and m.opt.target) and support.memcache():get2("should_build_in_" .. m.opt.target:fullname(), m.key)
+ or support.memcache():get2("should_build_in_" .. target:fullname(), m.key)
+ if rebuild then
+ dependinfo.files = {}
+ table.insert(dependinfo.files, sourcefile)
+ dependinfo.values = depvalues
+ return true, dependinfo
+ end
+ end
+ end
+ end
+
+ -- reused
+ if opt.name then
+ local m = get_from_target_mapper(target, opt.name)
+ if m and m.opt and m.opt.target then
+ local rebuild = support.memcache():get2("should_build_in_" .. m.opt.target:fullname(), m.key)
+ if rebuild then
+ dependinfo.files = {}
+ table.insert(dependinfo.files, sourcefile)
+ dependinfo.values = depvalues
+ end
+ return rebuild, dependinfo
+ end
+ end
+
+ -- need build this object?
+ local dryrun = option.get("dry-run")
+ 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
+ dependinfo.files = {}
+ table.insert(dependinfo.files, sourcefile)
+ dependinfo.values = depvalues
+ return true, dependinfo
+ 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 = 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 table.orderpairs(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:fullname() .. "module_mapper" .. (mode or "")
+end
+
+function _is_duplicated_headerunit(target, key)
+ local _, mapper_keys = get_target_module_mapper(target)
+ return mapper_keys[key]
+end
+
+function _builder(target)
+ local cachekey = tostring(target)
+ local builder = support.memcache():get2("builder", cachekey)
+ if builder == nil then
+ if target:has_tool("cxx", "clang", "clangxx", "clang_cl") 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
+ support.memcache():set2("builder", cachekey, builder)
+ end
+ return builder
+end
+
+function mark_build(target, name)
+ support.memcache():set2("should_build_in_" .. target:fullname(), name, true)
+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:fullname() .. "/module/build_modules", {rootjob = opt.rootjob})
+
+ -- add populate module job
+ local modulesjobs = {}
+ local populate_jobname = target:fullname() .. "/module/populate_module_map"
+ modulesjobs[populate_jobname] = {
+ name = populate_jobname,
+ job = batchjobs:newjob(populate_jobname, function(_, _)
+ _try_reuse_modules(target, modules)
+ _builder(target).populate_module_map(target, modules)
+ end)
+ }
+
+ -- add module jobs
+ _build_modules(target, sourcebatch, modules, table.join(opt, {
+ build_module = function(deps, module, name, objectfile, cppfile)
+ local job_name = target:fullname() .. "/module/" .. path.normalize(name or cppfile)
+ modulesjobs[job_name] = _builder(target).make_module_buildjobs(target, batchjobs, job_name, deps,
+ {module = module, objectfile = objectfile, cppfile = cppfile})
+ end
+ }))
+
+ -- build batchjobs for modules
+ build_batchjobs_for_modules(modulesjobs, batchjobs, opt.rootjob)
+end
+
+-- build modules for jobgraph
+function build_modules_for_jobgraph(target, jobgraph, sourcebatch, modules, opt)
+ local jobdeps = {}
+ local jobsize = jobgraph:size()
+ local build_modules_group = target:fullname() .. "/module/build_modules"
+ jobgraph:group(build_modules_group, function ()
+
+ -- add populate module job
+ local populate_jobname = target:fullname() .. "/module/populate_module_map"
+ jobgraph:add(populate_jobname, function(index, total, opt)
+ _try_reuse_modules(target, modules)
+ _builder(target).populate_module_map(target, modules)
+ end)
+
+ -- add module jobs
+ _build_modules(target, sourcebatch, modules, table.join(opt, {
+ build_module = function(deps, module, name, objectfile, cppfile)
+ local jobname = target:fullname() .. "/module/" .. path.normalize(name or cppfile)
+ _builder(target).make_module_jobgraph(target, jobgraph, {
+ module = module, objectfile = objectfile, cppfile = cppfile
+ })
+ jobdeps[jobname] = table.join(populate_jobname, deps)
+ end})
+ )
+ end)
+ if jobgraph:size() > jobsize then
+ return build_modules_group, jobdeps
+ end
+end
+
+-- build modules for batchcmds
+function build_modules_for_batchcmds(target, batchcmds, sourcebatch, modules, opt)
+ local depmtime = 0
+ opt.progress = opt.progress or 0
+
+ _try_reuse_modules(target, modules)
+ _builder(target).populate_module_map(target, modules)
+
+ -- build modules
+ _build_modules(target, sourcebatch, modules, table.join(opt, {
+ build_module = function(_, module, _, objectfile, cppfile)
+ depmtime = math.max(depmtime, _builder(target).make_module_buildcmds(target, batchcmds, {
+ module = module, cppfile = cppfile, objectfile = objectfile, progress = opt.progress}))
+ end
+ }))
+
+ batchcmds:set_depmtime(depmtime)
+end
+
+-- build headerunits for batchjobs
+function build_headerunits_for_batchjobs(target, batchjobs, sourcebatch, modules, opt)
+
+ local user_headerunits, stl_headerunits = 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:fullname() .. "/module/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:fullname() .. "/module/" .. path.normalize(key)
+ local job = _builder(target).make_headerunit_buildjobs(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
+
+-- build headerunits for jobgraph
+function build_headerunits_for_jobgraph(target, jobgraph, sourcebatch, modules, opt)
+ local user_headerunits, stl_headerunits = 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)
+ local jobsize = jobgraph:size()
+ local build_headerunits_group = target:fullname() .. "/module/build_headerunits"
+ jobgraph:group(build_headerunits_group, function ()
+ 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:fullname() .. "/module/" .. path.normalize(key)
+ _builder(target).make_headerunit_jobgraph(target,
+ job_name, jobgraph, headerunit, bmifile, outputdir, table.join(opt, {build = build}))
+ end
+ }))
+ 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)
+ if jobgraph:size() > jobsize then
+ return build_headerunits_group
+ end
+end
+
+-- build headerunits for batchcmds
+function build_headerunits_for_batchcmds(target, batchcmds, sourcebatch, modules, opt)
+ local user_headerunits, stl_headerunits = 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_buildcmds(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
+
+-- build modules and headerunits, and we need to build headerunits first
+function build_modules_and_headerunits(target, jobgraph, sourcebatch, modules, opt)
+ if jobgraph.add_orders then
+ local build_modules_group, jobdeps = build_modules_for_jobgraph(target, jobgraph, sourcebatch, modules, opt)
+ local build_headerunits_group = build_headerunits_for_jobgraph(target, jobgraph, sourcebatch, modules, opt)
+ if build_modules_group then
+ for jobname, deps in pairs(jobdeps) do
+ for _, depname in ipairs(deps) do
+ jobgraph:add_orders(depname, jobname)
+ end
+ end
+ if build_headerunits_group then
+ jobgraph:add_orders(build_headerunits_group, build_modules_group)
+ end
+ end
+ elseif jobgraph.runcmds then
+ build_headerunits_for_batchcmds(target, jobgraph, sourcebatch, modules, opt)
+ build_modules_for_batchcmds(target, jobgraph, sourcebatch, modules, opt)
+ elseif jobgraph.newjob then -- deprecated
+ build_modules_for_batchjobs(target, jobgraph, sourcebatch, modules, opt)
+ build_headerunits_for_batchjobs(target, jobgraph, sourcebatch, modules, opt)
+ end
+end
+
+-- generate metadata
+function generate_metadata(target, modules)
+ local public_modules
+ for _, module in table.orderpairs(modules) do
+ local _, _, cppfile = support.get_provided_module(module)
+ local fileconfig = target:fileconfig(cppfile)
+ local public = fileconfig and fileconfig.public
+ if public then
+ public_modules = public_modules or {}
+ table.insert(public_modules, module)
+ end
+ end
+
+ if not public_modules then
+ return
+ end
+
+ local jobs = option.get("jobs") or os.default_njob()
+ runjobs(target:fullname() .. "/module/install_modules", function(index, total, jobopt)
+ local module = public_modules[index]
+ local name, _, cppfile = support.get_provided_module(module)
+ local metafilepath = support.get_metafile(target, cppfile)
+ progress.show(jobopt.progress, "${color.build.target}<%s> generating.module.metadata %s", target:fullname(), name)
+ local metadata = _generate_meta_module_info(target, name, cppfile, module.requires)
+ json.savefile(metafilepath, metadata)
+ end, {comax = jobs, total = #public_modules})
+end
+
+-- flush target module mapper keys
+function flush_target_module_mapper_keys(target)
+ local memcache = support.memcache()
+ memcache:set2(target:fullname(), "module_mapper_keys", nil)
+end
+
+-- get or create a target module mapper
+function get_target_module_mapper(target)
+ local memcache = support.memcache()
+ local mapper = memcache:get2(target:fullname(), "module_mapper")
+ if not mapper then
+ mapper = {}
+ memcache:set2(target:fullname(), "module_mapper", mapper)
+ end
+
+ -- we generate the keys map to optimise the efficiency of _is_duplicated_headerunit
+ local mapper_keys = memcache:get2(target:fullname(), "module_mapper_keys")
+ if not mapper_keys then
+ mapper_keys = {}
+ for _, item in pairs(mapper) do
+ if item.key then
+ mapper_keys[item.key] = item
+ end
+ end
+ memcache:set2(target:fullname(), "module_mapper_keys", mapper_keys)
+ end
+ return mapper, mapper_keys
+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)
+ if not mapper[name] then
+ mapper[name] = {name = name, key = name, bmi = bmifile, sourcefile = sourcefile, opt = opt}
+ end
+ flush_target_module_mapper_keys(target)
+end
+
+-- add a headerunit to target mapper
+function add_headerunit_to_target_mapper(target, headerunit, bmifile)
+ local mapper = get_target_module_mapper(target)
+ local key = hash.uuid(path.normalize(headerunit.path))
+ local deduplicated = _is_duplicated_headerunit(target, key)
+ if deduplicated then
+ mapper[headerunit.name] = {name = headerunit.name, key = key, aliasof = deduplicated.name, headerunit = headerunit}
+ else
+ mapper[headerunit.name] = {name = headerunit.name, key = key, headerunit = headerunit, bmi = bmifile}
+ end
+ flush_target_module_mapper_keys(target)
+ return deduplicated and true or false
+end
+
+-- check if dependencies changed
+function is_dependencies_changed(target, module)
+ local cachekey = target:fullname() .. module.name
+ local requires = hashset.from(table.keys(module.requires or {}))
+ local oldrequires = support.memcache():get2(cachekey, "oldrequires")
+ local changed = false
+ if oldrequires then
+ if oldrequires ~= requires then
+ requires_changed = true
+ else
+ for required in requires:items() do
+ if not oldrequires:has(required) then
+ requires_changed = true
+ break
+ end
+ end
+ end
+ end
+ return requires, changed
+end
+
+-- patch sourcebatch
+function patch_sourcebatch(target, sourcebatch, opt)
+
+ -- add target deps modules
+ if target:orderdeps() then
+ local deps_sourcefiles = scanner.get_targetdeps_modules(target)
+ if deps_sourcefiles then
+ table.join2(sourcebatch.sourcefiles, deps_sourcefiles)
+ end
+ end
+
+ -- append std module
+ local std_modules = support.get_stdmodules(target)
+ if std_modules then
+ table.join2(sourcebatch.sourcefiles, std_modules)
+ end
+
+ -- extract packages modules dependencies
+ local package_modules_data = scanner.get_all_packages_modules(target, opt)
+ if package_modules_data then
+ -- append to sourcebatch
+ for _, package_module_data in table.orderpairs(package_modules_data) do
+ table.insert(sourcebatch.sourcefiles, package_module_data.file)
+ target:fileconfig_set(package_module_data.file, {external = package_module_data.external, defines = package_module_data.metadata.defines})
+ end
+ end
+
+ -- patch objectfiles and dependencies
+ 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
+