--!A cross-platform build utility based on Lua
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015-present, Xmake Open Source Community.
--
-- @author      ruki, Arthapz
-- @file        builder.lua
--

-- imports
import("core.base.json")
import("core.base.option")
import("core.base.hashset")
import("core.base.profiler")
import("async.runjobs")
import("private.action.clean.remove_files")
import("private.async.buildjobs")
import("core.tool.compiler")
import("core.project.config")
import("core.project.depend")
import("utils.progress")
import("support")
import("scanner")
import("mapper")

function _builder(target)
    return support.import_implementation_of(target, "builder")
end

-- generate meta module informations for package / other buildsystems import
--
-- e.g
-- {
--      "flags": ["--std=c++23"]
--      "imports": ["std", "bar"]
--      "name": "foo"
--      "file": "foo.cppm"
-- }
function _generate_meta_module_info(target, module)
    local fileconfig = target:fileconfig(module.sourcefile)
    local defines = table.join(target:get("defines") or {}, fileconfig and fileconfig.defines or {})
    local undefines = table.join(target:get("undefines") or {}, fileconfig and fileconfig.undefines or {})
    local modulehash = support.get_modulehash(module.sourcefile)
    local module_metadata = {name = module.name, file = path.join(modulehash, path.filename(module.sourcefile)), defines = defines, undefines = undefines}

    -- add imports
    for name, _ in table.orderpairs(module.deps) do
        module_metadata.imports = module_metadata.imports or {}
        table.insert(module_metadata.imports, name)
    end
    return module_metadata
end

function _get_module_buildgroup_for(target, moduletype)
    return target:fullname() .. "/modules/build/" .. moduletype
end

function _get_module_buildfilejob_for(target, sourcefile, moduletype)
    return _get_module_buildgroup_for(target, moduletype) .. "/" .. sourcefile
end

function _get_headerunit_buildgroup_for(target)
    return target:fullname() .. "/headerunit/build"
end

function _get_headerunit_buildfilejob_for(target, sourcefile)
    return _get_headerunit_buildgroup_for(target) .. "/" .. sourcefile
end

function _get_buildfilejob_for(target, sourcefile, opt)
    if opt and opt.headerunit then
        return _get_headerunit_buildfilejob_for(target, sourcefile)
    end
    return _get_module_buildfilejob_for(target, sourcefile, opt.moduletype)
end

function _get_jobdeps(target, module, jobgraph, buildfilejob)

    local memcache = support.memcache()
    local jobdeps = {}
    local moduletype = support.has_two_phase_compilation_support(target) and "bmi" or "onephase"
    for dep_name, dep in pairs(module.deps) do
        local dep_name = dep_name
        if dep.headerunit then
            dep_name = dep_name .. dep.key
        end
        local dep_module = mapper.get(target, dep_name)
        local dep_sourcefile = dep_module.sourcefile
        if dep.headerunit then
            dep_sourcefile = dep_sourcefile .. dep_module.key
        end

        local reused, from = support.is_reused(target, dep_sourcefile)
        local dep_jobname
        if dep_module.headerunit then
            dep_jobname = _get_headerunit_buildfilejob_for(reused and from or target, dep_sourcefile)
        else
            dep_jobname = _get_module_buildfilejob_for(reused and from or target, dep_sourcefile, moduletype)
        end
        -- if dep_jobname is available, order it now
        if jobgraph:has(dep_jobname) then
            jobdeps[buildfilejob] = jobdeps[buildfilejob] or {}
            table.insert(jobdeps[buildfilejob], dep_jobname)
        -- if dep_jobname is not currently available, save deps for later ordering
        else
            local dependent_jobs = memcache:get2("dependent_jobs", dep_jobname) or {}
            table.insert(dependent_jobs, buildfilejob)
            memcache:set2("dependent_jobs", dep_jobname, dependent_jobs)
        end
    end
    return jobdeps
end

function _get_saved_jobdeps_for(jobgraph, buildfilejob)
    local memcache = support.memcache()
    local dependent_jobs = memcache:get2("dependent_jobs", buildfilejob)
    local jobdeps = {}
    for _, dependent_job in ipairs(dependent_jobs) do
        if jobgraph:has(dependent_job) then
            jobdeps[dependent_job] = jobdeps[dependent_job] or {}
            table.insert(jobdeps[dependent_job], buildfilejob)
        end
    end
    return jobdeps
