--!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        package.lua
--

-- imports
import("core.base.semver")
import("core.base.option")
import("core.base.global")
import("core.base.hashset")
import("utils.progress")
import("core.cache.memcache")
import("core.project.project")
import("core.project.config")
import("core.tool.toolchain")
import("core.platform.platform")
import("core.package.package", {alias = "core_package"})
import("devel.git")
import("private.action.require.impl.check_api")
import("private.action.require.impl.repository")
import("private.action.require.impl.search_packages")
import("private.action.require.impl.utils.requirekey", {alias = "_get_requirekey"})
import("private.utils.package", {alias = "package_utils"})

-- get memcache
function _memcache()
    return memcache.cache("require.impl.package")
end

-- load require info
function _load_require(require_str, requires_extra, opt)
    opt = opt or {}

    -- parse require
    local packagename, version, reponame = package_utils.parse_requirestr(require_str)

    -- get require extra
    local require_extra = {}
    if requires_extra then
        require_extra = requires_extra[require_str] or {}
    end

    -- parse configs from package name, and we need to ignore 3rd package name, e.g. vcpkg::boost[core], ...
    -- @see https://github.com/xmake-io/xmake/issues/5727#issuecomment-2421040107
    --
    -- e.g.
    --   add_requires("boost[iostreams,system,thread,key=value] >=1.78.0")
    --   add_requires("libplist[shared,debug,codecs=[foo,bar,zoo]]")
    --
    local packagename_raw, configs_str = packagename:match("(.-)%[(.*)%]")
    if packagename_raw and configs_str and not packagename:find("::", 1, true) then
        configs_str = configs_str:gsub("%[(.*)%]", function (w)
            return w:replace(",", ":")
        end)
        packagename = packagename_raw
        local splitinfo = configs_str:split(",", {plain = true})
        for _, v in ipairs(splitinfo) do
            local v = v
            local parts = v:split("=", {plain = true})
            local k = parts[1]
            v = parts[2]
            require_extra.configs = require_extra.configs or {}
            local configs = require_extra.configs
            if v then
                if v:find(":", 1 ,true) then
                    configs[k] = v:split(":", {plain = true})
                else
                    configs[k] = option.boolean(v)
                end
            else
                configs[k] = true
            end
        end
    end

    -- resolve require info
    local resolvedinfo = opt.resolvedinfo
    local requirepath = opt.requirepath
    if requirepath then
        requirepath = requirepath .. "." .. packagename
    else
        requirepath = packagename
    end
    resolvedinfo = resolvedinfo and resolvedinfo[requirepath]
    if resolvedinfo then
        -- resolve the conflict package version
        if resolvedinfo.version then
            version = resolvedinfo.version
        end
        -- resolve the conflict package configs
        if resolvedinfo.configs then
            require_extra.configs = resolvedinfo.configs
        end
    end

    -- get required building configurations
    -- we need to clone a new configs object, because the whole requireinfo will be modified later.
    -- @see https://github.com/xmake-io/xmake-repo/pull/2067
    local require_build_configs = table.clone(require_extra.configs or require_extra.config)
    if require_extra.debug then
        require_build_configs = require_build_configs or {}
        require_build_configs.debug = true
    end

    -- vs_runtime is deprecated, we should use runtimes
    if require_build_configs and require_build_configs.vs_runtime then
        require_build_configs.runtimes = require_build_configs.vs_runtime
        require_build_configs.vs_runtime = nil
        wprint("add_requires(%s): vs_runtime is deprecated, please use runtimes!", require_str)
    end

    -- check require options
    local extra_options = hashset.of("plat", "arch", "kind", "host", "targetos",
    "alias", "group", "system", "option", "default", "optional", "debug",
    "verify", "external", "private", "build", "configs", "version", "public")
    for name, value in pairs(require_extra) do
        if not extra_options:has(name) then
            wprint("add_requires(\"%s\") has unknown option: {%s=%s}!", require_str, name, tostring(value))
        end
    end

    -- we always use xmake package, `add_requires("xmake::zlib")`,
    -- it is equivalent to `add_requires("zlib", {system = false})`
    if packagename:startswith("xmake::") then
        packagename = packagename:sub(8)
        require_extra.system = false
    end

    -- init required item
    local required = {}
    local parentinfo = opt.parentinfo
    parentinfo = parentinfo or {}
    required.packagename = packagename
    required.requireinfo =
    {
        originstr        = require_str,
        reponame         = reponame,
        version          = require_extra.version or version,
        host             = require_extra.host,      -- this package is only for host machine
        plat             = require_extra.plat,      -- require package in the given platform
        arch             = require_extra.arch,      -- require package in the given architecture
        targetos         = require_extra.targetos,  -- require package in the given target os
        kind             = require_extra.kind,      -- default: library, set package kind, e.g. binary, library, we can set `kind = "binary"` to only detect binary program and ignore library.
        alias            = require_extra.alias,     -- set package alias name
        group            = require_extra.group,     -- only uses the first package in same group
        system           = require_extra.system,    -- default: true, we can set it to disable system package manually
        option           = require_extra.option,    -- set and attach option
        configs          = require_build_configs,   -- the required building configurations
        default          = require_extra.default,   -- default: true, we can set it to disable package manually
        optional         = parentinfo.optional or require_extra.optional, -- default: false, inherit parentinfo.optional
        verify           = require_extra.verify,    -- default: true, we can set false to ignore sha256sum and select any version
        external         = require_extra.external,  -- default: true, we use sysincludedirs/-isystem instead of -I/xxx
        private          = require_extra.private,   -- default: false, private package, only for installation, do not export any links/includes and environments
        build            = require_extra.build,     -- default: false, always build packages, we do not use the precompiled artifacts
        resolvedinfo     = resolvedinfo             -- the resolved info for the conflict version/configs
    }
    return required.packagename, required.requireinfo
end

-- load package package from system
function _load_package_from_system(packagename)
    return core_package.load_from_system(packagename)
end

-- load package package from project
function _load_package_from_project(packagename)
    return core_package.load_from_project(packagename)
end

-- load package package from repositories
function _load_package_from_repository(packagename, opt)
    opt = opt or {}
    local packagedir, repo = repository.packagedir(packagename, opt)
    if packagedir then
        return core_package.load_from_repository(packagename, packagedir, {plat = opt.plat, arch = opt.arch, repo = repo})
    end
end

-- load package package from base
function _load_package_from_base(package, basename, opt)
    local package_base = _load_package_from_project(basename)
    if not package_base then
        package_base = _load_package_from_repository(basename, opt)
    end
    if package_base then
        package._BASE = package_base
    end
end

-- has locked requires?
function _has_locked_requires(opt)
    opt = opt or {}
    if not option.get("upgrade") or opt.force then
        return project.policy("package.requires_lock") and os.isfile(project.requireslock())
    end
end

-- get locked requires
function _get_locked_requires(requirekey, opt)
    opt = opt or {}
    local requireslock = _memcache():get("requireslock")
    if requireslock == nil or opt.force then
        if _has_locked_requires(opt) then
            requireslock = io.load(project.requireslock())
        end
        _memcache():set("requireslock", requireslock or false)
    end
    if requireslock then
        local plat = config.plat() or os.subhost()
        local arch = config.arch() or os.subarch()
        local key = plat .. "|" .. arch
        if requireslock[key] then
            return requireslock[key][requirekey], requireslock.__meta__.version
        end
    end
