note/专业积累/工具/xmake/xmake 源码.md
2023-07-05 09:34:06 +08:00

22 KiB

属性

scopeinfo

xmake.lua 生成属性信息,再根据类型生成

local _instance = scopeinfo
--[[
	kind : scopetype {root , target , rule , package , task}
	info : scopeconfig {come from xmake.lua}
	opt : unknown
]]
-- new an instance
function _instance.new(kind, info, opt)
    opt = opt or {}
    local instance = table.inherit(_instance)
    instance._KIND = kind or "root"
    instance._INFO = info
    instance._INTERPRETER = opt.interpreter
    instance._DEDUPLICATE = opt.deduplicate
    instance._ENABLE_FILTER = opt.enable_filter
    --print("scope", kind , info.__scriptdir , info.kind)
    return instance
end

Target

--[[
	name : scopename
	info : scopeinfo
]]
-- new a target instance
function _instance.new(name, info)
    local instance     = table.inherit(_instance)
    instance._NAME     = name
    instance._INFO     = info
    instance._CACHEID  = 1
    return instance
end

Rule


-- new a rule instance
function rule.new(name, info, opt)
    opt = opt or {}
    local instance = table.inherit(_instance)
    instance._NAME = name
    instance._INFO = info
    instance._PACKAGE = opt.package
    if opt.package then
        local deps = {}
        for _, depname in ipairs(table.wrap(instance:get("deps"))) do
            -- @xxx -> @package/xxx
            if depname:startswith("@") and not depname:find("/", 1, true) then
                depname = "@" .. opt.package:name() .. "/" .. depname:sub(2)
            end
            table.insert(deps, depname)
        end
        deps = table.unwrap(deps)
        if deps and #deps > 0 then
            instance:set("deps", deps)
        end
        for depname, extraconf in pairs(table.wrap(instance:extraconf("deps"))) do
            if depname:startswith("@") and not depname:find("/", 1, true) then
                depname = "@" .. opt.package:name() .. "/" .. depname:sub(2)
                instance:extraconf_set("deps", depname, extraconf)
            end
        end
    end
    return instance
end

Package

-- new an instance
function _instance.new(name, info, opt)
    opt = opt or {}
    local instance = table.inherit(_instance)
    instance._NAME      = name
    instance._INFO      = info
    instance._REPO      = opt.repo
    instance._SCRIPTDIR = opt.scriptdir and path.absolute(opt.scriptdir)
    return instance
end

Task

-- new a task instance
function task.new(name, info)
    local instance = table.inherit(task)
    instance._NAME = name
    instance._INFO = info
    return instance
end

事件

只触发一次

Project

  • targets
    • _load_targets
      • t:_load_before()
        • r:_load_before()
      • t:_load()
        • r:_load()
    • t:_load_after()
      • r:_load_after()
local _instance = Project
function project.targets()
    if project._memcache():get("targets", targets) then
    	return    
    end
    local targets, errors = project._load_targets()
    project._memcache():set("targets", targets)
    for _, t in ipairs(project.ordertargets()) do
        local ok, errors = t:_load_after()
    end
end
-- load targets
function project._load_targets()
    -- load all requires first and reload the project file to ensure has_package() works for targets
    local requires = project.required_packages()
    local ok, errors = project._load(true)

    -- load targets
    local results, errors = project._load_scope("target", true, true)

    -- make targets
    local targets = {}
    for targetname, targetinfo in pairs(results) do
        local t = target.new(targetname, targetinfo)
        if t and (t:get("enabled") == nil or t:get("enabled") == true) then
            targets[targetname] = t
        end
    end

    -- load and attach target deps, rules and packages
    for _, t in pairs(targets) do

        -- load rules from target and language
        t._RULES = t._RULES or {}
        local rulenames = {}
        for _, sourcefile in ipairs(table.wrap(t:get("files"))) do
            local extension = path.extension((sourcefile:gsub("|.*$", "")))
            local lang = language.load_ex(extension)
            table.join2(rulenames, lang:rules())
        end
        rulenames = table.unique(rulenames)
        for _, rulename in ipairs(rulenames) do
            local r = project.rule(rulename) or rule.rule(rulename)
            -- only add target rules
            if r:kind() == "target" then
                t._RULES[rulename] = r
                for _, deprule in ipairs(r:orderdeps()) do
                    t._RULES[deprule:name()] = deprule
                end
            end
            -- we need ignore `@package/rulename`, it will be loaded later
        end

        -- @note it's deprecated, please use on_load instead of before_load
        ok, errors = t:_load_before()

        -- we need call on_load() before building deps/rules,
        -- so we can use `target:add("deps", "xxx")` to add deps in on_load
        ok, errors = t:_load()
    end
    return targets