end

function _in_project_generator()
    local memcache = support.memcache()
    local in_project_generator = memcache:get("in_project_generator")
    if in_project_generator == nil then
        local val = os.getenv("XMAKE_IN_PROJECT_GENERATOR")
        if val and val ~= "" then
            in_project_generator = true
        end
        in_project_generator = in_project_generator or false
        memcache:set("in_project_generator", in_project_generator)
    end
    return in_project_generator
end

-- should we build this module or headerunit ?
function should_build(target, module)

    -- if we are generating project files, we always need to generate all build commands.
    -- @see https://github.com/xmake-io/xmake/issues/6600
    if _in_project_generator() then
        return true
    end

    profiler.enter(target:fullname(), "c++ modules", "builder", "check if " .. (module.name or module.sourcefile) .. " should be rebuilt")
    local memcache = support.memcache()
    local _should_build = memcache:get2(target:fullname(), "should_build_" .. module.sourcefile)
    if _should_build == nil then
        local key = module.headerunit and module.sourcefile .. module.key or module.sourcefile
        local reused, from = support.is_reused(target, key)
        if reused then
            local build = should_build(from, module)
            memcache:set2(target:fullname(), "should_build_" .. module.sourcefile, build)
            profiler.leave(target:fullname(), "c++ modules", "builder", "check if " .. (module.name or module.sourcefile) .. " should be rebuilt")
            return build
        end
        local compinst = compiler.load("cxx", {target = target})
        local compflags = compinst:compflags({sourcefile = module.sourcefile, target = target})

        local dependfile = target:dependfile(module.bmifile or module.objectfile)
        local dependinfo = {}
        dependinfo.files = {module.sourcefile}
        dependinfo.values = {compinst:program(), compflags}
        local objectfile_exists = (module.headerunit or support.is_bmionly(target, module.sourcefile)) and true or os.isfile(module.objectfile)
        dependinfo.lastmtime = (os.isfile(module.bmifile or module.objectfile) and objectfile_exists) and os.mtime(dependfile) or 0

        local fileconfig = target:fileconfig(module.sourcefile)
        local from_package = fileconfig and fileconfig.from_package
        local is_std = path.basename(module.sourcefile) == "std" or path.basename(module.sourcefile) == "std.compat"
        local rebuild = target:is_rebuilt() and not from_package and not is_std
        local old_dependinfo = rebuild and {} or (depend.load(dependfile) or {})
        old_dependinfo.files = {module.sourcefile}

        -- need build this object?
        local dryrun = option.get("dry-run")
        if dryrun or depend.is_changed(old_dependinfo, dependinfo) then
            depend.save(dependinfo, dependfile)
            memcache:set2(target:fullname(), "should_build_" .. module.sourcefile, true)
            profiler.leave(target:fullname(), "c++ modules", "builder", "check if " .. (module.name or module.sourcefile) .. " should be rebuilt")
            return true
        end

        -- force rebuild a module if any of its module dependency is rebuilt
        for dep_name, dep_module in table.orderpairs(module.deps) do
            local mapped_dep = mapper.get(target, dep_module.headerunit and dep_name .. dep_module.key or dep_name)

            if should_build(target, mapped_dep) then
                depend.save(dependinfo, dependfile)
                memcache:set2(target:fullname(), "should_build_" .. module.sourcefile, true)
                profiler.leave(target:fullname(), "c++ modules", "builder", "check if " .. (module.name or module.sourcefile) .. " should be rebuilt")
                return true, true
            end
        end

        memcache:set2(target:fullname(), "should_build_" .. module.sourcefile, false)
        profiler.leave(target:fullname(), "c++ modules", "builder", "check if " .. (module.name or module.sourcefile) .. " should be rebuilt")
        return false
    end
    profiler.leave(target:fullname(), "c++ modules", "builder", "check if " .. (module.name or module.sourcefile) .. " should be rebuilt")
    return _should_build
end