end

-- sort package deps
--
-- e.g.
--
-- a.deps = b
-- b.deps = c
--
-- orderdeps: c -> b -> a
--
function _sort_packagedeps(package)
    -- we must use native deps list instead of package:deps() to generate correct librarydeps
    local orderdeps = {}
    for _, dep in ipairs(package:plaindeps()) do
        if dep then
            table.join2(orderdeps, _sort_packagedeps(dep))
            table.insert(orderdeps, dep)
        end
    end
    return orderdeps
end

-- sort library deps and generate correct link order
--
-- e.g.
--
-- a.deps = b
-- b.deps = c
--
-- orderdeps: a -> b -> c
--
function _sort_librarydeps(package, opt)
    -- we must use native deps list instead of package:deps() to generate correct link order
    local orderdeps = {}
    for _, dep in ipairs(package:plaindeps()) do
        if dep and dep:is_library() and (opt and opt.private or not dep:is_private()) then
            table.insert(orderdeps, dep)
            table.join2(orderdeps, _sort_librarydeps(dep, opt))
        end
    end
    return orderdeps
end

-- get builtin configuration default values
function _get_default_config_value_of(name)
    local defaults = {
        debug = false,
        shared = false,
        pic = true,
        lto = false,
        asan = false
    }
    return defaults[name]
end

-- add some builtin configurations to package
function _add_package_configurations(package)
    -- we can define configs to override it and it's default value in package()
    if package:extraconf("configs", "debug", "default") == nil then
        local default = _get_default_config_value_of("debug")
        package:add("configs", "debug", {builtin = true, description = "Enable debug symbols.", default = default, type = "boolean"})
    end
    if package:extraconf("configs", "shared", "default") == nil then
        -- we always use static library if it's for wasm platform
        local readonly
        if package:is_plat("wasm") then
            readonly = true
        end
        local default = _get_default_config_value_of("shared")
        package:add("configs", "shared", {builtin = true, description = "Build shared library.", default = default, readonly = readonly, type = "boolean"})
    end
    if package:extraconf("configs", "pic", "default") == nil then
        local default = _get_default_config_value_of("pic")
        package:add("configs", "pic", {builtin = true, description = "Enable the position independent code.", default = default, type = "boolean"})
    end
    if package:extraconf("configs", "lto", "default") == nil then
        package:add("configs", "lto", {builtin = true, description = "Enable the link-time build optimization.", type = "boolean"})
    end
    if package:extraconf("configs", "asan", "default") == nil then
        package:add("configs", "asan", {builtin = true, description = "Enable the address sanitizer.", type = "boolean"})
    end
    if package:extraconf("configs", "runtimes", "default") == nil then
        local values = {"MT", "MTd", "MD", "MDd",
                        "c++_static", "c++_shared", "stdc++_static", "stdc++_shared"}
        package:add("configs", "runtimes", {builtin = true, description = "Set the compiler runtimes.", type = "string", values = values, restrict = function (value)
            local values_set = hashset.from(values)
            if type(value) ~= "string" then
                return false
            end
            if value then
                for _, item in ipairs(value:split(",", {plain = true})) do
                    if not values_set:has(item) then
                        return false
                    end
                end
            end
            return true
        end})
    end
    -- deprecated, please use runtimes
    if package:extraconf("configs", "vs_runtime", "default") == nil then
        package:add("configs", "vs_runtime", {builtin = true, description = "Set vs compiler runtime.", values = {"MT", "MTd", "MD", "MDd"}})
    end
    if package:extraconf("configs", "toolchains", "default") == nil then
        package:add("configs", "toolchains", {builtin = true, description = "Set package toolchains only for cross-compilation."})
    end
    package:add("configs", "cflags", {builtin = true, description = "Set the C compiler flags."})
    package:add("configs", "cxflags", {builtin = true, description = "Set the C/C++ compiler flags."})
    package:add("configs", "cxxflags", {builtin = true, description = "Set the C++ compiler flags."})
    package:add("configs", "asflags", {builtin = true, description = "Set the assembler flags."})
    package:add("configs", "ldflags", {builtin = true, description = "Set the binary linker flags."})
    package:add("configs", "shflags", {builtin = true, description = "Set the shared library linker flags."})
end

-- select version from scheme
function _select_version_from_scheme(scheme, requireinfo)

    -- check urls
    local urls = scheme:urls()
    local has_giturl = false
    for _, url in ipairs(urls) do
        if git.checkurl(url) then
            has_giturl = true
            break
        end
    end

    -- select package version
    local source = nil
    local version = nil
    local require_version = requireinfo.version
    local require_verify  = requireinfo.verify
    local is_system = requireinfo.system
    local has_versionlist = scheme:get("versions") or scheme:get("versionfiles")
    -- We need to strictly limit the number of valid URLs to filter out schemes that don't have any URLs, prioritizing them.
    if #urls > 0 and (not has_versionlist or require_verify == false)
        and (semver.is_valid(require_version) or semver.is_valid_range(require_version)) then
        -- no version list in package() or need not verify sha256sum? try selecting this version directly
        -- @see
        -- https://github.com/xmake-io/xmake/issues/930
        -- https://github.com/xmake-io/xmake/issues/1009
        -- https://github.com/xmake-io/xmake/issues/3551
        version = require_version
        source = "version"
    elseif #scheme:versions() > 0 then -- select version?
        version, source = try { function () return semver.select(require_version, scheme:versions()) end }
    end
    if not version and has_giturl then -- select branch?
        if require_version and #require_version == 40 and require_version:match("%w+") then
            version, source = require_version, "commit"
        else
            version, source = require_version ~= "latest" and require_version or "@default", "branch"
        end
    end
    -- local source package? we use a phony version
    if not version and require_version == "latest" and #urls == 0 then
        version = "latest"
        source = "version"
    end
    return version, source
end

