--!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
-- @file        target.lua
--

-- define module
local target    = target or {}
local _instance = _instance or {}

-- load modules
local bit             = require("base/bit")
local os              = require("base/os")
local path            = require("base/path")
local hash            = require("base/hash")
local utils           = require("base/utils")
local table           = require("base/table")
local baseoption      = require("base/option")
local hashset         = require("base/hashset")
local deprecated      = require("base/deprecated")
local select_script   = require("base/private/select_script")
local match_copyfiles = require("base/private/match_copyfiles")
local instance_deps   = require("base/private/instance_deps")
local is_cross        = require("base/private/is_cross")
local memcache        = require("cache/memcache")
local rule            = require("project/rule")
local option          = require("project/option")
local config          = require("project/config")
local policy          = require("project/policy")
local project_package = require("project/package")
local tool            = require("tool/tool")
local linker          = require("tool/linker")
local compiler        = require("tool/compiler")
local toolchain       = require("tool/toolchain")
local platform        = require("platform/platform")
local environment     = require("platform/environment")
local language        = require("language/language")
local sandbox         = require("sandbox/sandbox")
local sandbox_module  = require("sandbox/modules/import/core/sandbox/module")

-- new a target instance
function _instance.new(name, info)
    local instance     = table.inherit(_instance)
    instance._INFO     = info
    instance._CACHEID  = 1
    if name then
        instance:name_set(name)
    end
    return instance
end

-- get memcache
function _instance:memcache()
    local cache = self._MEMCACHE
    if not cache then
        cache = memcache.cache("core.project.target." .. tostring(self))
        self._MEMCACHE = cache
    end
    return cache
end

-- load rule, move cache to target
function _instance:_load_rule(ruleinst, suffix)

    -- init cache
    local key = ruleinst:fullname() .. (suffix and ("_" .. suffix) or "")
    local cache = self._RULES_LOADED or {}

    -- do load
    if cache[key] == nil then
        local on_load = ruleinst:script("load" .. (suffix and ("_" .. suffix) or ""))
        if on_load then
            local ok, errors = sandbox.load(on_load, self)
            cache[key] = {ok, errors}
        else
            cache[key] = {true}
        end

        -- before_load has been deprecated
        if on_load and suffix == "before" then
            deprecated.add(ruleinst:fullname() .. ".on_load", ruleinst:fullname() .. ".before_load")
        end
    end

    -- save cache
    self._RULES_LOADED = cache

    -- return results
    local results = cache[key]
    if results then
        return results[1], results[2]
    end
end

-- load rules
function _instance:_load_rules(suffix)
    for _, r in ipairs(self:orderules()) do
        local ok, errors = self:_load_rule(r, suffix)
        if not ok then
            return false, errors
        end
    end
    return true
end

-- do load target and rules
function _instance:_load()

    -- do load with target rules
    local ok, errors = self:_load_rules()
    if not ok then
        return false, errors
    end

    -- do load for target
    local on_load = self:script("load")
    if on_load then
        ok, errors = sandbox.load(on_load, self)
        if not ok then
            return false, errors
        end
    end

    -- mark as loaded
    self._LOADED = true
    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")
    if not ok then
        return false, errors
    end
    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)
        if not ok then
            return false, errors
        end
    end

    -- do after_load with target rules
    local ok, errors = self:_load_rules("after")
    if not ok then
        return false, errors
    end

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

-- get the visibility, private: 1, interface: 2, public: 3 = 1 | 2
function _instance:_visibility(opt)
    local visibility = 1
    if opt then
        if opt.interface then
            visibility = 2
        elseif opt.public then
            visibility = 3
        end
    end
    return visibility
end

-- update file rules
--
-- if we add files in on_load() dynamically, we need to update file rules,
-- otherwise it will cause: unknown source file: ...
--
function _instance:_update_filerules()
    local rulenames = {}
    local extensions = {}
    for _, sourcefile in ipairs(table.wrap(self:get("files"))) do
        local extension = path.extension((sourcefile:gsub("|.*$", "")))
        if not extensions[extension] then
            local sourcekind = self:extraconf("files", sourcefile, "sourcekind")
            local lang = sourcekind and language.load_sk(sourcekind) or language.load_ex(extension)
            if lang and lang:rules() then
                table.join2(rulenames, lang:rules())
            end
            extensions[extension] = true
        end
    end
    rulenames = table.unique(rulenames)
    for _, rulename in ipairs(rulenames) do
        local r = target._project() and target._project().rule(rulename, {namespace = self:namespace()}) or rule.rule(rulename)
        if r then
            -- only add target rules
            if r:kind() == "target" then
                if not self:rule(rulename) then
                    self:rule_add(r)
                    for _, deprule in ipairs(r:orderdeps()) do
                        if not self:rule(deprule:name()) then
                            self:rule_add(deprule)
                        end
                    end
                end
            end
        end
    end
end

-- invalidate the previous cache
function _instance:_invalidate(name)
    self._CACHEID = self._CACHEID + 1
    self._POLICIES = nil
    self:memcache():clear()
    -- we need to flush the source files cache if target/files are modified, e.g. `target:add("files", "xxx.c")`
    if name == "files" then
        self._SOURCEFILES = nil
        self._OBJECTFILES = nil
        self._SOURCEBATCHES = nil
        self:_update_filerules()
    elseif name == "deps" then
        self._DEPS = nil
        self._ORDERDEPS = nil
        self._INHERITDEPS = nil
    end
    if self._FILESCONFIG then
        self._FILESCONFIG[name] = nil
    end
end

-- build deps
function _instance:_build_deps()
    if target._project() then
        local instances   = target._project().targets()
        self._DEPS        = self._DEPS or {}
        self._ORDERDEPS   = self._ORDERDEPS or {}
        self._INHERITDEPS = self._INHERITDEPS or {}
        instance_deps.load_deps(self, instances, self._DEPS, self._ORDERDEPS, {self:fullname()})
        -- @see https://github.com/xmake-io/xmake/issues/4689
        instance_deps.load_deps(self, instances, {}, self._INHERITDEPS, {self:fullname()}, function (t, dep)
            local depinherit = t:extraconf("deps", dep:name(), "inherit")
            if depinherit == nil then
                depinherit = t:extraconf("deps", dep:fullname(), "inherit")
            end
            return depinherit == nil or depinherit
        end)
    end
end

-- is loaded?
function _instance:_is_loaded()
    return self._LOADED
end

-- get values from target deps with {interface|public = ...}
function _instance:_get_from_deps(name, result_values, result_sources, opt)
    local orderdeps = self:orderdeps({inherit = true})
    local total = #orderdeps
    for idx, _ in ipairs(orderdeps) do
        local dep = orderdeps[total + 1 - idx]
        -- We can inherit some configuration from dependencies.
        -- e.g. disable to inherit links, add_deps("foo", {links = false})
        -- @see https://github.com/xmake-io/xmake/issues/6925
        local inherit = self:extraconf("deps", dep:name(), name)
        if inherit ~= false then
            local values = dep:get(name, opt)
            if values ~= nil then
                table.insert(result_values, values)
                table.insert(result_sources, "dep::" .. dep:name())
            end
            local dep_values = {}
            local dep_sources = {}
            dep:_get_from_options(name, dep_values, dep_sources, opt)
            dep:_get_from_packages(name, dep_values, dep_sources, opt)
            for idx, values in ipairs(dep_values) do
                local dep_source = dep_sources[idx]
                table.insert(result_values, values)
                table.insert(result_sources, "dep::" .. dep:name() .. "/" .. dep_source)
            end
        end
    end
end

-- get values from target options with {interface|public = ...}
function _instance:_get_from_options(name, result_values, result_sources, opt)
    for _, opt_ in ipairs(self:orderopts(opt)) do
        local values = opt_:get(name)
        if values ~= nil then
            table.insert(result_values, values)
            table.insert(result_sources, "option::" .. opt_:name())
        end
    end
end

-- get values from target packages with {interface|public = ...}
function _instance:_get_from_packages(name, result_values, result_sources, opt)
    local function _filter_libfiles(libfiles)
        local result = {}
        for _, libfile in ipairs(table.wrap(libfiles)) do
            if not libfile:endswith(".dll") then
                table.insert(result, libfile)
            end
        end
        return table.unwrap(result)
    end
    for _, pkg in ipairs(self:orderpkgs(opt)) do
        local configinfo = self:pkgconfig(pkg:name())
        -- get values from package components
        -- e.g. `add_packages("sfml", {components = {"graphics", "window"}})`
        local selected_components = configinfo and configinfo.components or pkg:components_default()
        if selected_components and pkg:components() then
            local components_enabled = hashset.new()
            for _, comp in ipairs(table.wrap(selected_components)) do
                components_enabled:insert(comp)
                for _, dep in ipairs(table.wrap(pkg:component_orderdeps(comp))) do
                    components_enabled:insert(dep)
                end
            end
            components_enabled:insert("__base")
            -- if we can't find the values from the component, we need to fall back to __base to find them.
            -- it contains some common values of all components
            local values = {}
            local components = table.wrap(pkg:components())
            for _, component_name in ipairs(table.join(pkg:components_orderlist(), "__base")) do
                if components_enabled:has(component_name) then
                    local info = components[component_name]
                    if info then
                        local compvalues = info[name]
                        -- use full link path instead of links
                        -- @see https://github.com/xmake-io/xmake/issues/5066
                        if configinfo and configinfo.linkpath then
                            local libfiles = info.libfiles
                            if name == "links" then
                                if libfiles then
                                    compvalues = _filter_libfiles(libfiles)
                                end
                            elseif name == "linkdirs" then
                                if libfiles then
                                    compvalues = nil
                                end
                            end
                        end
                        table.join2(values, compvalues)
                    else
                        local components_str = table.concat(table.wrap(configinfo.components), ", ")
                        utils.warning("unknown component(%s) in add_packages(%s, {components = {%s}})", component_name, pkg:name(), components_str)
                    end
                end
            end
            if #values > 0 then
                table.insert(result_values, values)
                table.insert(result_sources, "package::" .. pkg:name())
            end
        -- get values instead of the builtin configs if exists extra package config
        -- e.g. `add_packages("xxx", {links = "xxx"})`
        elseif configinfo and configinfo[name] then
             local values = configinfo[name]
             if values ~= nil then
                table.insert(result_values, values)
                table.insert(result_sources, "package::" .. pkg:name())
            end
        else
            -- get values from the builtin package configs
            local values = pkg:get(name)
            -- use full link path instead of links
            -- @see https://github.com/xmake-io/xmake/issues/5066
            if configinfo and configinfo.linkpath then
                local libfiles = pkg:libraryfiles()
                if name == "links" then
                    if libfiles then
                        values = _filter_libfiles(libfiles)
                    end
                elseif name == "linkdirs" then
                    if libfiles then
                        values = nil
                    end
                end
            end
            if values ~= nil then
                table.insert(result_values, values)
                table.insert(result_sources, "package::" .. pkg:name())
            end
        end
    end
end