-- build modules for jobgraph
-- only build bmis if two phase compilation is supported
-- it not build also objectfiles
function build_modules_for_jobgraph(target, jobgraph, built_modules)

    profiler.enter(target:fullname(), "c++ modules", "builder", "schedule module bmi build jobs")
    local builder = _builder(target)
    local has_two_phase_compilation_support = support.has_two_phase_compilation_support(target)
    local jobdeps = {}
    local buildfilejobs = {}

    -- get named modules
    local _built_modules = {}
    for _, sourcefile in ipairs(built_modules) do
        local module = mapper.get(target, sourcefile)
        if module.name then
            table.insert(_built_modules, sourcefile)
        else
            if not support.is_bmionly(target, sourcefile) then
                local buildfilejob = _get_module_buildfilejob_for(target, sourcefile, "objectfile")
                table.join2(jobdeps, _get_jobdeps(target, module, jobgraph, buildfilejob))
                table.insert(buildfilejobs, buildfilejob)
                local objbuildfilejob = target:fullname() .. "/obj/" .. sourcefile
                if jobgraph:has(objbuildfilejob) then
                    jobdeps[objbuildfilejob] = {buildfilejob}
                end
            end
        end
    end
    -- add module jobs
    local moduletype = has_two_phase_compilation_support and "bmi" or "onephase"
    local buildgroup = _get_module_buildgroup_for(target, moduletype)
    jobgraph:group(buildgroup, function()
        for _, sourcefile in ipairs(_built_modules) do
            local module = mapper.get(target, sourcefile)
            local bmionly = support.is_bmionly(target, module.sourcefile)
            local buildfilejob = _get_module_buildfilejob_for(target, sourcefile, moduletype)
            table.insert(buildfilejobs, buildfilejob)
            jobgraph:add(buildfilejob, function(_, _, jobopt)
                progress.set_target(jobopt.progress, target)
                -- build bmi if named job
                jobopt.bmi = module.name
                -- build objectfile here if two phase compilation is not supported
                jobopt.objectfile = not has_two_phase_compilation_support and not bmionly
                builder.make_module_job(target, module, jobopt)
            end)
            table.join2(jobdeps, _get_jobdeps(target, module, jobgraph, buildfilejob))

            -- if two phase compilation supported set jobdeps for objectfile job
            if has_two_phase_compilation_support and not bmionly then
                local objbuildfilejob = _get_module_buildfilejob_for(target, sourcefile, "objectfile")
                jobdeps[objbuildfilejob] = {buildfilejob}
            end
        end
    end)

    -- insert saved jobdeps
    for _, buildfilejob in ipairs(buildfilejobs) do
        table.join2(jobdeps, _get_saved_jobdeps_for(jobgraph, buildfilejob))
    end

    -- apply jobdeps
    for jobname, deps in pairs(jobdeps) do
        for _, depname in ipairs(deps) do
            jobgraph:add_orders(depname, jobname)
        end
    end
    profiler.leave(target:fullname(), "c++ modules", "builder", "schedule module bmi build jobs")
end

-- build modules objectfiles for jobgraph if two phase compilation is supported
function build_objectfiles_for_jobgraph(target, jobgraph, built_modules)

    profiler.enter(target:fullname(), "c++ modules", "builder", "schedule module objectfiles build jobs")
    local builder = _builder(target)
    local has_two_phase_compilation_support = support.has_two_phase_compilation_support(target)
    local buildgroup = _get_module_buildgroup_for(target, "objectfile")
    local _built_modules = {}
    if has_two_phase_compilation_support then
        _built_modules = built_modules
    else
        for _, sourcefile in ipairs(built_modules) do
            local module = mapper.get(target, sourcefile)
            if not module.name then
                table.insert(_built_modules, sourcefile)
            end
        end
    end
    jobgraph:group(buildgroup, function()
        for _, sourcefile in ipairs(_built_modules) do
            if not support.is_bmionly(target, sourcefile) then
                local module = mapper.get(target, sourcefile)
                local buildfilejob = _get_module_buildfilejob_for(target, sourcefile, "objectfile")
                jobgraph:add(buildfilejob, function(_, _, jobopt)
                    progress.set_target(jobopt.progress, target)
                    jobopt.bmi = false
                    jobopt.objectfile = true
                    builder.make_module_job(target, module, jobopt)
                end)
            end
        end
    end)
    profiler.leave(target:fullname(), "c++ modules", "builder", "schedule module objectfiles build jobs")