-- select package version
function _select_package_version(package, requireinfo, locked_requireinfo)

    -- get it from the locked requireinfo
    if locked_requireinfo then
        local version = locked_requireinfo.version
        local source = "version"
        if locked_requireinfo.branch then
            source = "branch"
        elseif locked_requireinfo.tag then
            source = "tag"
        end
        package:version_set(version, source)
        return version, source
    end

    -- select version from schemes
    local version, source
    local version_latest, source_latest
    local scheme_version_map = {}
    for _, scheme in ipairs(package:schemes_orderlist()) do
        local scheme_version, scheme_source = _select_version_from_scheme(scheme, requireinfo)
        if scheme_version then
            scheme_version_map[scheme] = scheme_version
            scheme:version_set(scheme_version, scheme_source)
            if not version and scheme_version ~= "latest" then
                version = scheme_version
                source = scheme_source
            end
            if not version_latest and scheme_version == "latest" then
                version_latest = scheme_version
                source_latest = scheme_source
            end
        end
    end
    if not version then
        -- If no version is available for any scheme and no URLs are available, we will use the specified version by default.
        -- https://github.com/xmake-io/xmake/issues/7265
        local require_version = requireinfo.version
        local require_verify  = requireinfo.verify
        local current_scheme = package:current_scheme()
        local has_versionlist = current_scheme:get("versions") or current_scheme:get("versionfiles")
        if (not has_versionlist or require_verify == false)
            and (semver.is_valid(require_version) or semver.is_valid_range(require_version)) then
            local scheme_version = require_version
            local scheme_source = "version"
            scheme_version_map[current_scheme] = scheme_version
            current_scheme:version_set(scheme_version, scheme_source)
            version = scheme_version
            source = scheme_source
        end
    end
    if not version then
        version = version_latest
        source = source_latest
    end

    -- remove these invalid schemes if version is not matched
    table.remove_if(package:schemes_orderlist(), function (_, scheme)
        local scheme_version = scheme_version_map[scheme]
        if not scheme_version or (version and scheme_version ~= version) then
            package:schemes()[scheme:name()] = nil
            -- reset current scheme cache, we need to resolve new current scheme
            package:current_scheme_set(nil)
            return true
        end
    end)
    assert(#package:schemes_orderlist() > 0, "package(%s): no available schemes with urls and versions", package:name())

    local is_system = requireinfo.system
    if not version and not package:is_thirdparty() and is_system ~= true then
        local require_version = requireinfo.version
        raise("package(%s): version(%s) not found!", package:name(), require_version)
    end
    return version, source
end

-- check the configurations of packages
--
-- package("pcre2")
--      add_configs("bitwidth", {description = "Set the code unit width.", default = "8", values = {"8", "16", "32"}})
--      add_configs("bitwidth", {type = "number", values = {8, 16, 32}})
--      add_configs("bitwidth", {restrict = function(value) if tonumber(value) < 100 then return true end})
--
function _check_package_configurations(package)
    local configs_defined = {}
    for _, name in ipairs(package:get("configs")) do
        configs_defined[name] = package:extraconf("configs", name) or {}
    end
    for name, value in pairs(package:configs()) do
        local conf = configs_defined[name]
        if conf then
            local config_type = conf.type
            if config_type ~= nil and type(value) ~= config_type then
                raise("package(%s %s): invalid type(%s) for config(%s), need type(%s)!", package:displayname(), package:version_str(), type(value), name, config_type)
            end
            if conf.restrict then
                if not conf.restrict(value) then
                    raise("package(%s %s): invalid value(%s) for config(%s)!", package:displayname(), package:version_str(), string.serialize(value, {indent = false}), name)
                end
            elseif conf.values then
                local found = false
                for _, config_value in ipairs(conf.values) do
                    if tostring(value) == tostring(config_value) then
                        found = true
                        break
                    end
                end
                if not found then
                    raise("package(%s %s): invalid value(%s) for config(%s), please run `xmake require --info %s` to get all valid values!", package:displayname(), package:version_str(), value, name, package:name())
                end
            end
        else
            raise("package(%s %s): invalid config(%s), please run `xmake require --info %s` to get all configurations!", package:displayname(), package:version_str(), name, package:name())
        end
    end
end

-- check package toolchains
function _check_package_toolchains(package)
    if package:toolchains() then
        for _, toolchain_inst in pairs(package:toolchains()) do
            if not toolchain_inst:check() then
                raise("toolchain(\"%s\"): not found!", toolchain_inst:name())
            end
        end
    else
        -- maybe this package is host package, it's platform and toolchain has been not checked yet.
        local platform_inst = platform.load(package:plat(), package:arch(), {host = package:is_host()})
        if not platform_inst:check() then
            raise("no any matched platform for this package(%s)!", package:name())
        end
    end
end

-- match require path
function _match_requirepath(requirepath, requireconf)

    -- get pattern
    local function _get_pattern(pattern)
        pattern = pattern:gsub("([%+%.%-%^%$%(%)%%])", "%%%1")
        pattern = pattern:gsub("%*%*", "\001")
        pattern = pattern:gsub("%*", "\002")
        pattern = pattern:gsub("\001", ".*")
        pattern = pattern:gsub("\002", "[^.]*")
        pattern = string.ipattern(pattern, true)
        return pattern
    end

    -- get the excludes
    local excludes = requireconf:match("|.*$")
    if excludes then excludes = excludes:split("|", {plain = true}) end

    -- do match
    local pattern = requireconf:gsub("|.*$", "")
    pattern = _get_pattern(pattern)
    if (requirepath:match('^' .. pattern .. '$')) then
        -- exclude sub-deps, e.g. "libwebp.**|cmake|autoconf"
        local splitinfo = requirepath:split(".", {plain = true})
        if #splitinfo > 0 then
            local name = splitinfo[#splitinfo]
            for _, exclude in ipairs(excludes) do
                pattern = _get_pattern(exclude)
                if (name:match('^' .. pattern .. '$')) then
                    return false
                end
            end
        end
        return true
    end
end

-- init requireinfo
function _init_requireinfo(requireinfo, package, opt)
    -- pass root configs to top library package
    requireinfo.configs = requireinfo.configs or {}
    if opt.is_toplevel then
        requireinfo.is_toplevel = true

        -- we always pass some configurations from toplevel even it's headeronly, because it's library deps need inherit them
        -- @see https://github.com/xmake-io/xmake/issues/2688
        if package:is_library() then
            requireinfo.configs.toolchains = requireinfo.configs.toolchains or project.get("target.toolchains")
            if project.policy("package.inherit_external_configs") then
                requireinfo.configs.toolchains = requireinfo.configs.toolchains or get_config("toolchain")
            end
        end
        requireinfo.configs.runtimes = requireinfo.configs.runtimes or project.get("target.runtimes")
        if project.policy("package.inherit_external_configs") then
            requireinfo.configs.runtimes = requireinfo.configs.runtimes or get_config("runtimes") or get_config("vs_runtime")
        end
        if type(requireinfo.configs.runtimes) == "table" then
            requireinfo.configs.runtimes = table.concat(requireinfo.configs.runtimes, ",")
        end
        if requireinfo.configs.lto == nil then
            requireinfo.configs.lto = project.policy("build.optimization.lto")
        end
        if requireinfo.configs.asan == nil then
            requireinfo.configs.asan = project.policy("build.sanitizer.address")
        end
    end
    -- but we will ignore some configs for buildhash in the headeronly, moduleonly and host/binary package
    -- @note on_test still need these configs, @see https://github.com/xmake-io/xmake/issues/4124
    if package:is_headeronly() or package:is_moduleonly() or (package:is_binary() and not package:is_cross()) then
        requireinfo.ignored_configs_for_buildhash = {"runtimes", "toolchains", "lto", "asan", "pic"}
    end
end

-- finish requireinfo
function _finish_requireinfo(requireinfo, package)
    requireinfo.configs = requireinfo.configs or {}
    if package:is_plat("windows") then
        -- @see https://github.com/xmake-io/xmake/issues/4477#issuecomment-1913249489
        -- @note its buildhash will be ignored for headeronly
        local runtimes = requireinfo.configs.runtimes
        if runtimes then
            runtimes = runtimes:split(",")
        else
            runtimes = {}
        end
        -- we should not set default runtimes if runtimes has been set, e.g. `xmake f --toolchain=clang --runtimes=c++_shared`
        if #runtimes == 0 then
            local vs_runtime_default = project.policy("build.c++.msvc.runtime")
            if vs_runtime_default and is_mode("debug") then
                vs_runtime_default = vs_runtime_default .. "d"
            end
            table.insert(runtimes, vs_runtime_default or "MT")
        end
        requireinfo.configs.runtimes = table.concat(runtimes, ",")
    end
    -- we need to ensure readonly configs
    for _, name in ipairs(table.keys(requireinfo.configs)) do
        local current = requireinfo.configs[name]
        local default = package:extraconf("configs", name, "default")
        local readonly = package:extraconf("configs", name, "readonly")
        if name == "runtimes" then
            -- vs_runtime is deprecated, but we need also support it now.
            if default == nil then
                default = package:extraconf("configs", "vs_runtime", "default")
            end
            if readonly == nil then
                readonly = package:extraconf("configs", "vs_runtime", "readonly")
            end
            if default ~= nil or readonly ~= nil then
                wprint("please use add_configs(\"runtimes\") instead of add_configs(\"vs_runtime\").")
            end
        end
        if readonly and current ~= default then
            wprint("configs.%s is readonly in package(%s), it's always %s", name, package:name(), default)
            -- package:config() will use default value after loading package
            requireinfo.configs[name] = nil
        end
    end
    -- sync default value to prevent cache mismatch (buildhash)
    -- @see https://github.com/xmake-io/xmake/pull/4324
    for k, v in pairs(requireinfo.configs) do
        local default = _get_default_config_value_of(k)
        if v == default then
            requireinfo.configs[k] = nil
        end
    end

    -- all binary packages are host package
    -- we need to synchronize the setup to requireinfo so that all its dependent packages inherit from it.
    if package:is_binary() then
        requireinfo.host = true
    end
end

-- merge requireinfo from `add_requireconfs()`
--
-- add_requireconfs("*",                         {system = false, configs = {runtimes = "MD"}})
-- add_requireconfs("lib*",                      {system = false, configs = {runtimes = "MD"}})
-- add_requireconfs("libwebp",                   {system = false, configs = {runtimes = "MD"}})
-- add_requireconfs("libpng.zlib",               {system = false, override = true, configs = {cxflags = "-DTEST1"}, version = "1.2.10"})
-- add_requireconfs("libtiff.*",                 {system = false, configs = {cxflags = "-DTEST2"}})
-- add_requireconfs("libwebp.**|cmake|autoconf", {system = false, configs = {cxflags = "-DTEST3"}}) -- recursive deps
--
function _merge_requireinfo(requireinfo, requirepath)

    -- only for project
    if not os.isfile(os.projectfile()) then
        return
    end

    -- find requireconf from the given requirepath
    local requireconf_result = {}
    local requireconfs, requireconfs_extra = project.requireconfs_str()
    if requireconfs then
        for _, requireconf in ipairs(requireconfs) do
            if _match_requirepath(requirepath, requireconf) then
                local requireconf_extra = requireconfs_extra[requireconf]
                table.insert(requireconf_result, {requireconf = requireconf, requireconf_extra = requireconf_extra})
            end
        end
    end

    -- Append requireconf_extra into requireinfo
    -- and the configs of add_requires have a higher priority than add_requireconfs.
    --
    -- e.g.
    -- add_requireconfs("*", {configs = {debug = false}})
    -- add_requires("foo", "bar", {configs = {debug = true}})
    --
    -- foo and bar will be debug mode
    --
    -- We can also override the configs of add_requires
    --
    -- e.g.
    -- add_requires("zlib 1.2.11")
    -- add_requireconfs("zlib", {override = true, version = "1.2.10"})
    --
    -- We override the version of zlib to 1.2.10
    --
    -- If the same dependency is matched to multiple configurations,
    -- the configurations are merged by default,
    -- and if override is set, then it rewrites the previous configurations.
    --
    for _, item in ipairs(requireconf_result) do
        local requireconf_extra = item.requireconf_extra
        if requireconf_extra then
            -- preprocess requireconf_extra, (debug, override ..)
            local override = requireconf_extra.override
            if requireconf_extra.debug then
                requireconf_extra.configs = requireconf_extra.configs or {}
                requireconf_extra.configs.debug = true
                requireconf_extra.debug = nil
            end
            -- append or override configs and extra options
            for k, v in pairs(requireconf_extra.configs) do
                requireinfo.configs = requireinfo.configs or {}
                if override or requireinfo.configs[k] == nil then
                    requireinfo.configs[k] = v
                end
            end
            for k, v in pairs(requireconf_extra) do
                if k ~= "configs" then
                    if override or requireinfo[k] == nil then
                        requireinfo[k] = v
                    end
                end
            end
        end
    end
end

-- get package key
function _get_packagekey(packagename, requireinfo, version)
    return _get_requirekey(requireinfo, {name = packagename,
                                         host = requireinfo.host,
                                         plat = requireinfo.plat,
                                         arch = requireinfo.arch,
                                         kind = requireinfo.kind,
                                         version = version or requireinfo.version})
end

-- get locked package key
function _get_packagelock_key(requireinfo)
    local requirestr  = requireinfo.originstr
    local key         = _get_requirekey(requireinfo, {hash = true})
    return string.format("%s#%s", requirestr, key)
end

-- inherit some builtin configs of parent package if these config values are not default value
-- e.g. add_requires("libpng", {configs = {runtimes = "MD", pic = false}})
--
function _inherit_parent_configs(requireinfo, package, parentinfo)
    if package:is_library() then
        local requireinfo_configs = requireinfo.configs or {}
        local parentinfo_configs  = parentinfo.configs or {}
        if not requireinfo_configs.shared then
            if requireinfo_configs.runtimes == nil then
                requireinfo_configs.runtimes = parentinfo_configs.runtimes
            end
            if requireinfo_configs.pic == nil then
                requireinfo_configs.pic = parentinfo_configs.pic
            end
        end
        if parentinfo.plat then
            requireinfo.plat = parentinfo.plat
        end
        if parentinfo.arch then
            requireinfo.arch = parentinfo.arch
        end
        if parentinfo.host then
            requireinfo.host = parentinfo.host
        end
        requireinfo_configs.toolchains = requireinfo_configs.toolchains or parentinfo_configs.toolchains
        requireinfo_configs.runtimes = requireinfo_configs.runtimes or parentinfo_configs.runtimes
        requireinfo_configs.lto = requireinfo_configs.lto or parentinfo_configs.lto
        requireinfo_configs.asan = requireinfo_configs.asan or parentinfo_configs.asan
        requireinfo.configs = requireinfo_configs
    end
end

-- select artifacts for msvc
function _select_artifacts_for_msvc(package, artifacts_manifest)
    local msvc
    for _, instance in ipairs(package:toolchains()) do
        if instance:name() == "msvc" then
            msvc = instance
            break
        end
    end
    if not msvc then
        msvc = toolchain.load("msvc", {plat = package:plat(), arch = package:arch()})
    end
    local vcvars = msvc:config("vcvars")
    if vcvars then
        local vs_toolset = vcvars.VCToolsVersion
        if vs_toolset and semver.is_valid(vs_toolset) then
            local artifacts_infos = {}
            for key, artifacts_info in pairs(artifacts_manifest) do
                if key:startswith(package:plat() .. "-" .. package:arch() .. "-vc") and key:endswith("-" .. package:buildhash()) then
                    table.insert(artifacts_infos, artifacts_info)
                end
            end
            -- we sort them to select a newest toolset to get better optimzed performance
            table.sort(artifacts_infos, function (a, b)
                if a.toolset and b.toolset then
                    return semver.compare(a.toolset, b.toolset) > 0
                else
                    return false
                end
            end)
            if package:config("shared") or package:is_binary() then
                -- executable programs and dynamic libraries only need to select the latest toolset
                return artifacts_infos[1]
            else
                -- static libraries need to consider toolset compatibility
                for _, artifacts_info in ipairs(artifacts_infos) do
                    -- toolset is backwards compatible
                    --
                    -- @see https://github.com/xmake-io/xmake/issues/1513
                    -- https://docs.microsoft.com/en-us/cpp/porting/binary-compat-2015-2017?view=msvc-160
                    if artifacts_info.toolset and semver.compare(vs_toolset, artifacts_info.toolset) >= 0 then
                        return artifacts_info
                    end
                end
            end
        end
    end
end

-- select artifacts for generic
function _select_artifacts_for_generic(package, artifacts_manifest)
    local buildid = package:plat() .. "-" .. package:arch() .. "-" .. package:buildhash()
    return artifacts_manifest[buildid]
end

-- select to use precompiled artifacts?
function _select_artifacts(package, artifacts_manifest)
    -- the precompile policy is disabled in package?
    if package:policy("package.precompiled") == false then
        return
    end
    -- the precompile policy is disabled in project?
    if os.isfile(os.projectfile()) and project.policy("package.precompiled") == false then
        return
    end
    local artifacts_info
    if package:is_plat("windows") then -- for msvc
        artifacts_info = _select_artifacts_for_msvc(package, artifacts_manifest)
    else
        artifacts_info = _select_artifacts_for_generic(package, artifacts_manifest)
    end
    if artifacts_info then
        package:use_precompiled_artifacts(artifacts_info)
    end
end

-- select package runtimes
function _select_package_runtimes(package)
    local runtimes = package:config("runtimes")
    if runtimes then
        local runtimes_supported = hashset.new()
        local toolchains = package:toolchains() or
            platform.load(package:plat(), package:arch(), {host = package:is_host()}):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:split(",", {plain = true}))) do
            if runtimes_supported:has(runtime) then
                table.insert(runtimes_current, runtime)
            end
        end
        -- we need update runtimes for buildhash, configs ...
        --
        -- we use configs_overrided to override configs in configs and buildhash,
        -- but we shouldn't modify requireinfo.configs directly as it affects the package cache key
        --
        -- @see https://github.com/xmake-io/xmake/issues/4477#issuecomment-1913185727
        local requireinfo = package:requireinfo()
        if requireinfo then
            requireinfo.configs_overrided = requireinfo.configs_overrided or {}
            requireinfo.configs_overrided.runtimes = #runtimes_current > 0 and table.concat(runtimes_current, ",") or nil
            package:_invalidate_configs()
        end
    end