end

Target

  • _load_before
    • _load_rules
  • _load
    • _load_rules
    • on_load
  • _load_after
    • _load_rules
local _instance = Target
-- load rules  suffix = {before nil after}
function _instance:_load_rules(suffix)
    for k, r in ipairs(self:orderules()) do
        local ok, errors = self:_load_rule(r, suffix)
    end
    return true
end
-- do before_load for rules
-- @note it's deprecated, please use on_load instead of before_load
function _instance:_load_before()
    local ok, errors = self:_load_rules("before")
    return true
end
-- do load target and rules
function _instance:_load()

    -- do load with target rules
    local ok, errors = self:_load_rules()

    -- do load for target
    local on_load = self:script("load")
    ok, errors = sandbox.load(on_load, self)

    -- mark as loaded
    return true
end
-- do after_load target and rules
function _instance:_load_after()
    -- enter the environments of the target packages
    local oldenvs = os.addenvs(self:pkgenvs())

    -- do load for target
    local after_load = self:script("load_after")
    if after_load then
        local ok, errors = sandbox.load(after_load, self)
    end
    -- do after_load with target rules
    local ok, errors = self:_load_rules("after")

    -- leave the environments of the target packages
    os.setenvs(oldenvs)
    return true
end

Language

任务

Build

  • load

    • before_load

    • on_load

    • after_load

  • build

    • before_build

    • on_build

    • after_build

function main()
    -- lock the whole project
    project.lock()

    -- config it first
    task.run("config", {}, {disable_dump = true})

    -- enter project directory
    local oldir = os.cd(project.directory())

    -- clean up temporary files once a day
    cleaner.cleanup()

    -- do rules before building
    _do_project_rules("build_before")

    -- do build
    _do_build(targetname, group_pattern)

    -- dump cache stats
    if option.get("diagnosis") then
        build_cache.dump_stats()
    end
    -- do rules after building
    _do_project_rules("build_after")
    -- unlock the whole project
    project.unlock()
end

Project

  • install

    • before_install

    • on_install

    • after_install

  • load

    • before_load
    • on_load
    • after_load