end

-- build batchjobs for modules
function build_batchjobs_for_modules(modules, batchjobs, rootjob)
    return buildjobs(modules, batchjobs, rootjob)
end

-- build modules for batchjobs (deprecated)
function build_modules_for_batchjobs(target, batchjobs, built_modules, opt)

    opt.rootjob = batchjobs:group_leave() or opt.rootjob
    batchjobs:group_enter(target:fullname() .. "/build_cxxmodules_bmi", {rootjob = opt.rootjob})

    local builder = _builder(target)
    local has_two_phase_compilation_support = support.has_two_phase_compilation_support(target)

    -- if two phase supported only build named modules bmi
    local _built_modules = {}
    for _, sourcefile in ipairs(built_modules) do
        local module = mapper.get(target, sourcefile)
        if module.name then
            table.insert(_built_modules, sourcefile)
        end
    end

    -- add module jobs
    local jobs
    local moduletype = has_two_phase_compilation_support and "bmi" or "onephase"
    for _, sourcefile in ipairs(_built_modules) do
        local sourcefile = sourcefile
        jobs = jobs or {}
        local bmionly = support.is_bmionly(target, sourcefile)
        local module = mapper.get(target, sourcefile)

        local buildfilejob = _get_module_buildfilejob_for(target, sourcefile, moduletype)
        local deps = {}
        for dep_name, dep in pairs(module.deps) do
            local dep_name = dep_name
            if dep.headerunit then
                dep_name = dep_name .. dep.key
            end
            local dep_module = mapper.get(target, dep_name)
            local dep_sourcefile = dep_module.sourcefile
            if dep.headerunit then
                dep_sourcefile = dep_sourcefile .. dep_module.key
            end

            local reused, from = support.is_reused(target, dep_sourcefile)
            local dep_jobname
            if dep_module.headerunit then
                dep_jobname = _get_headerunit_buildfilejob_for(reused and from or target, dep_sourcefile)
            else
                dep_jobname = _get_module_buildfilejob_for(reused and from or target, dep_sourcefile, moduletype)
            end
            table.insert(deps, dep_jobname)
        end
        jobs[buildfilejob] = {
            name = buildfilejob,
            deps = deps,
            sourcefile = module.sourcefile,
            job = batchjobs:newjob(buildfilejob, function(_, _, jobopt)
                -- build bmi if named job
                jobopt.bmi = module.name
                -- build objectfile here if two phase compilation is not supported
                jobopt.objectfile = not has_two_phase_compilation_support and not bmionly
                builder.make_module_job(target, module, jobopt)
            end)}
    end

    return jobs
end

-- build modules objectfiles for batchjobs if two phase compilation is supported (deprecated)
function build_objectfiles_for_batchjobs(target, batchjobs, built_modules, opt)

    opt.rootjob = batchjobs:group_leave() or opt.rootjob
    batchjobs:group_enter(target:fullname() .. "/build_cxxmodules_objectfiles", {rootjob = opt.rootjob})
    local builder = _builder(target)
    local has_two_phase_compilation_support = support.has_two_phase_compilation_support(target)
    local _built_modules = {}
    if has_two_phase_compilation_support then
        _built_modules = built_modules
    else
        for _, sourcefile in ipairs(built_modules) do
            local module = mapper.get(target, sourcefile)
            if not module.name then
                table.insert(_built_modules, sourcefile)
            end
        end
    end

    local jobs
    for _, sourcefile in ipairs(_built_modules) do
        local sourcefile = sourcefile
        if not support.is_bmionly(target, sourcefile) then
            jobs = jobs or {}
            local module = mapper.get(target, sourcefile)
            local buildfilejob = _get_module_buildfilejob_for(target, sourcefile, "objectfile")
            jobs[buildfilejob] = {
                name = buildfilejob,
                sourcefile = module.sourcefile,
                job = batchjobs:newjob(buildfilejob, function(_, _, jobopt)
                    jobopt.bmi = false
                    jobopt.objectfile = true
                    builder.make_module_job(target, module, jobopt)
                end)}
        end
    end

    return jobs
end