-- get values from the given source
function _instance:_get_from_source(name, source, result_values, result_sources, opt)
    if source == "self" then
        local values = self:get(name, opt)
        if values ~= nil then
            table.insert(result_values, values)
            table.insert(result_sources, "self")
        end
    elseif source:startswith("dep::") then
        local depname = source:split("::", {plain = true, limit = 2})[2]
        if depname == "*" then
            self:_get_from_deps(name, result_values, result_sources, opt)
        else
            local depsource
            local splitinfo = depname:split("/", {plain = true})
            if #splitinfo == 2 then
                depname = splitinfo[1]
                depsource = splitinfo[2]
            end
            local dep = self:dep(depname)
            if dep then
                -- e.g.
                -- dep::foo/option::bar
                -- dep::foo/package::bar
                if depsource then
                    local dep_values = {}
                    local dep_sources = {}
                    dep:_get_from_source(name, depsource, dep_values, dep_sources, opt)
                    for idx, values in ipairs(dep_values) do
                        local dep_source = dep_sources[idx]
                        table.insert(result_values, values)
                        table.insert(result_sources, "dep::" .. depname .. "/" .. dep_source)
                    end
                else
                    -- dep::foo
                    local values = dep:get(name, opt)
                    if values ~= nil then
                        table.insert(result_values, values)
                        table.insert(result_sources, source)
                    end
                end
            end
        end
    elseif source:startswith("option::") then
        local optname = source:split("::", {plain = true, limit = 2})[2]
        if optname == "*" then
            self:_get_from_options(name, result_values, result_sources, opt)
        else
            local opt_ = self:opt(optname, opt)
            if opt_ then
                local values = opt_:get(name)
                if values ~= nil then
                    table.insert(result_values, values)
                    table.insert(result_sources, source)
                end
            end
        end
    elseif source:startswith("package::") then
        local pkgname = source:split("::", {plain = true, limit = 2})[2]
        if pkgname == "*" then
            self:_get_from_packages(name, result_values, result_sources, opt)
        else
            local pkg = self:pkg(pkgname, opt)
            if pkg then
                local values = pkg:get(name)
                if values ~= nil then
                    table.insert(result_values, values)
                    table.insert(result_sources, source)
                end
            end
        end
    elseif source == "*" then
        self:_get_from_source(name, "self", result_values, result_sources, opt)
        self:_get_from_source(name, "option::*", result_values, result_sources, opt)
        self:_get_from_source(name, "package::*", result_values, result_sources, opt)
        self:_get_from_source(name, "dep::*", result_values, result_sources, {interface = true})
    else
        os.raise("target:get_from(): unknown source %s", source)
    end
end

-- get the checked target, it's only for target:check_xxx api.
--
-- we should not inherit links from deps/packages when checking snippets in on_config,
-- because the target deps has been not built.
--
-- @see https://github.com/xmake-io/xmake/issues/4491
--
function _instance:_checked_target()
    local checked_target = self._CHECKED_TARGET
    if checked_target == nil then
        checked_target = self:clone()
        -- we need update target:cachekey(), because target flags may be cached in builder
        checked_target:_invalidate()
        checked_target.get_from = function (target, name, sources, opt)
            if (name == "links" or name == "linkdirs") and sources == "*" then
                sources = "self"
            end
            return _instance.get_from(target, name, sources, opt)
        end
        self._CHECKED_TARGET = checked_target
    end
    return checked_target
end

-- get format
function _instance:_format(kind)
    local formats = self._FORMATS
    if not formats then
        for _, toolchain_inst in ipairs(self:toolchains()) do
            formats = toolchain_inst:formats()
            if formats then
                break
            end
        end
        self._FORMATS = formats
    end
    if formats then
        return formats[kind or self:kind()]
    end
end

-- clone target, @note we can just call it in after_load()
function _instance:clone()
    if not self:_is_loaded() then
        os.raise("please call target:clone() in after_load().", self:fullname())
    end
    local instance = target.new(self:fullname(), self._INFO:clone())
    if self._DEPS then
        instance._DEPS = table.clone(self._DEPS)
    end
    if self._ORDERDEPS then
        instance._ORDERDEPS = table.clone(self._ORDERDEPS)
    end
    if self._INHERITDEPS then
        instance._INHERITDEPS = table.clone(self._INHERITDEPS)
    end
    if self._RULES then
        instance._RULES = table.clone(self._RULES)
    end
    if self._ORDERULES then
        instance._ORDERULES = table.clone(self._ORDERULES)
    end
    if self._DATA then
        instance._DATA = table.clone(self._DATA)
    end
    if self._SOURCEFILES then
        instance._SOURCEFILES = table.clone(self._SOURCEFILES)
    end
    if self._OBJECTFILES then
        instance._OBJECTFILES = table.clone(self._OBJECTFILES)
    end
    if self._SOURCEBATCHES then
        instance._SOURCEBATCHES = table.clone(self._SOURCEBATCHES, 3)
    end
    instance._LOADED = self._LOADED
    instance._LOADED_AFTER = true
    return instance
end

-- get the target info
--
-- e.g.
--
-- default: get private
--  - target:get("cflags")
--  - target:get("cflags", {private = true})
--
-- get private and interface
--  - target:get("cflags", {public = true})
--
-- get interface
--  - target:get("cflags", {interface = true})
--
-- get raw reference of values
--  - target:get("cflags", {rawref = true})
--
function _instance:get(name, opt)

    -- get values
    local values = self._INFO:get(name)

    -- get thr required visibility
    local vs_private   = 1
    local vs_interface = 2
    local vs_public    = 3 -- all
    local vs_required  = self:_visibility(opt)

    -- get all values? (private and interface)
    if vs_required == vs_public or (opt and opt.rawref) then
        return values
    end

    -- get the extra configuration
    local extraconf = self:extraconf(name)
    if extraconf then
        -- filter values for public, private or interface if be not dictionary
        if not table.is_dictionary(values) then
            local results = {}
            for _, value in ipairs(table.wrap(values)) do
                -- we always call self:extraconf() to handle group value
                local extra = self:extraconf(name, value)
                local vs_conf = self:_visibility(extra)
                if bit.band(vs_required, vs_conf) ~= 0 then
                    table.insert(results, value)
                end
            end
            if #results > 0 then
                return table.unwrap(results)
            end
        else
            return values
        end
    else
        -- only get the private values
        if bit.band(vs_required, vs_private) ~= 0 then
            return values
        end
    end
end

-- deprecated: get values from target dependencies
function _instance:get_from_deps(name, opt)
    deprecated.add("target:get_from(%s, \"dep:*\")", "target:get_from_deps(%s)", name)
    local result = {}
    local values = self:get_from(name, "dep::*", opt)
    if values then
        for _, v in ipairs(values) do
            table.join2(result, v)
        end
    end
    return result
end

-- deprecated: get values from target options with {interface|public = ...}
function _instance:get_from_opts(name, opt)
    deprecated.add("target:get_from(%s, \"option::*\")", "target:get_from_opts(%s)", name)
    local result = {}
    local values = self:get_from(name, "option::*", opt)
    if values then
        for _, v in ipairs(values) do
            table.join2(result, v)
        end
    end
    return result
end

-- deprecated: get values from target packages with {interface|public = ...}
function _instance:get_from_pkgs(name, opt)
    deprecated.add("target:get_from(%s, \"package::*\")", "target:get_from_pkgs(%s)", name)
    local result = {}
    local values = self:get_from(name, "package::*", opt)
    if values then
        for _, v in ipairs(values) do
            table.join2(result, v)
        end
    end
    return result
end

-- get values from the given sources
--
-- e.g.
--
-- only from the current target:
--      target:get_from("links")
--      target:get_from("links", "self")
--
-- from the given dep:
--      target:get_from("links", "dep::foo")
--      target:get_from("links", "dep::foo", {interface = true})
--      target:get_from("links", "dep::*")
--
-- from the given option:
--      target:get_from("links", "option::foo")
--      target:get_from("links", "option::*")
--
-- from the given package:
--      target:get_from("links", "package::foo")
--      target:get_from("links", "package::*")
--
-- from the given dep/option, dep/package
--      target:get_from("links", "dep::foo/option::bar")
--      target:get_from("links", "dep::foo/option::*")
--      target:get_from("links", "dep::foo/package::bar")
--      target:get_from("links", "dep::foo/package::*")
--
-- from the multiple sources:
--      target:get_from("links", {"self", "option::foo", "dep::bar", "package::zoo"})
--      target:get_from("links", {"self", "option::*", "dep::*", "package::*"})
--
-- from all:
--      target:get_from("links", "*")
--
-- return:
--      local values, sources = target:get_from("links", "*")
--      for idx, value in ipairs(values) do
--          local source = sources[idx]
--      end
--
function _instance:get_from(name, sources, opt)
    local result_values = {}
    local result_sources = {}
    sources = sources or "self"
    for _, source in ipairs(table.wrap(sources)) do
        self:_get_from_source(name, source, result_values, result_sources, opt)
    end
    if #result_values > 0 then
        return result_values, result_sources
    end
end

-- set the value to the target info
function _instance:set(name, ...)
    self._INFO:apival_set(name, ...)
    self:_invalidate(name)
end

-- add the value to the target info
function _instance:add(name, ...)
    self._INFO:apival_add(name, ...)
    self:_invalidate(name)
end

-- remove the value to the target info (deprecated)
function _instance:del(name, ...)
    self._INFO:apival_del(name, ...)
    self:_invalidate(name)
end

-- remove the value to the target info
function _instance:remove(name, ...)
    self._INFO:apival_remove(name, ...)
    self:_invalidate(name)
end

-- get the extra configuration
function _instance:extraconf(name, item, key)
    return self._INFO:extraconf(name, item, key)
end

-- set the extra configuration
function _instance:extraconf_set(name, item, key, value)
    self._INFO:extraconf_set(name, item, key, value)
end

-- get the extra configuration from the given source
--
-- e.g.
--
-- only from the current target:
--      target:extraconf_from("links")
--      target:extraconf_from("links", "self")
--
-- from the given dep:
--      target:extraconf_from("links", "dep::foo")
--
-- from the given option:
--      target:extraconf_from("links", "option::foo")
--
-- from the given package:
--      target:extraconf_from("links", "package::foo")
--
-- from the given dep/option, dep/package
--      target:extraconf_from("links", "dep::foo/option::bar")
--      target:extraconf_from("links", "dep::foo/package::bar")
--
function _instance:extraconf_from(name, source)
    if name:find("::") then
        local tmp = name
        name = source
        source = tmp
        utils.warning("please use target:extraconf_from(%s, %s) intead of target:extraconf_from(%s, %s)", name, source, source, name)
    end
    source = source or "self"
    if source == "self" then
        return self:extraconf(name)
    elseif source:startswith("dep::") then
        local depname = source:split("::", {plain = true, limit = 2})[2]
        local depsource
        local splitinfo = depname:split("/", {plain = true})
        if #splitinfo == 2 then
            depname = splitinfo[1]
            depsource = splitinfo[2]
        end
        local dep = self:dep(depname)
        if dep then
            -- e.g.
            -- dep::foo/option::bar
            -- dep::foo/package::bar
            if depsource then
                return dep:extraconf_from(name, dep_source)
            else
                -- dep::foo
                return dep:extraconf(name)
            end
        end
    elseif source:startswith("option::") then
        local optname = source:split("::", {plain = true, limit = 2})[2]
        local opt_ = self:opt(optname, opt)
        if opt_ then
            return opt_:extraconf(name)
        end
    elseif source:startswith("package::") then
        local pkgname = source:split("::", {plain = true, limit = 2})[2]
        local pkg = self:pkg(pkgname, opt)
        if pkg then
            return pkg:extraconf(name)
        end
    else
        os.raise("target:extraconf_from(): unknown source %s", source)
    end