-- make target info
function getinfo._make_targetinfo(mode, arch, target)

    -- init target info
    local targetinfo =
    {
        mode = mode
    ,   arch = arch
    ,   plat = config.get("plat")
    ,   vsarch = vsutils.vsarch(arch)
    ,   sdkver = config.get("vs_sdkver")
    }

    -- write only if not default
    -- use target:get("xxx") rather than target:xxx()

    -- save target kind
    targetinfo.kind          = target:kind()

    -- is default?
    targetinfo.default       = tostring(target:is_default())

    -- save target file
    targetinfo.basename      = vsutils.escape(target:basename())
    targetinfo.filename      = vsutils.escape(target:filename())

    -- save dirs
    targetinfo.targetdir     = _make_dirs(target:get("targetdir"))
    targetinfo.buildir       = _make_dirs(config.get("buildir"))
    targetinfo.rundir        = _make_dirs(target:get("rundir"))
    targetinfo.configdir     = _make_dirs(os.getenv("XMAKE_CONFIGDIR"))
    targetinfo.configfiledir = _make_dirs(target:get("configdir"))
    targetinfo.includedirs   = _make_dirs(table.join(_get_values_from_target(target, "includedirs") or {}, _get_values_from_target(target, "sysincludedirs")))
    targetinfo.linkdirs      = _make_dirs(_get_values_from_target(target, "linkdirs"))
    targetinfo.sourcedirs    = _make_dirs(_get_values_from_target(target, "values.project.vsxmake.sourcedirs"))
    targetinfo.pcheaderfile  = target:pcheaderfile("cxx") or target:pcheaderfile("c")

    -- save defines
    targetinfo.defines       = _make_arrs(_get_values_from_target(target, "defines"))

    -- save languages
    targetinfo.languages     = _make_arrs(_get_values_from_target(target, "languages"))
    if targetinfo.languages then
        -- fix c++17 to cxx17 for Xmake.props
        targetinfo.languages = targetinfo.languages:replace("c++", "cxx", {plain = true})
    end
    if target:is_phony() or target:is_headeronly() then
        return targetinfo
    end

    -- save subsystem
    local linkflags = linker.linkflags(target:kind(), target:sourcekinds(), {target = target})
    for _, linkflag in ipairs(linkflags) do
        if linkflag:lower():find("[%-/]subsystem:windows") then
            targetinfo.subsystem = "windows"
        end
    end
    if not targetinfo.subsystem then
        targetinfo.subsystem = "console"
    end

    -- save runenvs
    local runenvs = {}
    local addrunenvs, setrunenvs = make_runenvs(target)
    for k, v in table.orderpairs(target:pkgenvs()) do
        addrunenvs = addrunenvs or {}
        addrunenvs[k] = table.join(table.wrap(addrunenvs[k]), path.splitenv(v))
    end
    for _, dep in ipairs(target:orderdeps()) do
        for k, v in table.orderpairs(dep:pkgenvs()) do
            addrunenvs = addrunenvs or {}
            addrunenvs[k] = table.join(table.wrap(addrunenvs[k]), path.splitenv(v))
        end
    end
    for k, v in table.orderpairs(addrunenvs) do
        if k:upper() == "PATH" then
            runenvs[k] = _make_dirs(v) .. ";$([System.Environment]::GetEnvironmentVariable('" .. k .. "'))"
        else
            runenvs[k] = path.joinenv(v) .. ";$([System.Environment]::GetEnvironmentVariable('" .. k .."'))"
        end
    end
    for k, v in table.orderpairs(setrunenvs) do
        if #v == 1 then
            v = v[1]
            if path.is_absolute(v) and v:startswith(project.directory()) then
                runenvs[k] = _make_dirs(v)
            else
                runenvs[k] = v[1]
            end
        else
            runenvs[k] = path.joinenv(v)
        end
    end
    local runenvstr = {}
    for k, v in table.orderpairs(runenvs) do
        table.insert(runenvstr, k .. "=" .. v)
    end
    targetinfo.runenvs = table.concat(runenvstr, "\n")

    local runargs = target:get("runargs")
    if runargs then
        targetinfo.runargs = os.args(table.wrap(runargs))
    end

    -- use mfc? save the mfc runtime kind
    if target:rule("win.sdk.mfc.shared_app") or target:rule("win.sdk.mfc.shared") then
        targetinfo.mfckind = "Dynamic"
    elseif target:rule("win.sdk.mfc.static_app") or target:rule("win.sdk.mfc.static") then
        targetinfo.mfckind = "Static"
    end

    -- use cuda? save the cuda runtime version
    if target:rule("cuda") then
        local nvcc = find_tool("nvcc", { version = true })
        local ver = semver.new(nvcc.version)
        targetinfo.cudaver = ver:major() .. "." .. ver:minor()
    end
    return targetinfo
