--!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 scheme.lua -- -- @ref https://github.com/xmake-io/xmake/issues/7184 -- @note This module provides scheme management for packages, -- allowing custom download schemes and configurations -- to be defined and applied to package management. -- define module local scheme = scheme or {} local _instance = _instance or {} -- load modules local os = require("base/os") local io = require("base/io") local path = require("base/path") local utils = require("base/utils") local table = require("base/table") local option = require("base/option") local hashset = require("base/hashset") local scopeinfo = require("base/scopeinfo") local interpreter = require("base/interpreter") local language = require("language/language") local sandbox = require("sandbox/sandbox") -- new an instance function _instance.new(name, opt) opt = opt or {} local instance = table.inherit(_instance) instance._NAME = name instance._INFO = scopeinfo.new("scheme", {}, {interpreter = scheme._interpreter()}) instance._PACKAGE = opt.package return instance end -- get the scheme name -- -- @return the scheme name string -- function _instance:name() return self._NAME end -- get the instance type -- -- @return "scheme" -- function _instance:type() return "scheme" end -- is the default scheme? -- -- @return true if default -- function _instance:is_default() return self:name() == "__default__" end -- is precompiled binary scheme? -- -- @return true if precompiled -- function _instance:is_precompiled() return self:name() == "__precompiled__" end -- get the associated package -- -- @return the package instance -- function _instance:package() return self._PACKAGE end -- get the scheme configuration value -- -- @param name the config name -- @return the config value -- function _instance:get(name) local value = self._INFO:get(name) if value == nil and self:is_default() and self:package() then value = self:package():get(name) end return value end -- set the value to scheme info -- -- @param name the info name -- @param ... the values -- function _instance:set(name, ...) self._INFO:apival_set(name, ...) end -- add the value to scheme info function _instance:add(name, ...) self._INFO:apival_add(name, ...) end -- get the extra configuration function _instance:extraconf(name, item, key) local value = self._INFO:extraconf(name, item, key) if value == nil and self:is_default() and self:package() then value = self:package():extraconf(name, item, key) end return value end -- set the extra configuration function _instance:extraconf_set(name, item, key, value) return self._INFO:extraconf_set(name, item, key, value) end -- get the source urls -- -- @return the urls array -- function _instance:urls() local urls = self._URLS if urls == nil then urls = table.wrap(self:get("urls")) if #urls == 1 and urls[1] == "" then urls = {} end end return urls end -- set urls function _instance:urls_set(urls) self._URLS = urls end -- get the alias of url, @note need raw url function _instance:url_alias(url) return self:extraconf("urls", url, "alias") end -- get the version filter of url, @note need raw url function _instance:url_version(url) return self:extraconf("urls", url, "version") end -- get the excludes paths of url -- @note it supports the path pattern, but it only supports for archiver. function _instance:url_excludes(url) return self:extraconf("urls", url, "excludes") end -- get the includes paths of url -- @note it does not support the path pattern, and it only supports for git url now. -- @see https://github.com/xmake-io/xmake/issues/6071 function _instance:url_includes(url) return self:extraconf("urls", url, "includes") end -- get the http headers of url, @note need raw url function _instance:url_http_headers(url) return self:extraconf("urls", url, "http_headers") end -- get the script directory function _instance:scriptdir() local scriptdir = self._SCRIPTDIR if not scriptdir then if self:package() then scriptdir = self:package():scriptdir() end self._SCRIPTDIR = scriptdir end return scriptdir end -- get versions function _instance:versions() if self._VERSIONS == nil then -- we need to sort the build number in semver list -- https://github.com/xmake-io/xmake/issues/6953 local versions = {} for version, _ in table.orderpairs(self:_versions_list()) do local version = version -- remove the url alias prefix if exists local pos = version:find(':', 1, true) if pos then version = version:sub(pos + 1, -1) end table.insert(versions, version) end self._VERSIONS = table.unique(versions) end return self._VERSIONS end -- get versions list function _instance:_versions_list() if self._VERSIONS_LIST == nil then local versions = table.wrap(self:get("versions")) local versionfiles = self:get("versionfiles") if versionfiles then for _, versionfile in ipairs(table.wrap(versionfiles)) do local versionfile = versionfile if not path.is_absolute(versionfile) then local subpath = versionfile versionfile = path.join(self:scriptdir(), subpath) if not os.isfile(versionfile) and self:package() and self:package():base() then versionfile = path.join(self:package():base():scriptdir(), subpath) end end if os.isfile(versionfile) then local list = io.readfile(versionfile) for _, line in ipairs(list:split("\n")) do local splitinfo = line:split("%s+") if #splitinfo == 2 then local version = splitinfo[1] local shasum = splitinfo[2] versions[version] = shasum end end end end end self._VERSIONS_LIST = versions end return self._VERSIONS_LIST end -- set versions list function _instance:_versions_list_set(versions_list) self._VERSIONS_LIST = versions_list end -- get version function _instance:version() return self._VERSION end -- get version string function _instance:version_str() local package = self:package() if package and package:is_thirdparty() then local requireinfo = package:requireinfo() if requireinfo then return requireinfo.version end end return self._VERSION_STR end -- set version function _instance:version_set(version, source) -- save the semver version local sv = semver.new(version) if sv then self._VERSION = sv end -- save branch and tag if source == "branch" then self._BRANCH = version elseif source == "tag" then self._TAG = version elseif source == "commit" then self._COMMIT = version end -- save version string if source == "commit" then -- we strip it to avoid long paths self._VERSION_STR = version:sub(1, 8) else self._VERSION_STR = version end end -- get branch version function _instance:branch() return self._BRANCH end -- get tag version function _instance:tag() return self._TAG end -- get commit version function _instance:commit() return self._COMMIT end -- is git ref? function _instance:gitref() return self:branch() or self:tag() or self:commit() end -- get hash of the source package for the url_alias@version_str function _instance:sourcehash(url_alias) local versions = self:_versions_list() local version_str = self:version_str() if versions and version_str then local sourcehash = nil if url_alias then sourcehash = versions[url_alias .. ":" ..version_str] end if not sourcehash then sourcehash = versions[version_str] end if sourcehash and #sourcehash == 40 then sourcehash = sourcehash:lower() end return sourcehash end end -- get revision(commit, tag, branch) of the url_alias@version_str, only for git url function _instance:revision(url_alias) local revision = self:sourcehash(url_alias) if revision and #revision <= 40 then -- it will be sha256 of tar/gz file, not commit number if longer than 40 characters return revision end end -- get the patches of the current version -- -- @code -- add_patches("6.7.6", "https://cdn.kernel.org/pub/linux/kernel/v6.x/patch-6.7.6.xz", -- "a394326aa325f8a930a4ce33c69ba7b8b454aef1107a4d3c2a8ae12908615fc4", {reverse = true}) -- @endcode -- function _instance:patches() local patches = self._PATCHES if patches == nil then local patchinfos = self:get("patches") if patchinfos then local version_str = self:version_str() local patchinfo = patchinfos[version_str] if patchinfo then patches = {} patchinfo = table.wrap(patchinfo) for idx = 1, #patchinfo, 2 do local extra = self:extraconf("patches." .. version_str, patchinfo[idx]) table.insert(patches , {url = patchinfo[idx], sha256 = patchinfo[idx + 1], extra = extra}) end else -- match semver, e.g add_patches(">=1.0.0", url, sha256) for range, patchinfo in pairs(patchinfos) do local patchinfo = patchinfo if semver.satisfies(version_str, range) then patches = patches or {} patchinfo = table.wrap(patchinfo) for idx = 1, #patchinfo, 2 do local extra = self:extraconf("patches." .. range, patchinfo[idx]) table.insert(patches , {url = patchinfo[idx], sha256 = patchinfo[idx + 1], extra = extra}) end end end end end self._PATCHES = patches or false end return patches and patches or nil end -- get the resources of the current version function _instance:resources() local resources = self._RESOURCES if resources == nil then local resourceinfos = self:get("resources") if resourceinfos then local version_str = self:version_str() local resourceinfo = resourceinfos[version_str] if resourceinfo then resources = {} resourceinfo = table.wrap(resourceinfo) for idx = 1, #resourceinfo, 3 do local name = resourceinfo[idx] resources[name] = {url = resourceinfo[idx + 1], sha256 = resourceinfo[idx + 2]} end else -- match semver, e.g add_resources(">=1.0.0", name, url, sha256) for range, resourceinfo in pairs(resourceinfos) do local resourceinfo = resourceinfo if semver.satisfies(version_str, range) then resources = resources or {} resourceinfo = table.wrap(resourceinfo) for idx = 1, #resourceinfo, 3 do local name = resourceinfo[idx] resources[name] = {url = resourceinfo[idx + 1], sha256 = resourceinfo[idx + 2]} end end end end end self._RESOURCES = resources or false end return resources and resources or nil end -- interpreter function scheme._interpreter() local interp = scheme._INTERPRETER if not interp then interp = interpreter.new() interp:api_define(scheme.apis()) scheme._INTERPRETER = interp end return interp end -- get scheme apis function scheme.apis() return { values = { -- scheme.set_xxx "scheme.set_urls", -- scheme.add_xxx "scheme.add_urls" }, script = { -- scheme.on_xxx "scheme.on_download" , "scheme.on_install" , "scheme.on_test" }, keyvalues = { -- scheme.add_xxx "scheme.add_patches" , "scheme.add_resources" }, paths = { -- scheme.add_xxx "scheme.add_versionfiles" }, dictionary = { -- scheme.add_xxx "scheme.add_versions" } } end -- new scheme function scheme.new(name, opt) return _instance.new(name, opt) end -- return module return scheme