end

-- load required packages
function _load_package(packagename, requireinfo, opt)

    -- check circular dependency
    opt = opt or {}
    if opt.requirepath then
        local splitinfo = opt.requirepath:split(".", {plain = true})
        if #splitinfo > 3 and
            splitinfo[1] == splitinfo[#splitinfo - 1] and
            splitinfo[2] == splitinfo[#splitinfo] then
            raise("circular dependency(%s) detected in package(%s)!", opt.requirepath, splitinfo[1])
        end
    end

    -- strip trailing ~tag, e.g. zlib~debug
    local displayname
    if packagename:find('~', 1, true) then
        displayname = packagename
        local splitinfo = packagename:split('~', {plain = true, limit = 2})
        packagename = splitinfo[1]
        requireinfo.alias = requireinfo.alias or displayname
        requireinfo.label = splitinfo[2]
    end

    -- save requirekey
    local requirekey = _get_packagelock_key(requireinfo)
    requireinfo.requirekey = requirekey

    -- get locked requireinfo
    local locked_requireinfo = get_locked_requireinfo(requireinfo)

    -- load package from project first
    local package
    if os.isfile(os.projectfile()) then
        package = _load_package_from_project(packagename)
        if package and package:namespace() then
            packagename = package:namespace() .. "::" .. packagename
            if displayname then
                displayname = package:namespace() .. "::" .. displayname
            end
        end
    end

    -- load package from repositories
    local from_repo = false
    if not package then
        package = _load_package_from_repository(packagename, {
            plat = requireinfo.plat,
            arch = requireinfo.arch,
            name = requireinfo.reponame,
            locked_repo = locked_requireinfo and locked_requireinfo.repo})
        if package then
            from_repo = true
        end
    end

    -- load base package
    if package and package:get("base") then
        _load_package_from_base(package, package:get("base", {
            name = requireinfo.reponame, locked_repo = locked_requireinfo and locked_requireinfo.repo}))
    end

    -- load package from system
    local system = requireinfo.system
    if system == nil then
        system = opt.system
    end
    if not package and (system ~= false or packagename:find("::", 1, true)) then
        package = _load_package_from_system(packagename)
    end

    -- check unknown package
    if not package then
        cprint("${bright color.warning}note: ${clear}the following packages were not found in any repository (check if they are spelled correctly):")
        local tips
        local possible_package = get_possible_package(packagename)
        if possible_package then
            local possible_name = possible_package.name
            if possible_package.version then
                possible_name = possible_name .. " " .. possible_package.version
            end
            tips = string.format(", maybe ${bright}%s${clear} in %s", possible_name, possible_package.reponame)
        end
        cprint("  -> %s%s", packagename, tips or "")
        raise("package(%s) not found!", packagename)
    end

    -- init requireinfo
    _init_requireinfo(requireinfo, package, {is_toplevel = not opt.parentinfo})

    -- merge requireinfo from `add_requireconfs()`
    _merge_requireinfo(requireinfo, opt.requirepath)

    -- inherit some builtin configs of parent package, e.g. runtimes, pic
    if opt.parentinfo then
        _inherit_parent_configs(requireinfo, package, opt.parentinfo)
    end

    -- finish requireinfo
    _finish_requireinfo(requireinfo, package)

    -- save require info
    package:requireinfo_set(requireinfo)

    -- only load toolchain package and its deps
    if opt.toolchain then
        if package:is_toplevel() and not package:is_toolchain()then
            return
        end
    end

    -- init urls source
    package:_init_source()

    -- select package version
    local version, source = _select_package_version(package, requireinfo, locked_requireinfo)
    if version then
        package:data_set("__locked_requireinfo", locked_requireinfo)
    end

    -- get package key
    local packagekey = _get_packagekey(packagename, requireinfo, version)

    -- get package from cache first
    local package_cached = _memcache():get2("packages", packagekey)
    if package_cached then
        -- since toplevel is not part of packagekey, we need to ensure it's part of the cached package table too
        if requireinfo.is_toplevel and not package_cached:is_toplevel() then
            package_cached:requireinfo().is_toplevel = true
        end
        -- mark this cached package.requireinfo as override
        -- @see https://github.com/xmake-io/xmake/issues/4078
        if requireinfo.override then
            package_cached:requireinfo().override = true
        end
        return package_cached
    end

    -- save display name
    if not displayname then
        local packageid = _memcache():get2("packageids", packagename)
        displayname = packagename
        if packageid then
            displayname = displayname .. "#" .. tostring(packageid)
        end
        _memcache():set2("packageids", packagename, (packageid or 0) + 1)
    end
    package:displayname_set(displayname)

    -- disable parallelize if the package cache directory conflicts
    local cachedirs = _memcache():get2("cachedirs", package:cachedir())
    if cachedirs then
        package:set("parallelize", false)
    end
    _memcache():set2("cachedirs", package:cachedir(), true)

    -- add some builtin configurations to package
    _add_package_configurations(package)

    -- check package configurations
    _check_package_configurations(package)

    -- we need to check package toolchains before on_load and select runtimes,
    -- because we will call compiler-specific apis in on_load/on_fetch/find_package ..
    --
    -- @see https://github.com/xmake-io/xmake/pull/5466
    -- https://github.com/xmake-io/xmake/issues/4596#issuecomment-2014528801
    _check_package_toolchains(package)

    -- we need to select package runtimes before computing buildhash
    -- @see https://github.com/xmake-io/xmake/pull/4630#issuecomment-1910216561
    _select_package_runtimes(package)

    -- pre-compute the package buildhash
    package:_compute_buildhash()

    -- save artifacts info, we need to add it at last before buildhash need depend on package configurations
    -- it will switch to install precompiled binary package from xmake-mirror/build-artifacts
    if from_repo and not option.get("build") and not requireinfo.build and requireinfo.system ~= true then
        local artifacts_manifest = repository.artifacts_manifest(packagename, version)
        if artifacts_manifest then
            _select_artifacts(package, artifacts_manifest)
        end
    end

    -- we need to check package toolchains before on_load,
    -- because we will call compiler-specific apis in on_load/on_fetch/find_package ..
    --
    -- @see https://github.com/xmake-io/xmake/pull/5466
    -- https://github.com/xmake-io/xmake/issues/4596#issuecomment-2014528801
    _check_package_toolchains(package)

    -- do load
    package:_load()

    -- check api
    check_api(package, {load = true})

    -- save this package package to cache
    _memcache():set2("packages", packagekey, package)

    -- load ok
    package:_mark_as_loaded()
    return package
end

-- load all required packages
function _load_packages(requires, opt)

    -- no requires?
    if not requires or #requires == 0 then
        return {}
    end

    -- load packages
    local packages = {}
    local packages_nodeps = {}
    for _, requireitem in ipairs(load_requires(requires, opt.requires_extra, opt)) do

        -- load package
        local requireinfo = requireitem.info
        local requirepath = opt.requirepath and (opt.requirepath .. "." .. requireitem.name) or requireitem.name
        local package = _load_package(requireitem.name, requireinfo, table.join(opt, {requirepath = requirepath}))

        -- maybe package not found and optional
        if package then

            -- load dependent packages and save them first of this package
            if not package._DEPS then
                if package:get("deps") and opt.nodeps ~= true then

                    -- load dependent packages and do not load system/3rd packages for package/deps()
                    local _, plaindeps = _load_packages(package:get("deps"), {requirepath = requirepath,
                                                        requires_extra = package:extraconf("deps") or {},
                                                        parentinfo = requireinfo,
                                                        nodeps = opt.nodeps,
                                                        resolvedinfo = opt.resolvedinfo,
                                                        system = false})
                    for _, dep in ipairs(plaindeps) do
                        dep:parents_add(package)
                    end
                    package._PLAINDEPS = plaindeps
                    package._ORDERDEPS = table.unique(_sort_packagedeps(package))
                    package._LIBRARYDEPS = table.reverse_unique(_sort_librarydeps(package))
                    package._LIBRARYDEPS_WITH_PRIVATE = table.reverse_unique(_sort_librarydeps(package, {private = true}))
                    -- we always need load dependencies everytime
                    -- @see https://github.com/xmake-io/xmake/issues/4522
                    local packagedeps = {}
                    for _, dep in ipairs(package._ORDERDEPS) do
                        table.insert(packages, dep)
                        packagedeps[dep:name()] = dep
                    end
                    package._DEPS = packagedeps
                end
            end

            -- save this package
            table.insert(packages, package)
            table.insert(packages_nodeps, package)
        end
    end
    return packages, packages_nodeps
end

-- get package parents string
function _get_parents_str(package)
    local parents = package:parents()
    if parents then
        local parentnames = {}
        for _, parent in ipairs(parents) do
            table.insert(parentnames, parent:displayname())
        end
        if #parentnames == 0 then
            return
        end
        return table.concat(parentnames, ",")
    end
end

-- get require paths
function _get_requirepaths(package)
    local requirepaths = {}
    local parents = package:parents()
    if parents and #parents > 0 then
        for _, parent in ipairs(parents) do
            for _, requirepath in ipairs(_get_requirepaths(parent)) do
                table.insert(requirepaths, requirepath .. "." .. package:name())
            end
        end
        -- we also need to resolve requires conflict in toplevel, if `package.sync_requires_to_deps` policy is enabled.
        -- @see https://github.com/xmake-io/xmake/issues/5745#issuecomment-2513951471
        if project.policy("package.sync_requires_to_deps") then
            table.insert(requirepaths, package:name())
        end
    else
        table.insert(requirepaths, package:name())
    end
    return requirepaths
end

-- get compatibility key
function _get_package_compatkey(dep)
    local key = dep:plat() .. "/" .. dep:arch() .. "/" .. (dep:kind() or "")
    if dep:is_system() then
        key = key .. "/system"
    end
    local resolve_depconflict = project.policy("package.resolve_depconflict")
    if resolve_depconflict == nil then
        resolve_depconflict = dep:policy("package.resolve_depconflict")
    end
    if resolve_depconflict == false then
        if dep:version_str() then
            key = key .. "/" .. dep:version_str()
        end
        local configs = dep:requireinfo().configs
        if configs then
            local configs_order = {}
            for k, v in pairs(configs) do
                local v = v
                if type(v) == "table" then
                    v = string.serialize(v, {strip = true, indent = false, orderkeys = true})
                end
                table.insert(configs_order, k .. "=" .. tostring(v))
            end
            table.sort(configs_order)
            key = key .. ":" .. string.serialize(configs_order, true)
        end
    end
    return key
end

-- get package compatibility info
function _get_package_compatinfo(package)
    local compatinfo = {name = package:name()}
    if package:data("__locked_requireinfo") then
        compatinfo.locked = true
        compatinfo.versions = hashset.from(table.wrap(package:version_str()))
        return compatinfo
    end

    -- get compatible version range
    local requireinfo = package:requireinfo()
    local require_version = requireinfo.version
    if require_version then
        compatinfo.require_version = require_version
        if semver.is_valid(require_version) or semver.is_valid_range(require_version) then
            local versions = hashset.new()
            for _, version in ipairs(package:versions()) do
                if semver.satisfies(version, require_version) then
                    versions:insert(version)
                end
            end
            compatinfo.versions = versions
        elseif require_version == "latest" then
            compatinfo.versions = hashset.from(table.wrap(package:versions()))
        else
            compatinfo.versions = hashset.from(table.wrap(require_version))
        end
    else
        compatinfo.versions = hashset.from(table.wrap(package:versions()))
    end
    return compatinfo
end

-- check and resolve package conflicts
function _check_and_resolve_package_depconflicts_impl(package, name, deps, resolvedinfo)

    -- check version compatibility
    local versions
    for _, dep in ipairs(deps) do
        local compatinfo = _get_package_compatinfo(dep)
        assert(compatinfo.versions)

        if versions then
            for version in versions:items() do
                if not compatinfo.versions:has(version) then
                    versions:remove(version)
                end
            end
        else
            versions = compatinfo.versions
        end
    end

    if not versions or versions:empty() then
        print("package(%s): add_deps(%s, ...)", package:name(), name)
        for idx, dep in ipairs(deps) do
            cprint("  ${color.warning}->${clear} %s %s ${dim}%s",
                dep:displayname(), dep:version_str() or "", get_configs_str(dep))
        end
        print("we can use add_requireconfs(\"**.%s\", {override = true, version = \"x.x.x\"}) to override version.", name)
        raise("package(%s): conflict version dependencies!", name)
    end

    -- check configs compatibility
    local prevkey
    local configs
    local configs_conflict = false
    for _, dep in ipairs(deps) do
        local key = _get_package_compatkey(dep)
        if prevkey then
            if prevkey ~= key then
                configs_conflict = true
                break
            end
        else
            prevkey = key
        end
        local depconfigs = dep:requireinfo().configs
        if configs and depconfigs then
            for k, v in pairs(depconfigs) do
                local oldv = configs[k]
                if oldv ~= nil then
                    local v_key = tostring(v)
                    local oldv_key = tostring(oldv)
                    if type(v) == "table" then
                        v_key = string.serialize(v, {strip = true, indent = false, orderkeys = true})
                    end
                    if type(oldv) == "table" then
                        oldv_key = string.serialize(oldv, {strip = true, indent = false, orderkeys = true})
                    end
                    if v_key ~= oldv_key then
                        configs_conflict = true
                        break
                    end
                else
                    configs[k] = v
                end
            end
        else
            configs = depconfigs
        end
    end
    if configs_conflict then
        print("package(%s): add_deps(%s, ...)", package:name(), name)
        for idx, dep in ipairs(deps) do
            cprint("  ${color.warning}->${clear} %s %s ${dim}%s",
                dep:displayname(), dep:version_str() or "", get_configs_str(dep))
        end
        print("we can use add_requireconfs(\"**.%s\", {override = true, configs = {}}) to override configs.", name)
        raise("package(%s): conflict configs dependencies!", name)
    end

    -- resolve compatible version for all deps
    if versions and not versions:empty() then
        local version_best
        for version in versions:items() do
            if version_best == nil or semver.compare(version, version_best) > 0 then
                version_best = version
            end
        end
        if version_best then
            for _, dep in ipairs(deps) do
                for _, requirepath in ipairs(_get_requirepaths(dep)) do
                    resolvedinfo[requirepath] = {version = version_best}
                end
            end
        end
    end

    -- resolve configs
    if configs then
        for _, dep in ipairs(deps) do
            for _, requirepath in ipairs(_get_requirepaths(dep)) do
                resolvedinfo[requirepath] = resolvedinfo[requirepath] or {}
                resolvedinfo[requirepath].configs = configs
            end
        end
    end
end

-- check and resolve dependencies conflicts
--
-- It exists conflict for dependent packages for each root packages? resolve it first
-- e.g.
-- add_requires("foo") -> bar -> zlib 1.2.10
--                     -> xyz -> zlib 1.2.11 or other configs
--
-- add_requires("ddd") -> zlib
--
-- We assume that there is no conflict between `foo` and `ddd`.
--
-- Of course, conflicts caused by `add_packages("foo", "ddd")`
-- cannot be detected at present and can only be resolved by the user
--
function _check_and_resolve_package_depconflicts(package, resolvedinfo)
    local some_packagedeps = {}
    for _, dep in ipairs(package:librarydeps()) do
        local deps = some_packagedeps[dep:name()]
        if deps == nil then
            deps = {}
            some_packagedeps[dep:name()] = deps
        end
        table.insert(deps, dep)
    end
    for name, deps in pairs(some_packagedeps) do
        if #deps > 1 then
            _check_and_resolve_package_depconflicts_impl(package, name, deps, resolvedinfo)
        end
    end
end

-- must depend on the given package?
function _must_depend_on(package, dep)
    local manifest = package:manifest_load()
    if manifest and manifest.librarydeps then
        local librarydeps = hashset.from(manifest.librarydeps)
        return librarydeps:has(dep:name())
    end
    -- If we mark it as public, even if binary package is already installed,
    -- we need also to install it's public dep and export the it's envs.
    --
    -- @see https://github.com/xmake-io/xmake-repo/pull/6207
    -- https://github.com/xmake-io/xmake/pull/6101
    if package:is_binary() and package:extraconf("deps", dep:name(), "public") then
        return true
    end
end

-- compatible with all previous link dependencies?
-- @see https://github.com/xmake-io/xmake/issues/2719
function _compatible_with_previous_librarydeps(package, opt)

    -- skip to check compatibility if installation has been finished
    opt = opt or {}
    if opt.install_finished then
        return true
    end

    -- check strict compatibility for librarydeps?
    local strict_compatibility = project.policy("package.librarydeps.strict_compatibility")
    if strict_compatibility == nil then
        strict_compatibility = package:policy("package.librarydeps.strict_compatibility")
    end
    -- and we can disable it manually, @see https://github.com/xmake-io/xmake/pull/3738
    if strict_compatibility == false then
       return true
    end

    -- compute the buildhash for current librarydeps
    local depnames = hashset.new()
    local depinfos_curr = {}
    for _, dep in ipairs(package:librarydeps()) do
        if strict_compatibility or dep:policy("package.strict_compatibility") then
            depinfos_curr[dep:name()] = {
                version = dep:version_str(),
                buildhash = dep:buildhash()
            }
            depnames:insert(dep:name())
        end
    end

    -- compute the buildhash for previous librarydeps
    local depinfos_prev = {}
    local manifest = package:manifest_load()
    if manifest and manifest.librarydeps then
        local deps = manifest.deps or {}
        for _, depname in ipairs(manifest.librarydeps) do
            if strict_compatibility or (package:dep(depname) and package:dep(depname):policy("package.strict_compatibility")) or depinfos_curr[depname] then
                local depinfo = deps[depname]
                if depinfo and depinfo.buildhash then
                    depinfos_prev[depname] = depinfo
                    depnames:insert(depname)
                end
            end
        end
    end

    -- no any dependencies
    if depnames:empty() then
        return true
    end

    -- is compatible?
    local is_compatible = true
    local compatible_tips = {}
    for _, depname in depnames:keys() do
        local depinfo_prev = depinfos_prev[depname]
        local depinfo_curr = depinfos_curr[depname]
        if depinfo_prev and depinfo_curr then
            if depinfo_prev.buildhash ~= depinfo_curr.buildhash then
                is_compatible = false
                table.insert(compatible_tips, ("*%s"):format(depname))
            end
        elseif depinfo_prev then
            is_compatible = false
            table.insert(compatible_tips, ("-%s"):format(depname))
        elseif depinfo_curr then
            is_compatible = false
            table.insert(compatible_tips, ("+%s"):format(depname))
        end
    end
    if not is_compatible and #compatible_tips > 0 then
        package:data_set("librarydeps.compatible_tips", compatible_tips)
    end
    if not is_compatible then
        package:data_set("force_reinstall", true)
    end
    return is_compatible
end

-- get possible package
function get_possible_package(packagename)
    local packages_possible = search_packages("*", {description = false})
    if packages_possible then
        packages_possible = packages_possible["*"]
    end
    local result
    local distance_min
    for name, info in pairs(packages_possible) do
        local distance = packagename:levenshtein(info.name)
        if (distance_min == nil or distance < distance_min) and distance < 5 then
            distance_min = distance
            result = info
        end
    end
    return result
end

-- the cache directory
function cachedir()
    return path.join(global.directory(), "cache", "packages")
end

-- this package should be install?
function should_install(package, opt)
    opt = opt or {}
    if package:is_template() then
        return false
    end
    if not opt.install_finished and package:policy("package.install_always") then
        return true
    end
    if package:exists() and _compatible_with_previous_librarydeps(package, opt) then
        return false
    end
    -- we don't need to install it if this package only need to be fetched
    if package:is_fetchonly() then
        return false
    end
    -- only get system package? e.g. add_requires("xxx", {system = true})
    local requireinfo = package:requireinfo()
    if requireinfo and requireinfo.system then
        return false
    end
    if package:parents() then
        -- if all the packages that depend on it already exist, then there is no need to install it
        for _, parent in ipairs(package:parents()) do
            if should_install(parent, opt) and not parent:exists() then
                return true
            end

            -- if the existing parent package is already using it,
            -- then even if it is an optional package, you must make sure to install it
            --
            -- @see https://github.com/xmake-io/xmake/issues/1460
            --
            if parent:exists() and not option.get("force") and _must_depend_on(parent, package) then
                -- mark this package as non-optional because parent package need it
                local requireinfo = package:requireinfo()
                if requireinfo.optional then
                    requireinfo.optional = nil
                end
                return true
            end
        end
    else
        return true
    end
end

-- get package configs string
function get_configs_str(package)
    local configs = {}
    if package:is_optional() then
        table.insert(configs, "optional")
    end
    if package:is_private() then
        table.insert(configs, "private")
    end
    if package:is_host() then
        table.insert(configs, "host")
    end
    local requireinfo = package:requireinfo()
    if requireinfo then
        if requireinfo.plat then
            table.insert(configs, requireinfo.plat)
        end
        if requireinfo.arch then
            table.insert(configs, requireinfo.arch)
        end
        if requireinfo.kind then
            table.insert(configs, requireinfo.kind)
        end
        local ignored_configs_for_buildhash = hashset.from(requireinfo.ignored_configs_for_buildhash or {})
        local configs_overrided = requireinfo.configs_overrided or {}
        for k, v in pairs(requireinfo.configs) do
            local v = v
            if not ignored_configs_for_buildhash:has(k) then
                v = configs_overrided[k] or v
                if type(v) == "boolean" then
                    table.insert(configs, k .. ":" .. (v and "y" or "n"))
                else
                    table.insert(configs, k .. ":" .. string.serialize(v, {strip = true, indent = false}))
                end
            end
        end
    end
    local compatible_tips = package:data("librarydeps.compatible_tips")
    if compatible_tips then
        table.insert(configs, "deps:" .. table.concat(compatible_tips, ","))
    end
    local parents_str = _get_parents_str(package)
    if parents_str then
        table.insert(configs, "from:" .. parents_str)
    end
    local license = package:get("license")
    if license then
        table.insert(configs, "license:" .. license)
    end
    local configs_str = #configs > 0 and "[" .. table.concat(configs, ", ") .. "]" or ""
    local limitwidth = math.floor(os.getwinsize().width * 2 / 3)
    if #configs_str > limitwidth then
        configs_str = configs_str:sub(1, limitwidth) .. " ..)"
    end
    local resolvedinfo = requireinfo.resolvedinfo
    if resolvedinfo then
        configs_str = configs_str .. " " .. "${color.warning}(conflict resolved)${clear}"
    end
    return configs_str
end

-- get locked requireinfo
function get_locked_requireinfo(requireinfo, opt)
    local requirekey = requireinfo.requirekey
    local locked_requireinfo, requireslock_version
    if _has_locked_requires(opt) and requirekey then
        locked_requireinfo, requireslock_version = _get_locked_requires(requirekey, opt)
        if requireslock_version and semver.compare(project.requireslock_version(), requireslock_version) < 0 then
            locked_requireinfo = nil
        end
    end
    return locked_requireinfo, requireslock_version
end

-- load requires
function load_requires(requires, requires_extra, opt)
    opt = opt or {}
    local requireitems = {}
    for _, require_str in ipairs(requires) do
        local packagename, requireinfo = _load_require(require_str, requires_extra, opt)
        table.insert(requireitems, {name = packagename, info = requireinfo})
    end
    return requireitems
end

-- load all required packages
function load_packages(requires, opt)
    opt = opt or {}
    local unique = {}
    local packages = {}
    local resolvedinfo = {}
    for _, package in ipairs((_load_packages(requires, table.clone(opt)))) do
        if package:is_toplevel() then
            _check_and_resolve_package_depconflicts(package, resolvedinfo)
        end
        local key = _get_packagekey(package:name(), package:requireinfo())
        if not unique[key] then
            table.insert(packages, package)
            unique[key] = true
        end
    end

    -- we need to reload packages with new resolved deps if there are some dep conflicts
    if not table.empty(resolvedinfo) then
        for _, package in ipairs(packages) do
            local installdir = package:installdir({readonly = true})
            if os.isdir(installdir) and os.emptydir(installdir) then
                os.tryrm(installdir, {emptydirs = true})
            end
        end
        unique = {}
        packages = {}
        _memcache():clear()
        opt = table.join(opt, {resolvedinfo = resolvedinfo})
        for _, package in ipairs((_load_packages(requires, opt))) do
            local key = _get_packagekey(package:name(), package:requireinfo())
            if not unique[key] then
                table.insert(packages, package)
                unique[key] = true
            end
        end
    end
    return packages
end