end
-- make vstudio project
function getinfo.main(outputdir, vsinfo)

    -- enter project directory
    local oldir = os.cd(project.directory())

    -- init solution directory
    vsinfo.solution_dir = path.absolute(path.join(outputdir, "vsxmake" .. vsinfo.vstudio_version))
    vsinfo.programdir = _make_dirs(xmake.programdir())
    vsinfo.projectdir = project.directory()
    vsinfo.sln_projectfile = path.relative(project.rootfile(), vsinfo.solution_dir)
    local projectfile = path.filename(project.rootfile())
    vsinfo.slnfile = project.name() or path.filename(project.directory())
    -- write only if not default

    vsinfo.xmake_info = format("xmake version %s", xmake.version())
    vsinfo.solution_id = hash.uuid4(project.directory() .. vsinfo.solution_dir)
    vsinfo.vs_version = vsinfo.project_version .. ".0"

    -- init modes
    vsinfo.modes = _make_vsinfo_modes()

    -- init archs
    vsinfo.archs = _make_vsinfo_archs()

    -- init groups
    local groups, group_deps = _make_vsinfo_groups()
    vsinfo.groups            = table.orderkeys(groups)
    vsinfo.group_deps        = table.orderkeys(group_deps)
    vsinfo._groups           = groups
    vsinfo._group_deps       = group_deps

    -- init config flags
    local flags = {}
    for k, v in table.orderpairs(localcache.get("config", "options")) do
        if k ~= "plat" and k ~= "mode" and k ~= "arch" and k ~= "clean" and k ~= "buildir" then
            table.insert(flags, "--" .. k .. "=" .. tostring(v))
        end
    end
    vsinfo.configflags = os.args(flags)

    -- load targets
    local targets = {}
    vsinfo._arch_modes = {}
    for _, mode in ipairs(vsinfo.modes) do
        vsinfo._arch_modes[mode] = {}
        for _, arch in ipairs(vsinfo.archs) do
            vsinfo._arch_modes[mode][arch] = { mode = mode, arch = arch }

            -- trace
            print("checking for %s.%s ...", mode, arch)

            -- reload config, project and platform
            -- modify config
            config.set("as", nil, {force = true}) -- force to re-check as for ml/ml64
            config.set("mode", mode, {readonly = true, force = true})
            config.set("arch", arch, {readonly = true, force = true})

            -- clear all options
            for _, opt in ipairs(project.options()) do
                opt:clear()
            end

            -- clear cache
            memcache.clear()
            
            -- check platform
            platform.load(config.plat(), arch):check()

            -- check project options
            project.check()

            -- install and update requires
            install_requires()

            -- load package rules for targets
            _load_package_rules_for_targets()

            -- config targets
            _config_targets()

            -- update config files
            generate_configfiles()
            generate_configheader()

            -- ensure to enter project directory
            os.cd(project.directory())

            -- save targets
            for targetname, target in table.orderpairs(project.targets()) do

                -- https://github.com/xmake-io/xmake/issues/2337
                target:data_set("plugin.project.kind", "vsxmake")

                -- make target with the given mode and arch
                targets[targetname] = targets[targetname] or {}
                local _target = targets[targetname]

                -- init target info
                _target.target = targetname
                _target.vcxprojdir = path.join(vsinfo.solution_dir, targetname)
                _target.target_id = hash.uuid4(targetname)
                _target.kind = target:kind()
                _target.absscriptdir = target:scriptdir()
                _target.scriptdir = path.relative(target:scriptdir(), _target.vcxprojdir)
                _target.projectdir = path.relative(project.directory(), _target.vcxprojdir)
                local targetdir = target:get("targetdir")
                if targetdir then _target.targetdir = path.relative(targetdir, _target.vcxprojdir) end
                _target._targets = _target._targets or {}
                _target._targets[mode] = _target._targets[mode] or {}
                local targetinfo = _make_targetinfo(mode, arch, target)
                _target._targets[mode][arch] = targetinfo
                _target.sdkver = targetinfo.sdkver
                _target.default = targetinfo.default

                -- save all sourcefiles and headerfiles
                _target.sourcefiles = table.unique(table.join(_target.sourcefiles or {}, (target:sourcefiles())))
                _target.headerfiles = table.unique(table.join(_target.headerfiles or {}, (target:headerfiles())))

                -- sort them to stabilize generation
                table.sort(_target.sourcefiles)
                table.sort(_target.headerfiles)

                -- save file groups
                _target.filegroups = target:get("filegroups")
                _target.filegroups_extraconf = target:extraconf("filegroups")

                -- save deps
                _target.deps = table.unique(table.join(_target.deps or {}, table.orderkeys(target:deps()), nil))
            end
        end
    end
    os.cd(oldir)
    for _, target in table.orderpairs(targets) do
        target._paths = {}
        local dirs = {}
        local projectdir = project.directory()
        local root = target.absscriptdir or projectdir
        target.sourcefiles = table.imap(target.sourcefiles, function(_, v) return path.relative(v, projectdir) end)
        target.headerfiles = table.imap(target.headerfiles, function(_, v) return path.relative(v, projectdir) end)
        for _, f in ipairs(table.join(target.sourcefiles, target.headerfiles)) do
            local dir = _make_filter(f, target, root)
            local escaped_f = vsutils.escape(f)
            target._paths[f] =
            {
                -- @see https://github.com/xmake-io/xmake/issues/2077
                path = path.is_absolute(escaped_f) and escaped_f or "$(XmakeProjectDir)\\" .. escaped_f,
                dir = vsutils.escape(dir)
            }
            while dir and dir ~= "." do
                if not dirs[dir] then
                    dirs[dir] =
                    {
                        dir = vsutils.escape(dir),
                        dir_id = hash.uuid4(dir)
                    }
                end
                dir = path.directory(dir) or "."
            end
        end
        target._dirs = dirs
        target.dirs = table.orderkeys(dirs)
        target._deps = {}
        for _, v in ipairs(target.deps) do
            target._deps[v] = targets[v]
        end
    end

    -- we need set startup project for default or binary target
    -- @see https://github.com/xmake-io/xmake/issues/1249
    local targetnames = {}
    for targetname, target in table.orderpairs(project.targets()) do
        if target:get("default") == true then
            table.insert(targetnames, 1, targetname)
        elseif target:is_binary() then
            local first_target = targetnames[1] and project.target(targetnames[1])
            if not first_target or first_target:is_default() then
                table.insert(targetnames, 1, targetname)
            else
                table.insert(targetnames, targetname)
            end
        else
            table.insert(targetnames, targetname)
        end
    end
    vsinfo.targets = targetnames
    vsinfo._targets = targets
    return vsinfo