-- build modules for batchcmds
function build_modules_for_batchcmds(target, batchcmds, built_modules, opt)
    opt.progress = opt.progress or 0
    local depmtime = 0
    -- build modules
    local builder = _builder(target)
    local has_two_phase_compilation_support = support.has_two_phase_compilation_support(target)

    local _built_modules = {}
    for _, sourcefile in ipairs(built_modules) do
        local module = mapper.get(target, sourcefile)
        if module.name then
            table.insert(_built_modules, sourcefile)
        end
    end

    for _, sourcefile in ipairs(_built_modules) do
        local bmionly = support.is_bmionly(target, sourcefile)
        local module = mapper.get(target, sourcefile)
        local jobopt = {}
        jobopt.bmi = module.name
        jobopt.objectfile = not has_two_phase_compilation_support and not bmionly
        jobopt.progress = opt.progress
        depmtime = math.max(depmtime, builder.make_module_buildcmds(target, batchcmds, module, jobopt))
    end
    batchcmds:set_depmtime(depmtime)
end

-- build modules objectfiles for batchcmds if two phase compilation is supported
function build_objectfiles_for_batchcmds(target, batchcmds, built_modules, opt)

    opt.progress = opt.progress or 0
    local depmtime = 0
    local builder = _builder(target)
    local has_two_phase_compilation_support = support.has_two_phase_compilation_support(target)
    local _built_modules = {}
    if has_two_phase_compilation_support then
        _built_modules = built_modules
    else
        for _, sourcefile in ipairs(built_modules) do
            local module = mapper.get(target, sourcefile)
            if not module.name then
                table.insert(_built_modules, sourcefile)
            end
        end
    end

    for _, sourcefile in ipairs(_built_modules) do
        if not support.is_bmionly(target, sourcefile) then
            local module = mapper.get(target, sourcefile)
            local jobopt = {}
            jobopt.bmi = false
            jobopt.objectfile = true
            jobopt.progress = opt.progress
            depmtime = math.max(depmtime, builder.make_module_buildcmds(target, batchcmds, module, jobopt))
        end
    end
    batchcmds:set_depmtime(depmtime)
end

-- build headerunits for jobgraph
function build_headerunits_for_jobgraph(target, jobgraph, built_stlheaderunits, built_headerunits)

    profiler.enter(target:fullname(), "c++ modules", "builder", "schedule headerunits build jobs")
    local builder = _builder(target)
    function make_headerunit_job(headerfile, opt)
        local reused, from = support.is_reused(target, headerfile)
        local _target = reused and from or target
        local headerunit = mapper.get(target, headerfile)
        if not headerunit.alias then
            local buildfilejob = _get_headerunit_buildfilejob_for(_target, headerunit.sourcefile .. headerunit.key)
            if not jobgraph:has(buildfilejob) then
                jobgraph:add(buildfilejob, function(_, _, jobopt)
                    progress.set_target(jobopt.progress, _target)
                    builder.make_headerunit_job(_target, headerunit, table.join(jobopt, opt))
                end)
            end
        end
    end

    local headerunit_buildgroup = _get_headerunit_buildgroup_for(target)
    if built_stlheaderunits then
        -- build stl header units first as other headerunits may need them
        jobgraph:group(headerunit_buildgroup, function()
            for _, headerfile in ipairs(built_stlheaderunits) do
                make_headerunit_job(headerfile, {stl_headerunit = true})
            end
        end)
    end

    if built_headerunits then
        jobgraph:group(headerunit_buildgroup, function()
            for _, headerfile in ipairs(built_headerunits) do
                make_headerunit_job(headerfile)
            end
        end)
    end
    profiler.leave(target:fullname(), "c++ modules", "builder", "schedule headerunits build jobs")
end

