--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, TBOOX Open Source Group. -- -- @author ruki -- @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.repository") import("private.action.require.impl.utils.requirekey", {alias = "_get_requirekey"}) -- get memcache function _memcache() return memcache.cache("require.impl.package") end -- -- parse require string -- -- basic -- - add_requires("zlib") -- -- semver -- - add_requires("tbox >=1.5.1", "zlib >=1.2.11") -- - add_requires("tbox", {version = ">=1.5.1"}) -- -- git branch/tag -- - add_requires("zlib master") -- -- with the given repository -- - add_requires("xmake-repo@tbox >=1.5.1") -- -- with the given configs -- - add_requires("aaa_bbb_ccc >=1.5.1 <1.6.0", {optional = true, alias = "mypkg", debug = true}) -- - add_requires("tbox", {config = {coroutine = true, abc = "xxx"}}) -- -- with namespace and the 3rd package manager -- - add_requires("xmake::xmake-repo@tbox >=1.5.1") -- - add_requires("vcpkg::ffmpeg") -- - add_requires("conan::OpenSSL/1.0.2n@conan/stable") -- - add_requires("conan::openssl/1.1.1g") -- new -- - add_requires("brew::pcre2/libpcre2-8 10.x", {alias = "pcre2"}) -- -- clone as a standalone package with the different configs -- we can install and use these three packages at the same time. -- - add_requires("zlib") -- - add_requires("zlib~debug", {debug = true}) -- - add_requires("zlib~shared", {configs = {shared = true}, alias = "zlib_shared"}) -- -- - add_requires("zlib~label1") -- - add_requires("zlib", {label = "label2"}) -- -- private package, only for installation, do not export any links/includes and environments to target -- - add_requires("zlib", {private = true}) -- -- {system = nil/true/false}: -- nil: get remote or system packages -- true: only get system package -- false: only get remote packages -- -- {build = true}: always build packages, we do not use the precompiled artifacts -- function _parse_require(require_str) -- split package and version info local splitinfo = require_str:split('%s+') assert(splitinfo and #splitinfo > 0, "require(\"%s\"): invalid!", require_str) -- get package info local packageinfo = splitinfo[1] -- get version -- -- e.g. -- -- latest -- >=1.5.1 <1.6.0 -- master || >1.4 -- ~1.2.3 -- ^1.1 -- local version = "latest" if #splitinfo > 1 then version = table.concat(table.slice(splitinfo, 2), " ") end assert(version, "require(\"%s\"): unknown version!", require_str) -- require third-party packages? e.g. brew::pcre2/libpcre2-8 local reponame = nil local packagename = nil if require_str:find("::", 1, true) then packagename = packageinfo else -- get repository name, package name and package url local pos = packageinfo:lastof('@', true) if pos then packagename = packageinfo:sub(pos + 1) reponame = packageinfo:sub(1, pos - 1) else packagename = packageinfo end end -- check package name assert(packagename, "require(\"%s\"): the package name not found!", require_str) return packagename, version, reponame end -- load require info function _load_require(require_str, requires_extra, parentinfo) -- parse require local packagename, version, reponame = _parse_require(require_str) -- get require extra local require_extra = {} if requires_extra then require_extra = requires_extra[require_str] or {} 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 -- require packge in the current host platform if require_extra.host then if is_subhost(core_package.targetplat()) and os.subarch() == core_package.targetarch() then -- we need to pass plat/arch to avoid repeat installation -- @see https://github.com/xmake-io/xmake/issues/1579 else require_extra.plat = os.subhost() require_extra.arch = os.subarch() end 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") 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 = {} parentinfo = parentinfo or {} required.packagename = packagename required.requireinfo = { originstr = require_str, reponame = reponame, version = require_extra.version or version, 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 } 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 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 return version, source end -- has git url? local has_giturl = false for _, url in ipairs(package: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 if (not package:get("versions") 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 #package:versions() > 0 then -- select version? version, source = try { function () return semver.select(require_version, package: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 #package:urls() == 0 then version = "latest" source = "version" end if not version and not package:is_thirdparty() then 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()) 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) -- we need to synchronise the plat/arch inherited from the parent package as early as possible if requireinfo.plat then package:plat_set(requireinfo.plat) end if requireinfo.arch then package:arch_set(requireinfo.arch) end 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 if not table.contains(runtimes, "MT", "MD", "MTd", "MDd") then table.insert(runtimes, "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 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, 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 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:artifacts_set(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()):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 trailng ~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) 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 assert(package, "package(%s) not found!", packagename) -- 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:version_set(version, source) 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 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() -- load all components for _, component in pairs(package:components()) do component:_load() end -- load environments from the manifest to enable the environments of on_install() package:envs_load() -- 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, 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 -- check 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_package_depconflicts(package) local packagekeys = {} for _, dep in ipairs(package:librarydeps()) do local key = _get_packagekey(dep:name(), dep:requireinfo()) local prevkey = packagekeys[dep:name()] if prevkey then assert(key == prevkey, "package(%s): conflict dependencies with package(%s) in %s!", key, prevkey, package:name()) else packagekeys[dep:name()] = key 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 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 -- 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 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 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 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 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.parentinfo) 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 = {} for _, package in ipairs((_load_packages(requires, opt))) do if package:is_toplevel() then _check_package_depconflicts(package) end local key = _get_packagekey(package:name(), package:requireinfo()) if not unique[key] then table.insert(packages, package) unique[key] = true end end return packages end