end
-- make
function make(version)
    return function(outputdir)
        -- trace
        vprint("using project kind vs%d", version)

        -- get info and params
        local info = getinfo(outputdir, vsinfo(version))
        local paramsprovidersln = _buildparams(info)

        -- write solution file
        local sln = path.join(info.solution_dir, info.slnfile .. ".sln")
        _writefileifneeded(sln, render(template_sln, "#([A-Za-z0-9_,%.%*%(%)]+)#", "@([^@]+)@", paramsprovidersln))

        -- add solution custom file
        _trycp(template_props, info.solution_dir)
        _trycp(template_targets, info.solution_dir)

        for _, target in ipairs(info.targets) do
            local paramsprovidertarget = _buildparams(info, target, "<!-- nil -->")
            local proj_dir = info._targets[target].vcxprojdir

            -- write project file
            local proj = path.join(proj_dir, target .. ".vcxproj")
            _writefileifneeded(proj, render(template_vcx, "#([A-Za-z0-9_,%.%*%(%)]+)#", "@([^@]+)@", paramsprovidertarget))

            local projfil = path.join(proj_dir, target .. ".vcxproj.filters")
            _writefileifneeded(projfil, render(template_fil, "#([A-Za-z0-9_,%.%*%(%)]+)#", "@([^@]+)@", paramsprovidertarget))

            -- add project custom file
            _trycp(template_props, proj_dir)
            _trycp(template_targets, proj_dir)
            _trycp(template_items, proj_dir)
            _trycp(template_itemfil, proj_dir)
        end

        -- clear config and local cache
        _clear_cache()

        -- save plugin arguments for autoupdate
        _save_plugin_arguments()
    end
end
-- main
function main()

    -- in project generator?
    os.setenv("XMAKE_IN_PROJECT_GENERATOR", "true")

    -- config it first
    task.run("config")

    -- make project
    _make[option.get("kind")](option.get("outputdir"))

    -- trace
    cprint("${color.success}create ok!")
    os.setenv("XMAKE_IN_PROJECT_GENERATOR", nil)
end

库依赖

Package

-- register the required local package
function _register_required_package(instance, required_package)

    -- disable it if this package is missing
    if not instance:exists() then
        required_package:enable(false)
    else
        -- clear require info first
        required_package:clear()

        -- add packages info with all dependencies
        local envs = {}
        _register_required_package_base(instance, required_package)
        _register_required_package_libs(instance, required_package)
        _register_required_package_envs(instance, envs)
        for _, dep in ipairs(instance:librarydeps()) do
            if instance:is_library() then
                _register_required_package_libs(dep, required_package, true)
            end
        end
        for _, dep in ipairs(instance:orderdeps()) do
            if not dep:is_private() then
                _register_required_package_envs(dep, envs)
            end
        end
        if #table.keys(envs) > 0 then
            required_package:add({envs = envs})
        end

        -- enable this require info
        required_package:enable(true)
    end

    -- save this require info and flush the whole cache file
    required_package:save()
end