-- build headerunits for batchjobs
function build_headerunits_for_batchjobs(target, batchjobs, built_stlheaderunits, built_headerunits, opt)

    -- 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() .. "/build_headerunits", {rootjob = opt.rootjob})
    local builder = _builder(target)
    local jobs = {}
    function make_headerunit_job(headerfile, opt)
        local reused, from = support.is_reused(target, headerfile)
        local _target = reused and from or target
        local headerunit = mapper.get(target, headerfile)
        if not headerunit.alias then
            local buildfilejob = _get_headerunit_buildfilejob_for(_target, headerunit.sourcefile .. headerunit.key)
            jobs[buildfilejob] = {
                name = buildfilejob,
                sourcefile = headerunit.sourcefile,
                job = batchjobs:newjob(buildfilejob, function(_, _, jobopt)
                    builder.make_headerunit_job(_target, headerunit, table.join(jobopt, opt))
                end)}
        end
    end

    if built_stlheaderunits then
        -- build stl header units first as other headerunits may need them
        for _, headerfile in ipairs(built_stlheaderunits) do
            make_headerunit_job(headerfile, {stl_headerunit = true})
        end
    end

    if built_headerunits then
        for _, headerfile in ipairs(built_headerunits) do
            make_headerunit_job(headerfile)
        end
    end
    return jobs
end

-- build headerunits for batchcmds
function build_headerunits_for_batchcmds(target, batchcmds, built_stlheaderunits, built_headerunits, opt)

    local builder = _builder(target)
    function make_headerunit_buildcmds(headerfile, jobopt)
        local reused, from = support.is_reused(target, headerfile)
        local _target = reused and from or target
        local headerunit = mapper.get(target, headerfile)
        if not headerunit.alias then
            builder.make_headerunit_buildcmds(_target, batchcmds, headerunit, table.join(opt, jobopt))
        end
    end

    -- build stl header units first as other headerunits may need them
    -- opt.stl_headerunit = true
    for _, headerfile in ipairs(built_stlheaderunits) do
        make_headerunit_buildcmds(headerfile, {stl_headerunit = true})
    end
    -- opt.stl_headerunit = false
    for _, headerfile in ipairs(built_headerunits) do
        make_headerunit_buildcmds(headerfile)
    end
end