end

-- get configuration source information of the given api item
function _instance:sourceinfo(name, item)
    return self._INFO:sourceinfo(name, item)
end

-- get user private data
function _instance:data(name)
    return self._DATA and self._DATA[name]
end

-- set user private data
function _instance:data_set(name, data)
    self._DATA = self._DATA or {}
    self._DATA[name] = data
end

-- add user private data
function _instance:data_add(name, data)
    self._DATA = self._DATA or {}
    self._DATA[name] = table.unwrap(table.join(self._DATA[name] or {}, data))
end

-- get values
function _instance:values(name, sourcefile)

    -- get values from the source file first
    local values = {}
    if sourcefile then
        local fileconfig = self:fileconfig(sourcefile)
        if fileconfig then
            local filevalues = fileconfig.values
            if filevalues then
                -- we use '_' to simplify setting, for example:
                --
                -- add_files("xxx.mof", {values = {wdk_mof_header = "xxx.h"}})
                -- add_files("xxx.mof", {values = {["wdk.mof.header"] = "xxx.h"}})
                --
                table.join2(values, filevalues[name] or filevalues[name:gsub("%.", "_")])
            end
        end
    end

    -- get values from target
    table.join2(values, self:get("values." .. name))
    if #values > 0 then
        values = table.unwrap(values)
    else
        values = nil
    end
    return values
end

-- set values
function _instance:values_set(name, ...)
    self:set("values." .. name, ...)
end

-- add values
function _instance:values_add(name, ...)
    self:add("values." .. name, ...)
end

-- get the target info
function _instance:info()
    return self._INFO:info()
end

-- get the type: target
function _instance:type()
    return "target"
end

-- get the target name
function _instance:name()
    return self._NAME
end

