diff options
| author | ruki <[email protected]> | 2025-04-21 09:45:41 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-04-21 09:45:41 +0800 |
| commit | 5d999522fe625c54a572fcdd28bc82f2850a3d5a (patch) | |
| tree | 4d1d5852b9ab35542e12ee4cd534ae4d90116d02 /xmake/rules/c++/modules/builder.lua | |
| parent | b82b7f253f29154093f665ba5096834e8d31bbf8 (diff) | |
| parent | a1fefbea336ad2294b853059d32850877f54bb90 (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.lua | 618 |
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 + |