function generate_metadata(target, modules)
    profiler.enter(target:fullname(), "c++ modules", "builder", "generate module metadata")
    local public_modules
    for sourcefile, module in table.orderpairs(modules) do
        local fileconfig = target:fileconfig(sourcefile)
        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
        profiler.leave(target:fullname(), "c++ modules", "builder", "generate module metadata")
        return
    end

    local jobs = option.get("jobs") or os.default_njob()
    runjobs(target:fullname() .. "_install_modules", function(index, _, jobopt)
        progress.set_target(jobopt.progress, target)
        local module = public_modules[index]
        local metafilepath = support.get_metafile(target, module)
        progress.show(jobopt.progress, "${color.build.target}generating.module.metadata %s", module.name)
        local metadata = _generate_meta_module_info(target, module)
        json.savefile(metafilepath, metadata)
    end, {comax = jobs, total = #public_modules})
    profiler.leave(target:fullname(), "c++ modules", "builder", "generate module metadata")
end

-- check if dependencies changed
function is_dependencies_changed(target, module)
    profiler.enter(target:fullname(), "c++ modules", "builder", "check if dependency chain changed")
    local cachekey = target:fullname() .. (module.name or module.sourcefile)
    local requires = hashset.from(table.keys(module.deps or {}))
    local oldrequires = support.memcache():get2(cachekey, "oldrequires")
    local changed = false
    if oldrequires then
        if oldrequires ~= requires then
           changed = true
        else
           for required in requires:items() do
              if not oldrequires:has(required) then
                  changed = true
                  break
              end
           end
        end
    end
    profiler.leave(target:fullname(), "c++ modules", "builder", "check if dependency chain changed")
    return requires, changed
end

function show_progress(target, module, opt)
    local show = function(...)
        local batchcmds = opt and opt.batchcmds
        if batchcmds then
            batchcmds:show_progress(opt.progress, ...)
        else
            progress.show(opt.progress, ...)
        end
    end
    local suffix = opt.suffix or ""
    local cmd = opt.cmd or ""
    local dim = ""
    if option.get("verbose") then
        dim = "${dim}"
    end
    local header = "${clear}" .. dim
    if opt.headerunit then
        local name = module.method == "include-angle" and ("<" .. path.filename(module.name) .. ">") or module.name
        show(header .. "compiling.headerunit.$(mode) %s${clear}%s" .. suffix, name, cmd)
    elseif opt.bmi then
        if opt.objectfile then
            show(header .. "compiling.module.$(mode) %s${clear}%s" .. suffix, module.name, cmd)
        else
            show(header .. "compiling.module.bmi.$(mode) %s${clear}%s" .. suffix, module.name, cmd)
        end
    else
        show("compiling.$(mode) %s${clear}%s" .. suffix, module.sourcefile, cmd)
    end
end

function clean(target)
    -- we cannot use target:data("cxx.has_modules"),
    -- because on_config will be not called when cleaning targets
    if support.contains_modules(target) then
        remove_files(support.modules_cachedir(target, {interface = true}))
        remove_files(support.modules_cachedir(target, {interface = false}))
        remove_files(support.modules_cachedir(target, {headerunit = true}))
        if option.get("all") then
            support.localcache():clear()
            support.localcache():save()
        end
    end
end

function build_bmis(target, jobgraph, _, opt)
    opt = opt or {}
    if target:data("cxx.has_modules") then
        if target:is_moduleonly() and not target:data("cxx.modules.reused") or target:is_phony() then
            return
        end
        profiler.enter(target:fullname(), "c++ modules", "builder", "bmis")
        local modules = scanner.get_modules(target)
        -- avoid building non referenced modules
        local built_modules, built_headerunits, _ = scanner.sort_modules_by_dependencies(target, modules, {jobgraph = target:policy("build.jobgraph")})
        local headerunits, stlheaderunits = scanner.sort_headerunits(target, built_headerunits)
        if jobgraph.add_orders then -- jobgraph
            -- build headerunits
            if stlheaderunits or headerunits then
                build_headerunits_for_jobgraph(target, jobgraph, stlheaderunits, headerunits)
            end

            -- build modules
            build_modules_for_jobgraph(target, jobgraph, built_modules)
        elseif jobgraph.newjob then -- batchjobs (deprecated)
            opt.batchjobs = true
            -- build headerunits
            local headerunit_jobs = build_headerunits_for_batchjobs(target, jobgraph, stlheaderunits, headerunits, opt)

            -- build modules
            local module_jobs = build_modules_for_batchjobs(target, jobgraph, built_modules, opt)

            build_batchjobs_for_modules(table.join(headerunit_jobs or {}, module_jobs or {}), jobgraph, opt.rootjob)
        elseif jobgraph.runcmds then -- batchcmds
            opt.progress = opt.progress or 0
            -- build headerunits
            build_headerunits_for_batchcmds(target, jobgraph, stlheaderunits, headerunits, opt)

            local append_requires_flags = _builder(target).append_requires_flags
            if append_requires_flags then
                append_requires_flags(target, built_modules)
            end

            -- build modules
            build_modules_for_batchcmds(target, jobgraph, built_modules, opt)
        else
            assert(false, "shouldn't be here :D")
        end
        profiler.leave(target:fullname(), "c++ modules", "builder", "bmis")
    end
end

function build_objectfiles(target, jobgraph, _, opt)

    if target:data("cxx.has_modules") then
        if target:is_moduleonly() and not target:data("cxx.modules.reused") or target:is_phony() then
            return
        end
        profiler.enter(target:fullname(), "c++ modules", "builder", "objectfiles")
        local modules = scanner.get_modules(target)
        -- avoid building non referenced modules
        local built_modules, _, _ = scanner.sort_modules_by_dependencies(target, modules, {jobgraph = target:policy("build.jobgraph")})
        local append_requires_flags = _builder(target).append_requires_flags
        if append_requires_flags then
            append_requires_flags(target, built_modules)
        end

        if jobgraph.add_orders then -- jobgraph
            -- build modules objectfiles
            build_objectfiles_for_jobgraph(target, jobgraph, built_modules)
        elseif jobgraph.newjob then -- batchjobs (deprecated)
            opt.batchjobs = true
            -- build modules objectfiles
            local jobs = table.join(build_objectfiles_for_batchjobs(target, jobgraph, built_modules, opt) or {}, jobs or {})

            build_batchjobs_for_modules(jobs, jobgraph, opt.rootjob)
        elseif jobgraph.runcmds then -- batchcmds
            -- build modules objectfiles
            build_objectfiles_for_batchcmds(target, jobgraph, built_modules, opt)
        else
            assert(false, "shouldn't be here :D")
        end
        profiler.leave(target:fullname(), "c++ modules", "builder", "objectfiles")
    end
end