-- set the target name
function _instance:name_set(name)
    local parts = name:split("::", {plain = true})
    self._NAME = parts[#parts]
    table.remove(parts)
    if #parts > 0 then
        self._NAMESPACE = table.concat(parts, "::")
    end
end

-- get the namespace
function _instance:namespace()
    return self._NAMESPACE
end

-- get the full name
function _instance:fullname()
    local namespace = self:namespace()
    return namespace and namespace .. "::" .. self:name() or self:name()
end

-- get the target kind
function _instance:kind()
    return self:get("kind") or "binary"
end

-- get the target kind (deprecated)
function _instance:targetkind()
    return self:kind()
end

-- get the platform of this target
function _instance:plat()
    return self:get("plat") or config.get("plat") or os.host()
end

-- get the architecture of this target
function _instance:arch()
    return self:get("arch") or config.get("arch") or os.arch()
end

-- the current target is belong to the given platforms?
function _instance:is_plat(...)
    local plat = self:plat()
    for _, v in ipairs(table.pack(...)) do
        if v and plat == v then
            return true
        end
    end
end

-- the current target is belong to the given architectures?
function _instance:is_arch(...)
    local arch = self:arch()
    for _, v in ipairs(table.pack(...)) do
        if v and arch:find("^" .. v:gsub("%-", "%%-") .. "$") then
            return true
        end
    end
end

-- is 64bits architecture?
function _instance:is_arch64()
    return self:is_arch(".+64.*")
end

-- get the platform instance
function _instance:platform()
    local platform_inst = self._PLATFORM
    if platform_inst == nil then
        platform_inst, errors = platform.load(self:plat(), self:arch())
        if not platform_inst then
            os.raise(errors)
        end
        self._PLATFORM = platform_inst
    end
    return platform_inst
end

-- get the cache key
function _instance:cachekey()
    return string.format("%s_%d", tostring(self), self._CACHEID)
end

-- get the target version
function _instance:version()
    local version = self:get("version")
    local version_build
    if version then
        version_build = self:extraconf("version", version, "build")
        if type(version_build) == "string" then
            version_build = os.date(version_build, os.time())
        end
    end
    return version, version_build
end

-- get the target soname
-- @see https://github.com/tboox/tbox/issues/214
--
-- set_version("1.0.1", {soname = "1.0"}) -> libfoo.so.1.0, libfoo.1.0.dylib
-- set_version("1.0.1", {soname = "1"}) -> libfoo.so.1, libfoo.1.dylib
-- set_version("1.0.1", {soname = true}) -> libfoo.so.1, libfoo.1.dylib
-- set_version("1.0.1", {soname = ""}) -> libfoo.so, libfoo.dylib
function _instance:soname()
    if not self:is_shared() then
        return
    end
    if self:is_plat("windows", "mingw", "cygwin", "msys") then
        return
    end
    local version = self:get("version")
    local version_soname
    if version then
        version_soname = self:extraconf("version", version, "soname")
        if version_soname == true then
            version_soname = version:split(".", {plain = true})[1]
        end
    end
    if not version_soname then
        return
    end
    local soname = self:filename()
    if type(version_soname) == "string" and #version_soname > 0 then
        local extension = path.extension(soname)
        if extension == ".dylib" then
            soname = path.basename(soname) .. "." .. version_soname .. extension
        else
            soname = soname .. "." .. version_soname
        end
    end
    return soname
end

-- get the target license
function _instance:license()
    return self:get("license")
end

-- get the target policy
function _instance:policy(name)
    local policies = self._POLICIES
    if not policies then
        policies = self:get("policy")
        self._POLICIES = policies
        if policies then
            local defined_policies = policy.policies()
            for name, _ in pairs(policies) do
                if not defined_policies[name] then
                    utils.warning("unknown policy(%s), please run `xmake l core.project.policy.policies` if you want to all policies", name)
                end
            end
        end
    end
    local value
    if policies then
        value = policies[name]
    end
    if value == nil and target._project() then
        value = target._project().policy(name)
    end
    return policy.check(name, value)
end

-- get the base name of target file
function _instance:basename()
    local filename = self:get("filename")
    if filename then
        return path.basename(filename)
    end
    return self:get("basename") or self:name()
end

-- get the target compiler
function _instance:compiler(sourcekind)
    if not sourcekind then
        os.raise("please pass sourcekind to the first argument of target:compiler(), e.g. cc, cxx, as")
    end
    local compilerinst = self:memcache():get("compiler_" .. sourcekind)
    if not compilerinst then
        local instance, errors = compiler.load(sourcekind, self)
        if not instance then
            os.raise(errors)
        end
        compilerinst = instance
        self:memcache():set("compiler_" .. sourcekind, compilerinst)
    end
    return compilerinst
end

-- get the target linker
function _instance:linker()
    local linkerinst = self:memcache():get("linker")
    if not linkerinst then
        local instance, errors = linker.load(self:kind(), self:sourcekinds(), self)
        if not instance then
            os.raise(errors)
        end
        linkerinst = instance
        self:memcache():set("linker", linkerinst)
    end
    return linkerinst
end

-- make linking command for this target
function _instance:linkcmd(objectfiles)
    return self:linker():linkcmd(objectfiles or self:objectfiles(), self:targetfile(), {target = self})
end

-- make linking arguments for this target
function _instance:linkargv(objectfiles)
    return self:linker():linkargv(objectfiles or self:objectfiles(), self:targetfile(), {target = self})
end

-- make link flags for the given target
function _instance:linkflags()
    return self:linker():linkflags({target = self})
end

-- get the given dependent target
function _instance:dep(name)
    local deps = self:deps()
    if deps then
        local dep = deps[name]
        if dep == nil then
            local namespace = self:namespace()
            if namespace then
                dep = deps[namespace .. "::" .. name]
            end
        end
        return dep
    end
end

-- get target deps
function _instance:deps()
    if not self:_is_loaded() then
        os.raise("please call target:deps() or target:dep() in after_load()!")
    end
    if self._DEPS == nil then
        self:_build_deps()
    end
    return self._DEPS
end

-- get target ordered deps
function _instance:orderdeps(opt)
    opt = opt or {}
    if not self:_is_loaded() then
        os.raise("please call target:orderdeps() in after_load()!")
    end
    if self._DEPS == nil then
        self:_build_deps()
    end
    return opt.inherit and self._INHERITDEPS or self._ORDERDEPS
end

-- get target rules
function _instance:rules()
    return self._RULES
end

-- get target ordered rules
function _instance:orderules()
    local rules = self._RULES
    local orderules = self._ORDERULES
    if orderules == nil and rules then
        orderules = instance_deps.sort(rules)
        self._ORDERULES = orderules
    end
    return orderules
end

-- get target rule from the given rule name
function _instance:rule(name)
    if self._RULES then
        local r = self._RULES[name]
        if r == nil and self:namespace() then
            r = self._RULES[self:namespace() .. "::" .. name]
        end
        return r
    end
end

-- add rule
--
-- @note If a rule has the same name as a built-in rule,
-- it will be replaced in the target:rules() and target:orderules(), but will be not replaced globally in the project.rules()
function _instance:rule_add(r)
    self._RULES = self._RULES or {}
    self._RULES[r:fullname()] = r
    self._ORDERULES = nil
end

-- enable or disable rule
function _instance:rule_enable(name, enabled)
    local ruleinst = self:rule(name)
    if ruleinst then
        self:data_set("__rule_enabled." .. name, enabled)
    else
        utils.warning("target(%s): rule(%s) not found", self:name(), name)
    end
end

-- the given rule is enabled or disabled?
function _instance:rule_is_enabled(name)
    local enabled = self:data("__rule_enabled." .. name)
    return enabled ~= false
end

-- is phony target?
function _instance:is_phony()
    local targetkind = self:kind()
    return not targetkind or targetkind == "phony"
end

-- is binary target?
function _instance:is_binary()
    return self:kind() == "binary"
end

-- is shared library target?
function _instance:is_shared()
    return self:kind() == "shared"
end

-- is static library target?
function _instance:is_static()
    return self:kind() == "static"
end

-- is object files target?
function _instance:is_object()
    return self:kind() == "object"
end

-- is headeronly target?
function _instance:is_headeronly()
    return self:kind() == "headeronly"
end

-- is moduleonly target?
function _instance:is_moduleonly()
    return self:kind() == "moduleonly"
end

-- is library target?
function _instance:is_library()
    return self:is_static() or self:is_shared() or self:is_headeronly() or self:is_moduleonly()
end

-- is default target?
function _instance:is_default()
    local default = self:get("default")
    return default == nil or default == true
end

-- is enabled?
function _instance:is_enabled()
    return self:get("enabled") ~= false
end

-- is rebuilt?
function _instance:is_rebuilt()
    return self:data("rebuilt")
end

-- is cross-compilation?
function _instance:is_cross()
    return is_cross(self:plat(), self:arch())
end

-- get the enabled option
function _instance:opt(name, opt)
    return self:opts(opt)[name]
end

-- get the enabled options
function _instance:opts(opt)
    opt = opt or {}
    local cachekey = "opts"
    if opt.public then
        cachekey = cachekey .. "_public"
    elseif opt.interface then
        cachekey = cachekey .. "_interface"
    end
    local opts = self:memcache():get(cachekey)
    if not opts then
        opts = {}
        for _, opt_ in ipairs(self:orderopts(opt)) do
            opts[opt_:name()] = opt_
        end
        self:memcache():set(cachekey, opts)
    end
    return opts
end

-- get the enabled ordered options with {public|interface = ...}
function _instance:orderopts(opt)
    opt = opt or {}
    local cachekey = "orderopts"
    if opt.public then
        cachekey = cachekey .. "_public"
    elseif opt.interface then
        cachekey = cachekey .. "_interface"
    end
    local orderopts = self:memcache():get(cachekey)
    if not orderopts then
        orderopts = {}
        for _, name in ipairs(table.wrap(self:get("options", opt))) do
            local opt_ = nil
            local enabled = config.get(name)
            if enabled == nil and self:namespace() then
                enabled = config.get(self:namespace() .. "::" .. name)
            end
            if enabled then
                opt_ = option.load(name, {namespace = self:namespace()})
            end
            if opt_ then
                table.insert(orderopts, opt_)
            end
        end
        self:memcache():set(cachekey, orderopts)
    end
    return orderopts
end

-- get the enabled package
function _instance:pkg(name, opt)
    return self:pkgs(opt)[name]
end

-- get the enabled packages
function _instance:pkgs(opt)
    opt = opt or {}
    local cachekey = "pkgs"
    if opt.public then
        cachekey = cachekey .. "_public"
    elseif opt.interface then
        cachekey = cachekey .. "_interface"
    end
    local packages = self:memcache():get(cachekey)
    if not packages then
        packages = {}
        for _, pkg in ipairs(self:orderpkgs(opt)) do
            packages[pkg:name()] = pkg
        end
        self:memcache():set(cachekey, packages)
    end
    return packages
end

-- get the required packages with {interface|public = ..}
function _instance:orderpkgs(opt)
    opt = opt or {}
    local cachekey = "orderpkgs"
    if opt.public then
        cachekey = cachekey .. "_public"
    elseif opt.interface then
        cachekey = cachekey .. "_interface"
    end
    local packages = self:memcache():get(cachekey)
    if not packages then
        packages = {}
        local requires = target._project().required_packages()
        if requires then
            for _, packagename in ipairs(table.wrap(self:get("packages", opt))) do
                local pkg = requires[packagename]
                -- attempt to get package with namespace
                if pkg == nil and packagename:find("::", 1, true) then
                    local parts = packagename:split("::", {plain = true})
                    local namespace_pkg = requires[parts[#parts]]
                    if namespace_pkg and namespace_pkg:namespace() then
                        local fullname = namespace_pkg:fullname()
                        if fullname:endswith(packagename) then
                            pkg = namespace_pkg
                        end
                    end
                end
                if pkg and pkg:enabled() then
                    table.insert(packages, pkg)
                end
            end
        end
        self:memcache():set(cachekey, packages)
    end
    return packages
end

-- get the environments of packages
function _instance:pkgenvs()
    local pkgenvs = self._PKGENVS
    if pkgenvs == nil then
        local pkgs = hashset.new()
        for _, pkgname in ipairs(table.wrap(self:get("packages"))) do
            local pkg = self:pkg(pkgname)
            if pkg then
                pkgs:insert(pkg)
            end
        end
        -- we can also get package envs from deps (public package)
        -- @see https://github.com/xmake-io/xmake/issues/2729
        for _, dep in ipairs(self:orderdeps()) do
            for _, pkgname in ipairs(table.wrap(dep:get("packages", {interface = true}))) do
                local pkg = dep:pkg(pkgname)
                if pkg then
                    pkgs:insert(pkg)
                end
            end
        end
        for _, pkg in pkgs:orderkeys() do
            local envs = pkg:envs()
            if envs then
                for name, values in table.orderpairs(envs) do
                    if type(values) == "table" then
                        values = path.joinenv(values)
                    end
                    pkgenvs = pkgenvs or {}
                    if pkgenvs[name] then
                        pkgenvs[name] = pkgenvs[name] .. path.envsep() .. values
                    else
                        pkgenvs[name] = values
                    end
                end
            end
        end
        self._PKGENVS = pkgenvs or false
    end
    return pkgenvs or nil
end

-- get the config info of the given package
function _instance:pkgconfig(pkgname)
    local extra_packages = self:extraconf("packages")
    if extra_packages then
        return extra_packages[pkgname]
    end
end

-- get the object files directory
function _instance:objectdir(opt)

    -- the object directory
    local objectdir = self:get("objectdir")
    if not objectdir then
        objectdir = path.join(config.builddir(), ".objs")
    end
    local namespace = self:namespace()
    if namespace then
        objectdir = path.join(objectdir, (namespace:replace("::", path.sep())), self:name())
    else
        objectdir = path.join(objectdir, self:name())
    end

    -- get root directory of target
    local intermediate_directory = self:policy("build.intermediate_directory")
    if (opt and opt.root) or intermediate_directory == false then
        return objectdir
    end

    -- generate intermediate directory
    local plat = self:plat()
    if plat then
        objectdir = path.join(objectdir, plat)
    end
    local arch = self:arch()
    if arch then
        objectdir = path.join(objectdir, arch)
    end
    local mode = config.mode()
    if mode then
        objectdir = path.join(objectdir, mode)
    end
    return objectdir
end

-- get the dependent files directory
function _instance:dependir(opt)

    -- init the dependent directory
    local dependir = self:get("dependir")
    if not dependir then
        dependir = path.join(config.builddir(), ".deps")
    end
    local namespace = self:namespace()
    if namespace then
        dependir = path.join(dependir, (namespace:replace("::", path.sep())), self:name())
    else
        dependir = path.join(dependir, self:name())
    end

    -- get root directory of target
    local intermediate_directory = self:policy("build.intermediate_directory")
    if (opt and opt.root) or intermediate_directory == false then
        return dependir
    end

    -- generate intermediate directory
    local plat = self:plat()
    if plat then
        dependir = path.join(dependir, plat)
    end
    local arch = self:arch()
    if arch then
        dependir = path.join(dependir, arch)
    end
    local mode = config.mode()
    if mode then
        dependir = path.join(dependir, mode)
    end
    return dependir
end

-- get the autogen files directory
function _instance:autogendir(opt)

    -- init the autogen directory
    local autogendir = self:get("autogendir")
    if not autogendir then
        autogendir = path.join(config.builddir(), ".gens")
    end
    local namespace = self:namespace()
    if namespace then
        autogendir = path.join(autogendir, (namespace:replace("::", path.sep())), self:name())
    else
        autogendir = path.join(autogendir, self:name())
    end

    -- get root directory of target
    local intermediate_directory = self:policy("build.intermediate_directory")
    if (opt and opt.root) or intermediate_directory == false then
        return autogendir
    end

    -- generate intermediate directory
    local plat = self:plat()
    if plat then
        autogendir = path.join(autogendir, plat)
    end
    local arch = self:arch()
    if arch then
        autogendir = path.join(autogendir, arch)
    end
    local mode = config.mode()
    if mode then
        autogendir = path.join(autogendir, mode)
    end
    return autogendir
end

-- get the autogen file path from the given source file path
function _instance:autogenfile(sourcefile, opt)

    -- get relative directory in the autogen directory
    local relativedir = nil
    local origindir  = path.directory(path.absolute(sourcefile))
    local autogendir = path.absolute(self:autogendir())
    if origindir:startswith(autogendir) then
        relativedir = path.join("gens", path.relative(origindir, autogendir))
    end

    -- get relative directory in the source directory
    if not relativedir then
        relativedir = path.directory(sourcefile)
    end

    -- translate path
    --
    -- e.g.
    --
    -- src/xxx.c
    --      project/xmake.lua
    --          build/.objs
    --          build/.gens
    --
    -- objectfile: project/build/.objs/xxxx/../../xxx.c will be out of range for objectdir
    -- autogenfile: project/build/.gens/xxxx/../../xxx.c will be out of range for autogendir
    --
    -- we need to replace '..' with '__' in this case
    --
    if path.is_absolute(relativedir) and os.host() == "windows" then
        -- remove C:\\ and whitespaces and fix long path issue
        -- e.g. C:\\Program Files (x64)\\xxx\Windows.h
        --
        -- @see
        -- https://github.com/xmake-io/xmake/issues/3021
        -- https://github.com/xmake-io/xmake/issues/3715
        relativedir = hash.strhash128(relativedir)
    end
    relativedir = relativedir:gsub("%.%.", "__")
    local rootdir = (opt and opt.rootdir) and opt.rootdir or self:autogendir()
    if relativedir ~= "." then
        rootdir = path.join(rootdir, relativedir)
    end
    return path.join(rootdir, (opt and opt.filename) and opt.filename or path.filename(sourcefile))
end

-- get the default target directory
function _instance:_default_targetdir()
    local targetdir = config.builddir()

    -- get root directory of target
    local intermediate_directory = self:policy("build.intermediate_directory")
    if intermediate_directory == false then
        return targetdir
    end

    -- generate intermediate directory
    local plat = self:plat()
    if plat then
        targetdir = path.join(targetdir, plat)
    end
    local arch = self:arch()
    if arch then
        targetdir = path.join(targetdir, arch)
    end
    local mode = config.mode()
    if mode then
        targetdir = path.join(targetdir, mode)
    end
    local namespace = self:namespace()
    if namespace then
        targetdir = path.join(targetdir, (namespace:replace("::", path.sep())))
    end
    return targetdir
end

-- get the target directory
function _instance:targetdir()
    local targetdir = self:get("targetdir")
    if not targetdir then
        return self:_default_targetdir()
    end

    -- we can use `set_targetdir("xxx", {bindir = "", libdir = ""})` to set sub-directory
    local subdir_kind
    if self:is_binary() or (self:is_shared() and self:is_plat("windows", "mingw")) then
        subdir_kind = "bindir"
    elseif self:is_static() or self:is_shared() then
        subdir_kind = "libdir"
    end
    return self:_artifactdir(subdir_kind)
end

-- get the build artifact output directory,
--
-- @param subdir_kind  the sub-directory kind, e.g. libdir, bindir, includedir
--
function _instance:_artifactdir(subdir_kind)
    local targetdir = self:get("targetdir")
    if not targetdir then
        return self:_default_targetdir()
    end

    if subdir_kind then
        local subdir = self:extraconf("targetdir", targetdir, subdir_kind)
        if subdir then
            return path.join(targetdir, subdir)
        end
    end
    return targetdir
end

-- get the extra build artifact file
--
-- supported artifact kinds:
--    1. implib: windows DLL/EXE implib(.lib, .dll.a)
--
-- otherwise returns nil
--
function _instance:artifactfile(kind)
    if kind == "implib" then
        if (self:is_shared() or self:is_binary()) and self:is_plat("windows", "mingw") then
            return path.join(self:_artifactdir("libdir"), path.basename(self:filename()) .. (self:is_plat("mingw") and ".dll.a" or ".lib"))
        end
    end
end

-- get the target file name
function _instance:filename()

    -- no target file?
    if self:is_object() or self:is_phony() or self:is_headeronly() or self:is_moduleonly() then
        return
    end

    -- make the target file name and attempt to use the format of linker first
    local targetkind = self:targetkind()
    local filename = self:get("filename")
    if not filename then
        local prefixname = self:get("prefixname")
        local suffixname = self:get("suffixname")
        local extension  = self:get("extension")
        filename = target.filename(self:basename(), targetkind, {
            format = self:_format(),
            plat = self:plat(), arch = self:arch(),
            prefixname = prefixname,
            suffixname = suffixname,
            extension = extension})
    end
    return filename
end

-- get the link name only for static/shared library
function _instance:linkname()
    if self:is_static() or self:is_shared() then
        local filename = self:get("filename")
        if filename then
            return target.linkname(filename)
        else
            local linkname = self:basename()
            local suffixname = self:get("suffixname")
            if suffixname then
                linkname = linkname .. suffixname
            end
            return linkname
        end
    end
end

-- get the target file
function _instance:targetfile()
    local filename = self:filename()
    if filename then
        return path.join(self:targetdir(), filename)
    end
end

-- get the symbol file
function _instance:symbolfile()

    -- the target directory
    local targetdir = self:targetdir()
    assert(targetdir and type(targetdir) == "string")

    -- the symbol file name
    local prefixname = self:get("prefixname")
    local suffixname = self:get("suffixname")
    local filename = target.filename(self:basename(), "symbol", {
        plat = self:plat(), arch = self:arch(),
        format = self:_format("symbol"),
        prefixname = prefixname,
        suffixname = suffixname})
    assert(filename)

    -- make the symbol file path
    return path.join(targetdir, filename)
end

-- get the script directory of xmake.lua
function _instance:scriptdir()
    return self:get("__scriptdir")
end

-- get configuration output directory
function _instance:configdir()
    return self:get("configdir") or config.builddir()
end

-- get run directory
function _instance:rundir()
    return baseoption.get("workdir") or self:get("rundir") or path.directory(self:targetfile())
end

-- get prefix directory
function _instance:prefixdir()
    return self:get("prefixdir")
end

-- get the installed binary directory
function _instance:bindir()
    local bindir = self:extraconf("prefixdir", self:prefixdir(), "bindir")
    if bindir == nil then
        bindir = "bin"
    end
    return self:installdir(bindir)
end

-- get the installed library directory
function _instance:libdir()
    local libdir = self:extraconf("prefixdir", self:prefixdir(), "libdir")
    if libdir == nil then
        libdir = "lib"
    end
    return self:installdir(libdir)
end

-- get the installed include directory
function _instance:includedir()
    local includedir = self:extraconf("prefixdir", self:prefixdir(), "includedir")
    if includedir == nil then
        includedir = "include"
    end
    return self:installdir(includedir)
end

-- get install directory
function _instance:installdir(...)
    opt = opt or {}
    local installdir = baseoption.get("installdir")
    if not installdir then
        -- DESTDIR: be compatible with https://www.gnu.org/prep/standards/html_node/DESTDIR.html
        installdir = self:get("installdir") or os.getenv("INSTALLDIR") or os.getenv("PREFIX") or os.getenv("DESTDIR") or platform.get("installdir")
        if installdir then
            installdir = installdir:trim()
        end
    end
    if installdir then
        local prefixdir = self:prefixdir()
        if prefixdir then
            installdir = path.join(installdir, prefixdir)
        end
        return path.normalize(path.join(installdir, ...))
    end
end

-- get package directory
function _instance:packagedir()
    -- get the output directory
    local outputdir   = baseoption.get("outputdir") or config.builddir()
    local packagename = self:name():lower()
    if #packagename > 1 and bit.band(packagename:byte(2), 0xc0) == 0x80 then
        utils.warning("package(%s): cannot generate package, becauese it contains unicode characters!", packagename)
        return
    end
    return path.join(outputdir, "packages", packagename:sub(1, 1), packagename)
end

-- get rules of the source file
function _instance:filerules(sourcefile)

    -- add rules from file config
    local rules = {}
    local override = false
    local fileconfig = self:fileconfig(sourcefile)
    if fileconfig then
        local filerules = fileconfig.rules or fileconfig.rule
        if filerules then
            override = filerules.override
            for _, rulename in ipairs(table.wrap(filerules)) do
                local r = target._project().rule(rulename, {namespace = self:namespace()}) or
                            rule.rule(rulename) or self:rule(rulename)
                if r then
                    table.insert(rules, r)
                end
            end
        end
    end
    -- override? e.g. add_files("src/*.c", {rules = {"xxx", override = true}})
    if override then
        return rules, true
    end

    -- load all rules for this target with sourcekinds and extensions
    local key2rules = self:memcache():get("key2rules")
    if not key2rules then
        key2rules = {}
        for _, r in pairs(table.wrap(self:rules())) do
            -- we can also get sourcekinds from add_rules("xxx", {sourcekinds = "cxx"})
            local rule_sourcekinds = self:extraconf("rules", r:name(), "sourcekinds") or r:get("sourcekinds")
            for _, sourcekind in ipairs(table.wrap(rule_sourcekinds)) do
                key2rules[sourcekind] = key2rules[sourcekind] or {}
                table.insert(key2rules[sourcekind], r)
            end
            -- we can also get extensions from add_rules("xxx", {extensions = ".cpp"})
            local rule_extensions = self:extraconf("rules", r:name(), "extensions") or r:get("extensions")
            for _, extension in ipairs(table.wrap(rule_extensions)) do
                extension = extension:lower()
                key2rules[extension] = key2rules[extension] or {}
                table.insert(key2rules[extension], r)
            end
        end
        self:memcache():set("key2rules", key2rules)
    end

    -- get target rules from the given sourcekind or extension
    --
    -- @note we prefer to use rules with extension because we need to be able to
    -- override the language code rules set by set_sourcekinds
    --
    -- e.g. set_extensions(".bpf.c") will override c++ rules
    --
    local rules_override = {}
    local filename = path.filename(sourcefile):lower()
    for _, r in ipairs(table.wrap(key2rules[path.extension(filename, 2)] or
                                  key2rules[path.extension(filename)] or
                                  key2rules[self:sourcekind_of(filename)] or
                                  key2rules[fileconfig and fileconfig.sourcekind])) do -- add_files("*.nasm", {sourcekind = "asm"})
        if self:extraconf("rules", r:name(), "override") then
            table.insert(rules_override, r)
        else
            table.insert(rules, r)
        end
    end

    -- we will use overrided rules first, e.g. add_rules("xxx", {override = true})
    return #rules_override > 0 and rules_override or rules
end

-- get the config info of the given source file
function _instance:fileconfig(sourcefile, opt)
    opt = opt or {}
    local filetype = opt.filetype or "files"

    -- get configs from user, e.g. target:fileconfig_set/add
    -- it has contained all original configs
    if self._FILESCONFIG_USER then
        local filesconfig = self._FILESCONFIG_USER[filetype]
        if filesconfig and filesconfig[sourcefile] then
            return filesconfig[sourcefile]
        end
    end

    -- get orignal configs from `add_xxxfiles()`
    self._FILESCONFIG = self._FILESCONFIG or {}
    local filesconfig = self._FILESCONFIG[filetype]
    if not filesconfig then
        filesconfig = {}
        for filepath, fileconfig in pairs(table.wrap(self:extraconf(filetype))) do
            local results = os.match(filepath)
            if #results > 0 then
                for _, file in ipairs(results) do
                    if path.is_absolute(file) then
                        file = path.relative(file, os.projectdir())
                    end
                    filesconfig[file] = fileconfig
                end
            else
                -- we also need support always_added, @see https://github.com/xmake-io/xmake/issues/1634
                if fileconfig.always_added then
                    filesconfig[filepath] = fileconfig
                end
            end
        end
        self._FILESCONFIG[filetype] = filesconfig
    end
    return filesconfig[sourcefile]
end

-- set the config info to the given source file
function _instance:fileconfig_set(sourcefile, info, opt)
    opt = opt or {}
    self._FILESCONFIG_USER = self._FILESCONFIG_USER or {}
    local filetype = opt.filetype or "files"
    local filesconfig = self._FILESCONFIG_USER[filetype]
    if not filesconfig then
        filesconfig = {}
        self._FILESCONFIG_USER[filetype] = filesconfig
    end
    filesconfig[sourcefile] = info
end

-- add the config info to the given source file
function _instance:fileconfig_add(sourcefile, info, opt)
    opt = opt or {}
    self._FILESCONFIG_USER = self._FILESCONFIG_USER or {}
    local filetype = opt.filetype or "files"
    local filesconfig = self._FILESCONFIG_USER[filetype]
    if not filesconfig then
        filesconfig = {}
        self._FILESCONFIG_USER[filetype] = filesconfig
    end

    -- we fetch orignal configs first if no user configs
    local fileconfig = filesconfig[sourcefile]
    if not fileconfig then
        fileconfig = table.clone(self:fileconfig(sourcefile, opt))
        filesconfig[sourcefile] = fileconfig
    end
    if fileconfig then
        for k, v in pairs(info) do
            if k == "force" then
                -- fileconfig_add("xxx.c", {force = {cxxflags = ""}})
                local force = fileconfig[k] or {}
                for k2, v2 in pairs(v) do
                    if force[k2] then
                        force[k2] = table.join(force[k2], v2)
                    else
                        force[k2] = v2
                    end
                end
                fileconfig[k] = force
            else
                -- fileconfig_add("xxx.c", {cxxflags = ""})
                if fileconfig[k] then
                    fileconfig[k] = table.join(fileconfig[k], v)
                else
                    fileconfig[k] = v
                end
            end
        end
    else
        filesconfig[sourcefile] = info
    end
end

-- get the source files
function _instance:sourcefiles()

    -- cached? return it directly
    if self._SOURCEFILES then
        return self._SOURCEFILES, false
    end

    -- get files
    local files = self:get("files")
    if not files then
        return {}, false
    end

    -- match files
    local i = 1
    local count = 0
    local sourcefiles = {}
    local sourcefiles_removed = {}
    local sourcefiles_inserted = {}
    local removed_count = 0
    local targetcache = memcache.cache("core.project.target")
    for _, file in ipairs(table.wrap(files)) do

        -- mark as removed files?
        local removed = false
        local prefix = "__remove_"
        if file:startswith(prefix) then
            file = file:sub(#prefix + 1)
            removed = true
        end

        local results
        if removed then
            results = {file}
        else
            -- find source files and try to cache the matching results of os.match across targets
            -- @see https://github.com/xmake-io/xmake/issues/1353
            results = targetcache:get2("sourcefiles", file)
            if results == nil then
                results = os.files(file)
                if #results == 0 then
                    -- attempt to find source directories if maybe compile it as directory with the custom rules
                    if #self:filerules(file) > 0 then
                        results = os.dirs(file)
                    end
                end

                -- Even if the current source file does not exist yet, we always add it.
                -- This is usually used for some rules that automatically generate code files,
                -- because they ensure that the code files have been generated before compilation.
                --
                -- @see https://github.com/xmake-io/xmake/issues/1540
                --
                -- e.g. add_files("src/test.c", {always_added = true})
                --
                if #results == 0 and self:extraconf("files", file, "always_added") then
                    results = {file}
                end
            end
            targetcache:set2("sourcefiles", file, results)
        end
        if #results == 0 then
            local sourceinfo = self:sourceinfo("files", file) or {}
            utils.warning("%s:%d${clear}: cannot match %s_files(\"%s\") in %s(%s)",
                sourceinfo.file or "", sourceinfo.line or -1, (removed and "remove" or "add"), file, self:type(), self:fullname())
        end

        -- process source files
        for _, sourcefile in ipairs(results) do

            -- convert to the relative path
            if path.is_absolute(sourcefile) then
                sourcefile = path.relative(sourcefile, os.projectdir())
            end

            -- add or remove it
            if removed then
                removed_count = removed_count + 1
                table.insert(sourcefiles_removed, sourcefile)
            elseif not sourcefiles_inserted[sourcefile] then
                table.insert(sourcefiles, sourcefile)
                sourcefiles_inserted[sourcefile] = true
            end
        end
    end

    -- remove all source files which need be removed
    if removed_count > 0 then
        table.remove_if(sourcefiles, function (i, sourcefile)
            for _, removed_file in ipairs(sourcefiles_removed) do
                local pattern = path.translate((removed_file:gsub("|.*$", "")))
                if pattern:sub(1, 2):find('%.[/\\]') then
                    pattern = pattern:sub(3)
                end
                pattern = path.pattern(pattern)
                -- we need to match whole pattern, https://github.com/xmake-io/xmake/issues/3523
                if sourcefile:match("^" .. pattern .. "$") then
                    return true
                end
            end
        end)
    end
    self._SOURCEFILES = sourcefiles

    -- ok and sourcefiles are modified
    return sourcefiles, true
end

-- get object file from source file
function _instance:objectfile(sourcefile)
    return self:autogenfile(sourcefile, {rootdir = self:objectdir(),
        filename = target.filename(path.filename(sourcefile), "object", {
            plat = self:plat(),
            arch = self:arch(),
            format = self:_format("object")})})
end

-- get the object files
function _instance:objectfiles()

    -- get source batches
    local sourcebatches, modified = self:sourcebatches()

    -- cached? return it directly
    if self._OBJECTFILES and not modified then
        return self._OBJECTFILES
    end

    -- get object files from source batches
    local objectfiles = {}
    local batchcount = 0
    local orderkeys = table.keys(sourcebatches)
    table.sort(orderkeys) -- @note we need to guarantee the order of objectfiles for depend.is_changed() and etc.
    for _, k in ipairs(orderkeys) do
        local sourcebatch = sourcebatches[k]
        table.join2(objectfiles, sourcebatch.objectfiles)
        batchcount = batchcount + 1
    end

    -- some object files may be repeat and appear link errors if multi-batches exists, so we need to remove all repeat object files
    -- e.g. add_files("src/*.c", {rules = {"rule1", "rule2"}})
    local deduplicate = batchcount > 1

    -- get object files from all dependent targets (object kind)
    -- @note we only merge objects in plain deps, e.g. binary -> (static -> object, object ...)
    local plaindeps = self:get("deps")
    if plaindeps and (self:is_binary() or self:is_shared() or self:is_static()) then
        local function _get_all_objectfiles_of_object_dep (t)
            local _objectfiles = {}
            table.join2(_objectfiles, t:objectfiles())
            local _plaindeps = t:get("deps")
            if _plaindeps then
                for _, depname in ipairs(table.wrap(_plaindeps)) do
                    local dep = t:dep(depname)
                    if dep and dep:is_object() then
                        table.join2(_objectfiles, _get_all_objectfiles_of_object_dep(dep))
                    end
                end
            end
            return _objectfiles
        end
        for _, depname in ipairs(table.wrap(plaindeps)) do
            local dep = self:dep(depname)
            if dep and dep:is_object() then
                table.join2(objectfiles, _get_all_objectfiles_of_object_dep(dep))
                deduplicate = true
            end
        end
    end

    -- remove repeat object files
    if deduplicate then
        objectfiles = table.unique(objectfiles)
    end

    -- cache it
    self._OBJECTFILES = objectfiles
    return objectfiles
end

-- get the header files
function _instance:headerfiles(outputdir, opt)
    opt = opt or {}
    local headerfiles = self:get("headerfiles", opt) or {}
    -- add_headerfiles("src/*.h", {install = false})
    -- @see https://github.com/xmake-io/xmake/issues/2577
    if opt.installonly then
       local installfiles = {}
       for _, headerfile in ipairs(table.wrap(headerfiles)) do
           if self:extraconf("headerfiles", headerfile, "install") ~= false then
               table.insert(installfiles, headerfile)
           end
       end
       headerfiles = installfiles
    end
    if not headerfiles then
        return
    end

    if not outputdir then
        if self:includedir() then
            outputdir = self:includedir()
        end
    end
    return match_copyfiles(self, "headerfiles", outputdir, {copyfiles = headerfiles})
end

-- get the configuration files
function _instance:configfiles(outputdir)
    return match_copyfiles(self, "configfiles", outputdir or self:configdir(), {pathfilter = function (dstpath, fileinfo)
            if dstpath:endswith(".in") then
                dstpath = dstpath:sub(1, -4)
            end
            return dstpath
        end})
end

-- get the install files
function _instance:installfiles(outputdir, opt)
    local installfiles = self:get("installfiles", opt) or {}
    return match_copyfiles(self, "installfiles", outputdir or self:installdir(), {copyfiles = installfiles})
end

-- get the extra files
function _instance:extrafiles(outputdir)
    return match_copyfiles(self, "extrafiles", outputdir)
end

-- get depend file from object file
function _instance:dependfile(objectfile)

    -- get the dependent original file and directory, @note relative to the root directory
    local originfile = path.absolute(objectfile and objectfile or self:targetfile())
    local origindir  = path.directory(originfile)

    -- get relative directory in the object directory
    local relativedir = nil
    local objectdir = path.absolute(self:objectdir())
    if origindir:startswith(objectdir) then
        relativedir = path.relative(origindir, objectdir)
    end

    -- get relative directory in the target directory
    if not relativedir then
        local targetdir = path.absolute(self:targetdir())
        if origindir:startswith(targetdir) then
            relativedir = path.relative(origindir, targetdir)
        end
    end

    -- get relative directory in the autogen directory
    if not relativedir then
        local autogendir = path.absolute(self:autogendir())
        if origindir:startswith(autogendir) then
            relativedir = path.join("gens", path.relative(origindir, autogendir))
        end
    end

    -- get relative directory in the build directory
    if not relativedir then
        local builddir = path.absolute(config.builddir())
        if origindir:startswith(builddir) then
            relativedir = path.join("build", path.relative(origindir, builddir))
        end
    end

    -- get relative directory in the project directory
    if not relativedir then
        local projectdir = os.projectdir()
        if origindir:startswith(projectdir) then
            relativedir = path.relative(origindir, projectdir)
        end
    end

    -- get the relative directory from the origin file
    if not relativedir then
        relativedir = origindir
    end
    if path.is_absolute(relativedir) and os.host() == "windows" then
        -- remove C:\\ and whitespaces and fix long path issue
        -- e.g. C:\\Program Files (x64)\\xxx\Windows.h
        --
        -- @see
        -- https://github.com/xmake-io/xmake/issues/3021
        -- https://github.com/xmake-io/xmake/issues/3715
        relativedir = hash.strhash128(relativedir)
    end

    -- originfile: project/build/.objs/xxxx/../../xxx.c will be out of range for objectdir
    --
    -- we need to replace '..' to '__' in this case
    --
    relativedir = relativedir:gsub("%.%.", "__")

    -- make dependent file
    -- full file name(not base) to avoid name-clash of original file
    return path.join(self:dependir(), relativedir, path.filename(originfile) .. ".d")
end

-- get the dependent include files
function _instance:dependfiles()
    local sourcebatches, modified = self:sourcebatches()
    if self._DEPENDFILES and not modified then
        return self._DEPENDFILES
    end
    local dependfiles = {}
    for _, sourcebatch in pairs(self:sourcebatches()) do
        table.join2(dependfiles, sourcebatch.dependfiles)
    end
    self._DEPENDFILES = dependfiles
    return dependfiles
end

-- get the sourcekind for the given source file
function _instance:sourcekind_of(sourcefile)

    -- get the sourcekind of this source file
    local sourcekind = language.sourcekind_of(sourcefile)
    local fileconfig = self:fileconfig(sourcefile)
    if fileconfig and fileconfig.sourcekind then
        -- we can override the sourcekind, e.g. add_files("*.c", {sourcekind = "cxx"})
        sourcekind = fileconfig.sourcekind
    end
    return sourcekind
end

-- get the kinds of sourcefiles
--
-- e.g. cc cxx mm mxx as ...
--
function _instance:sourcekinds()
    local sourcekinds = self._SOURCEKINDS
    if not sourcekinds then
        sourcekinds = {}
        local sourcebatches = self:sourcebatches()
        for _, sourcebatch in table.orderpairs(sourcebatches) do
            local sourcekind = sourcebatch.sourcekind
            if sourcekind then
                table.insert(sourcekinds, sourcekind)
            end
        end
        -- if the source file is added dynamically, we may not be able to get the sourcekinds,
        -- so we can only continue to get it from the rule
        -- https://github.com/xmake-io/xmake/issues/1622#issuecomment-927726697
        for _, ruleinst in ipairs(self:orderules()) do
            local rule_sourcekinds = ruleinst:get("sourcekinds")
            if rule_sourcekinds then
                table.insert(sourcekinds, rule_sourcekinds)
            end
        end
        sourcekinds = table.unique(sourcekinds)
        self._SOURCEKINDS = sourcekinds
    end
    return sourcekinds
end

-- get source count
function _instance:sourcecount()
    return #self:sourcefiles()
end

-- get source batches
function _instance:sourcebatches()

    -- get source files
    local sourcefiles, modified = self:sourcefiles()

    -- cached? return it directly
    if self._SOURCEBATCHES and not modified then
        return self._SOURCEBATCHES, false
    end

    -- make source batches for each source kinds
    local sourcebatches = {}
    for _, sourcefile in ipairs(sourcefiles) do

        -- get file rules
        local filerules, override = self:filerules(sourcefile)
        if #filerules == 0 then
            os.raise("unknown source file: %s", sourcefile)
        end

        -- add source batch for the file rules
        for _, filerule in ipairs(filerules) do

            -- get rule name
            local rulename = filerule:name()

            -- make this batch
            local sourcebatch = sourcebatches[rulename] or {sourcefiles = {}}
            sourcebatches[rulename] = sourcebatch

            -- save the rule name
            sourcebatch.rulename = rulename

            -- add source file to this batch
            table.insert(sourcebatch.sourcefiles, sourcefile)

            -- attempt to get source kind from the builtin languages
            local sourcekind = self:sourcekind_of(sourcefile)
            if sourcekind and filerule:get("sourcekinds") and not override then

                -- save source kind
                sourcebatch.sourcekind = sourcekind

                -- insert object files to source batches
                -- and we need to avoid duplication with object files, which may cause some conflicts.
                -- e.g. c++.build, c++ module and unity_build rules
                -- @see https://github.com/xmake-io/xmake/issues/6420
                if filerule:extraconf("sourcekinds", sourcekind, "objectfiles") ~= false then
                    sourcebatch.objectfiles = sourcebatch.objectfiles or {}
                    sourcebatch.dependfiles = sourcebatch.dependfiles or {}
                    local objectfile = self:objectfile(sourcefile)
                    table.insert(sourcebatch.objectfiles, objectfile)
                    table.insert(sourcebatch.dependfiles, self:dependfile(objectfile))
                end
            end
        end
    end
    self._SOURCEBATCHES = sourcebatches
    return sourcebatches, modified
end

-- get xxx_script
function _instance:script(name, generic)

    -- get script
    local script = self:get(name)
    local result = select_script(script, {plat = self:plat(), arch = self:arch()}) or generic

    -- imports some modules first
    if result and result ~= generic then
        local scope = getfenv(result)
        if scope then
            for _, modulename in ipairs(table.wrap(self:get("imports"))) do
                scope[sandbox_module.name(modulename)] = sandbox_module.import(modulename, {anonymous = true})
            end
        end
    end
    return result
end

-- get the precompiled header file (xxx.[h|hpp|inl])
--
-- @param langkind  c/cxx
--
function _instance:pcheaderfile(langkind)
    local pcheaderfile = self:get("p" .. langkind .. "header")
    if table.empty(pcheaderfile) then
        pcheaderfile = nil
    end
    return pcheaderfile
end

-- set the precompiled header file
function _instance:pcheaderfile_set(langkind, headerfile)
    self:set("p" .. langkind .. "header", headerfile)
    self._PCOUTPUTFILES = nil
end

-- get the output of precompiled header file (xxx.h.pch)
--
-- @param langkind  c/cxx
--
function _instance:pcoutputfile(langkind)
    self._PCOUTPUTFILES = self._PCOUTPUTFILES or {}
    local pcoutputfile = self._PCOUTPUTFILES[langkind]
    if pcoutputfile then
        return pcoutputfile
    end

    -- get the precompiled header file in the object directory
    local pcheaderfile = self:pcheaderfile(langkind)
    if pcheaderfile then
        local is_gcc = false
        local is_msvc = false
        local sourcekinds = {c = "cc", cxx = "cxx", m = "mm", mxx = "mxx"}
        local sourcekind = assert(sourcekinds[langkind], "unknown language kind: " .. langkind)
        local _, toolname = self:tool(sourcekind)
        if toolname then
            if toolname == "gcc" or toolname == "gxx" then
                is_gcc = true
            elseif toolname == "cl" then
                is_msvc = true
            end
        end

        -- make precompiled output file
        --
        -- @note gcc has not -include-pch option to set the pch file path
        --
        pcoutputfile = self:objectfile(pcheaderfile)
        local pcoutputfilename = path.basename(pcoutputfile)
        if is_gcc then
            pcoutputfilename = pcoutputfilename .. ".gch"
        else
            -- different vs versions of pch files are not backward compatible,
            -- so we need to distinguish between them.
            --
            -- @see https://github.com/xmake-io/xmake/issues/5413
            local msvc = self:toolchain("msvc")
            if is_msvc and msvc then
                local vs_toolset = msvc:config("vs_toolset")
                if vs_toolset then
                    vs_toolset = sandbox_module.import("private.utils.toolchain", {anonymous = true}).get_vs_toolset_ver(vs_toolset)
                end
                if vs_toolset then
                    pcoutputfilename = pcoutputfilename .. "_" .. vs_toolset
                end
            end
            pcoutputfilename = pcoutputfilename .. ".pch"
        end
        pcoutputfile = path.join(path.directory(pcoutputfile), sourcekind, pcoutputfilename)
        self._PCOUTPUTFILES[langkind] = pcoutputfile
        return pcoutputfile
    end
end

-- get runtimes
function _instance:runtimes()
    local runtimes = self:memcache():get("runtimes")
    if runtimes == nil then
        runtimes = self:get("runtimes")
        if runtimes then
            local runtimes_supported = hashset.new()
            local toolchains = self:toolchains() or platform.load(self:plat(), self:arch()):toolchains()
            if toolchains then
                for _, toolchain_inst in ipairs(toolchains) do
                    if toolchain_inst:is_standalone() and toolchain_inst:get("runtimes") then
                        for _, runtime in ipairs(table.wrap(toolchain_inst:get("runtimes"))) do
                            runtimes_supported:insert(runtime)
                        end
                    end
                end
            end
            local runtimes_current = {}
            for _, runtime in ipairs(table.wrap(runtimes)) do
                if runtimes_supported:has(runtime) then
                    table.insert(runtimes_current, runtime)
                end
            end
            runtimes = table.unwrap(runtimes_current)
        end
        runtimes = runtimes or false
        self:memcache():set("runtimes", runtimes)
    end
    return runtimes or nil
end

-- has the given runtime for the current toolchains?
function _instance:has_runtime(...)
    local runtimes_set = self:memcache():get("runtimes_set")
    if runtimes_set == nil then
        runtimes_set = hashset.from(table.wrap(self:runtimes()))
        self:memcache():set("runtimes_set", runtimes_set)
    end
    for _, v in ipairs(table.pack(...)) do
        if runtimes_set:has(v) then
            return true
        end
    end
end

-- get the given toolchain
function _instance:toolchain(name)
    local toolchains_map = self:memcache():get("toolchains_map")
    if toolchains_map == nil then
        toolchains_map = {}
        for _, toolchain_inst in ipairs(self:toolchains()) do
            toolchains_map[toolchain_inst:name()] = toolchain_inst
        end
        self:memcache():set("toolchains_map", toolchains_map)
    end
    return toolchains_map[name]
end

-- get the toolchains
function _instance:toolchains()
    local toolchains = self:memcache():get("toolchains")
    if toolchains == nil then

        -- load target toolchains first
        local has_standalone = false
        local target_toolchains = self:get("toolchains")
        if target_toolchains then
            toolchains = {}
            for _, name in ipairs(table.wrap(target_toolchains)) do
                local toolchain_opt = table.copy(self:extraconf("toolchains", name))
                toolchain_opt.arch = self:arch()
                toolchain_opt.plat = self:plat()
                toolchain_opt.namespace = self:namespace()
                local toolchain_inst, errors = toolchain.load(name, toolchain_opt)
                -- attempt to load toolchain from project
                if not toolchain_inst and target._project() then
                    toolchain_inst = target._project().toolchain(name, toolchain_opt)
                end
                if not toolchain_inst then
                    os.raise(errors)
                end
                if toolchain_inst:is_standalone() then
                    has_standalone = true
                end
                table.insert(toolchains, toolchain_inst)
            end

            -- we always need a standalone toolchain
            -- because we maybe only set partial toolchains in target, e.g. nasm toolchain
            --
            -- @note platform has been checked in config/_check_target_toolchains
            if not has_standalone then
                for _, toolchain_inst in ipairs(self:platform():toolchains()) do
                    if toolchain_inst:is_standalone() then
                        table.insert(toolchains, toolchain_inst)
                        has_standalone = true
                        break
                    end
                end
            end
        else
            toolchains = self:platform():toolchains()
        end

        self:memcache():set("toolchains", toolchains)
    end
    return toolchains
end

-- get the program and name of the given tool kind
function _instance:tool(toolkind)
    -- we cannot get tool in on_load, because target:toolchains() has been not checked in configuration stage.
    if not self._LOADED_AFTER then
        os.raise("we cannot get tool(%s) before target(%s) is loaded, maybe it is called on_load(), please call it in on_config().", toolkind, self:fullname())
    end
    return toolchain.tool(self:toolchains(), toolkind, {cachekey = "target_" .. self:fullname(), plat = self:plat(), arch = self:arch(),
                                                        before_get = function()
        -- get program from set_toolset
        local program = self:get("toolset." .. toolkind)

        -- get program from `xmake f --cc`
        if not program and not self:get("toolchains") then
            program = config.get(toolkind)
        end

        -- contain toolname? parse it, e.g. 'gcc@xxxx.exe'
        -- https://github.com/xmake-io/xmake/issues/1361
        local toolname
        if program then
            local pos = program:find('@', 1, true)
            if pos then
                -- we need to ignore valid path with `@`, e.g. /usr/local/opt/go@1.17/bin/go
                -- https://github.com/xmake-io/xmake/issues/2853
                local prefix = program:sub(1, pos - 1)
                if prefix and not prefix:find("[/\\]") then
                    toolname = prefix
                    program = program:sub(pos + 1)
                end
            end
        end

        -- find toolname
        if program and not toolname then
            local find_toolname = sandbox_module.import("lib.detect.find_toolname", {anonymous = true})
            toolname = find_toolname(program)
        end
        return program, toolname
    end})
end

-- get tool configuration from the toolchains
function _instance:toolconfig(name)
    return toolchain.toolconfig(self:toolchains(), name, {cachekey = "target_" .. self:fullname(), plat = self:plat(), arch = self:arch(),
                                                          after_get = function(toolchain_inst)
        -- get flags from target.on_xxflags()
        local script = toolchain_inst:get("target.on_" .. name)
        if type(script) == "function" then
            local ok, result_or_errors = utils.trycall(script, nil, self)
            if ok then
                return result_or_errors
            else
                os.raise(result_or_errors)
            end
        end
    end})
end

-- has source files with the given source kind?
function _instance:has_sourcekind(...)
    local sourcekinds_set = self._SOURCEKINDS_SET
    if sourcekinds_set == nil then
        sourcekinds_set = hashset.from(self:sourcekinds())
        self._SOURCEKINDS_SET = sourcekinds_set
    end
    for _, v in ipairs(table.pack(...)) do
        if sourcekinds_set:has(v) then
            return true
        end
    end
end

-- has the given tool for the current target?
--
-- e.g.
--
-- if target:has_tool("cc", "clang", "gcc") then
--    ...
-- end
function _instance:has_tool(toolkind, ...)
    local target_utils = target._target_utils
    if target_utils == nil then
        target_utils = sandbox_module.import("private.utils.target", {anonymous = true})
        target._target_utils = target_utils
    end
    local _, toolname = self:tool(toolkind)
    return target_utils.has_tool(toolname, table.pack(...))
end

-- has the given c funcs?
--
-- @param funcs     the funcs
-- @param opt       the argument options, e.g. {includes = "xxx.h", configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:has_cfuncs(funcs, opt)
    opt = opt or {}
    opt.target = self
    return sandbox_module.import("lib.detect.has_cfuncs", {anonymous = true})(funcs, opt)
end

-- has the given c++ funcs?
--
-- @param funcs     the funcs
-- @param opt       the argument options, e.g. {includes = "xxx.h", configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:has_cxxfuncs(funcs, opt)
    opt = opt or {}
    opt.target = self
    return sandbox_module.import("lib.detect.has_cxxfuncs", {anonymous = true})(funcs, opt)
end

-- has the given c types?
--
-- @param types     the types
-- @param opt       the argument options, e.g. {configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:has_ctypes(types, opt)
    opt = opt or {}
    opt.target = self
    return sandbox_module.import("lib.detect.has_ctypes", {anonymous = true})(types, opt)
end

-- has the given c++ types?
--
-- @param types     the types
-- @param opt       the argument options, e.g. {configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:has_cxxtypes(types, opt)
    opt = opt or {}
    opt.target = self
    return sandbox_module.import("lib.detect.has_cxxtypes", {anonymous = true})(types, opt)
end

-- has the given c includes?
--
-- @param includes  the includes
-- @param opt       the argument options, e.g. {configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:has_cincludes(includes, opt)
    opt = opt or {}
    opt.target = self
    return sandbox_module.import("lib.detect.has_cincludes", {anonymous = true})(includes, opt)
end

-- has the given c++ includes?
--
-- @param includes  the includes
-- @param opt       the argument options, e.g. {configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:has_cxxincludes(includes, opt)
    opt = opt or {}
    opt.target = self
    return sandbox_module.import("lib.detect.has_cxxincludes", {anonymous = true})(includes, opt)
end

-- has the given c flags?
--
-- @param flags     the flags
-- @param opt       the argument options, e.g. { flagskey = "xxx" }
--
-- @return          true or false, errors
--
function _instance:has_cflags(flags, opt)
    local compinst = self:compiler("cc")
    return compinst:has_flags(flags, "cflags", opt)
end

-- has the given c++ flags?
--
-- @param flags     the flags
-- @param opt       the argument options, e.g. { flagskey = "xxx" }
--
-- @return          true or false, errors
--
function _instance:has_cxxflags(flags, opt)
    local compinst = self:compiler("cxx")
    return compinst:has_flags(flags, "cxxflags", opt)
end

-- has the given features?
--
-- @param features  the features, e.g. {"c_static_assert", "cxx_constexpr"}
-- @param opt       the argument options, e.g. {flags = ""}
--
-- @return          true or false, errors
--
function _instance:has_features(features, opt)
    opt = opt or {}
    opt.target = self:_checked_target()
    return sandbox_module.import("core.tool.compiler", {anonymous = true}).has_features(features, opt)
end

-- check the size of type
--
-- @param typename  the typename
-- @param opt       the argument options, e.g. {includes = "xxx.h", configs = {defines = ""}}
--
-- @return          the type size
--
function _instance:check_sizeof(typename, opt)
    opt = opt or {}
    opt.target = self:_checked_target()
    return sandbox_module.import("lib.detect.check_sizeof", {anonymous = true})(typename, opt)
end

-- check the endianness of compiler
--
-- @param opt       the argument options, e.g. {includes = "xxx.h", configs = {defines = ""}}
--
-- @return          the type size
--
function _instance:check_bigendian(opt)
  opt = opt or {}
  opt.target = self:_checked_target()
  return sandbox_module.import("lib.detect.check_bigendian", {anonymous = true})(opt)
end

-- check the given c snippets?
--
-- @param snippets  the snippets
-- @param opt       the argument options, e.g. {includes = "xxx.h", configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:check_csnippets(snippets, opt)
    opt = opt or {}
    opt.target = self:_checked_target()
    return sandbox_module.import("lib.detect.check_csnippets", {anonymous = true})(snippets, opt)
end

-- check the given c++ snippets?
--
-- @param snippets  the snippets
-- @param opt       the argument options, e.g. {includes = "xxx.h", configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:check_cxxsnippets(snippets, opt)
    opt = opt or {}
    opt.target = self
    return sandbox_module.import("lib.detect.check_cxxsnippets", {anonymous = true})(snippets, opt)
end

-- check the given objc snippets?
--
-- @param snippets  the snippets
-- @param opt       the argument options, e.g. {includes = "xxx.h", configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:check_msnippets(snippets, opt)
    opt = opt or {}
    opt.target = self:_checked_target()
    return sandbox_module.import("lib.detect.check_msnippets", {anonymous = true})(snippets, opt)
end

-- check the given objc++ snippets?
--
-- @param snippets  the snippets
-- @param opt       the argument options, e.g. {includes = "xxx.h", configs = {defines = ""}}
--
-- @return          true or false, errors
--
function _instance:check_mxxsnippets(snippets, opt)
    opt = opt or {}
    opt.target = self:_checked_target()
    return sandbox_module.import("lib.detect.check_mxxsnippets", {anonymous = true})(snippets, opt)
end

-- get project
function target._project()
    return target._PROJECT
end

-- get target apis
function target.apis()

    return
    {
        values =
        {
            -- target.set_xxx
            "target.set_kind"
        ,   "target.set_plat"
        ,   "target.set_arch"
        ,   "target.set_strip"
        ,   "target.set_rules"
        ,   "target.set_group"
        ,   "target.add_filegroups"
        ,   "target.set_version"
        ,   "target.set_license"
        ,   "target.set_enabled"
        ,   "target.set_default"
        ,   "target.set_options"
        ,   "target.set_symbols"
        ,   "target.set_filename"
        ,   "target.set_basename"
        ,   "target.set_extension"
        ,   "target.set_prefixname"
        ,   "target.set_suffixname"
        ,   "target.set_warnings"
        ,   "target.set_fpmodels"
        ,   "target.set_optimize"
        ,   "target.set_runtimes"
        ,   "target.set_languages"
        ,   "target.set_toolchains"
        ,   "target.set_runargs"
        ,   "target.set_exceptions"
        ,   "target.set_encodings"
        ,   "target.set_prefixdir"
            -- target.add_xxx
        ,   "target.add_deps"
        ,   "target.add_rules"
        ,   "target.add_options"
        ,   "target.add_packages"
        ,   "target.add_imports"
        ,   "target.add_languages"
        ,   "target.add_vectorexts"
        ,   "target.add_toolchains"
        ,   "target.add_tests"
        }
    ,   keyvalues =
        {
            -- target.set_xxx
            "target.set_values"
        ,   "target.set_configvar"
        ,   "target.set_runenv"
        ,   "target.set_toolset"
        ,   "target.set_policy"
            -- target.add_xxx
        ,   "target.add_values"
        ,   "target.add_runenvs"
        }
    ,   paths =
        {
            -- target.set_xxx
            "target.set_targetdir"
        ,   "target.set_objectdir"
        ,   "target.set_dependir"
        ,   "target.set_autogendir"
        ,   "target.set_configdir"
        ,   "target.set_installdir"
        ,   "target.set_rundir"
            -- target.add_xxx
        ,   "target.add_files"
        ,   "target.add_cleanfiles"
        ,   "target.add_configfiles"
        ,   "target.add_installfiles"
        ,   "target.add_extrafiles"
            -- target.del_xxx (deprecated)
        ,   "target.del_files"
            -- target.remove_xxx
        ,   "target.remove_files"
        ,   "target.remove_headerfiles"
        ,   "target.remove_configfiles"
        ,   "target.remove_installfiles"
        ,   "target.remove_extrafiles"
        }
    ,   script =
        {
            -- target.on_xxx
            "target.on_run"
        ,   "target.on_test"
        ,   "target.on_load"
        ,   "target.on_config"
        ,   "target.on_prepare"
        ,   "target.on_prepare_file"
        ,   "target.on_prepare_files"
        ,   "target.on_link"
        ,   "target.on_build"
        ,   "target.on_build_file"
        ,   "target.on_build_files"
        ,   "target.on_clean"
        ,   "target.on_package"
        ,   "target.on_install"
        ,   "target.on_uninstall"
        ,   "target.on_preparecmd"
        ,   "target.on_preparecmd_file"
        ,   "target.on_preparecmd_files"
        ,   "target.on_linkcmd"
        ,   "target.on_buildcmd"
        ,   "target.on_buildcmd_file"
        ,   "target.on_buildcmd_files"
        ,   "target.on_installcmd"
        ,   "target.on_uninstallcmd"
            -- target.before_xxx
        ,   "target.before_run"
        ,   "target.before_test"
        ,   "target.before_config"
        ,   "target.before_prepare"
        ,   "target.before_prepare_file"
        ,   "target.before_prepare_files"
        ,   "target.before_link"
        ,   "target.before_build"
        ,   "target.before_build_file"
        ,   "target.before_build_files"
        ,   "target.before_clean"
        ,   "target.before_package"
        ,   "target.before_install"
        ,   "target.before_uninstall"
        ,   "target.before_preparecmd"
        ,   "target.before_preparecmd_file"
        ,   "target.before_preparecmd_files"
        ,   "target.before_linkcmd"
        ,   "target.before_buildcmd"
        ,   "target.before_buildcmd_file"
        ,   "target.before_buildcmd_files"
        ,   "target.before_installcmd"
        ,   "target.before_uninstallcmd"
            -- target.after_xxx
        ,   "target.after_run"
        ,   "target.after_test"
        ,   "target.after_load"
        ,   "target.after_config"
        ,   "target.after_prepare"
        ,   "target.after_prepare_file"
        ,   "target.after_prepare_files"
        ,   "target.after_link"
        ,   "target.after_build"
        ,   "target.after_build_file"
        ,   "target.after_build_files"
        ,   "target.after_clean"
        ,   "target.after_package"
        ,   "target.after_install"
        ,   "target.after_uninstall"
        ,   "target.after_preparecmd"
        ,   "target.after_preparecmd_file"
        ,   "target.after_preparecmd_files"
        ,   "target.after_linkcmd"
        ,   "target.after_buildcmd"
        ,   "target.after_buildcmd_file"
        ,   "target.after_buildcmd_files"
        ,   "target.after_installcmd"
        ,   "target.after_uninstallcmd"
        }
    }
end

-- get the filename from the given target name and kind
function target.filename(targetname, targetkind, opt)
    opt = opt or {}
    assert(targetname and targetkind)

    -- make filename by format
    local filename = targetname
    local format = opt.format or platform.format(targetkind, opt.plat, opt.arch) or "$(name)"
    if format then
        local splitinfo = format:split("$(name)", {plain = true, strict = true})
        local prefixname = splitinfo[1] or ""
        local suffixname = ""
        local extension = splitinfo[2] or ""
        splitinfo = extension:split('.', {plain = true, limit = 2, strict = true})
        if #splitinfo == 2 and splitinfo[1] ~= "" then
            suffixname = splitinfo[1]
            extension  = "." .. splitinfo[2]
        end
        if opt.prefixname then
            prefixname = opt.prefixname
        end
        if opt.suffixname then
            suffixname = opt.suffixname
        end
        if opt.extension then
            extension = opt.extension
        end
        filename = prefixname .. targetname .. suffixname .. extension
    end
    return filename
end

-- get the link name of the target file
function target.linkname(filename, opt)
    -- for implib/mingw, e.g. libxxx.dll.a
    opt = opt or {}
    if filename:startswith("lib") and filename:endswith(".dll.a") then
        return filename:sub(4, #filename - 6)
    end
    -- for macOS, libxxx.tbd
    if filename:startswith("lib") and filename:endswith(".tbd") then
        return filename:sub(4, #filename - 4)
    end
    local linkname, count = filename:gsub(target.filename("__pattern__", "static", {plat = opt.plat}):gsub("%.", "%%."):gsub("__pattern__", "(.+)") .. "$", "%1")
    if count == 0 then
        linkname, count = filename:gsub(target.filename("__pattern__", "shared", {plat = opt.plat}):gsub("%.", "%%."):gsub("__pattern__", "(.+)") .. "$", "%1")
    end
    -- in order to be compatible with mingw/windows library with .lib
    if count == 0 and opt.plat == "mingw" then
        linkname, count = filename:gsub(target.filename("__pattern__", "static", {plat = "windows"}):gsub("%.", "%%."):gsub("__pattern__", "(.+)") .. "$", "%1")
    end
    if count > 0 and linkname then
        return linkname
    end
    -- fallback to the generic unix library name, libxxx.a, libxxx.so, ..
    if filename:startswith("lib") then
        if filename:endswith(".a") or filename:endswith(".so") then
            return path.basename(filename:sub(4))
        end
    elseif filename:endswith(".so") or filename:endswith(".dylib") then
        -- for custom shared libraries name, xxx.so, xxx.dylib
        return filename
    end
    return nil
end

-- new a target instance
function target.new(...)
    return _instance.new(...)
end

-- return module
return target
