summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2026-08-14 23:28:20 +0800
committerruki <[email protected]>2026-08-14 23:28:20 +0800
commita79cf74f920c088528d66a67a59b3d8eaa1d4f7a (patch)
treedca4cb0ca1fd210e04e8c59ffd9ab8c410747c1f
parent6c246c12a09524780e5bfdc53bc27a892e4793a9 (diff)
autofetch addons
-rw-r--r--tests/actions/addon/projects/autofetch-badinclude/xmake-addons.lua2
-rw-r--r--tests/actions/addon/projects/autofetch-badinclude/xmake.lua2
-rw-r--r--tests/actions/addon/projects/autofetch-badname/xmake-addons.lua2
-rw-r--r--tests/actions/addon/projects/autofetch-badname/xmake.lua2
-rw-r--r--tests/actions/addon/projects/autofetch/xmake-addons.lua1
-rw-r--r--tests/actions/addon/projects/autofetch/xmake.lua5
-rw-r--r--tests/actions/addon/test.lua58
-rw-r--r--xmake/core/base/interpreter.lua38
-rw-r--r--xmake/core/base/task.lua6
-rw-r--r--xmake/core/package/addon.lua11
-rw-r--r--xmake/core/project/addons.lua188
-rw-r--r--xmake/core/project/project.lua73
-rw-r--r--xmake/core/sandbox/modules/import/core/project/addons.lua48
-rw-r--r--xmake/modules/private/action/require/impl/install_packages.lua2
-rw-r--r--xmake/modules/private/action/require/impl/package.lua3
-rw-r--r--xmake/modules/private/addons/main.lua92
16 files changed, 532 insertions, 1 deletions
diff --git a/tests/actions/addon/projects/autofetch-badinclude/xmake-addons.lua b/tests/actions/addon/projects/autofetch-badinclude/xmake-addons.lua
new file mode 100644
index 000000000..6931855ae
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch-badinclude/xmake-addons.lua
@@ -0,0 +1,2 @@
+-- this file only declares the addons, it cannot reference them
+includes("@addon/custom-include/check")
diff --git a/tests/actions/addon/projects/autofetch-badinclude/xmake.lua b/tests/actions/addon/projects/autofetch-badinclude/xmake.lua
new file mode 100644
index 000000000..0d94cf61c
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch-badinclude/xmake.lua
@@ -0,0 +1,2 @@
+target("test")
+ set_kind("phony")
diff --git a/tests/actions/addon/projects/autofetch-badname/xmake-addons.lua b/tests/actions/addon/projects/autofetch-badname/xmake-addons.lua
new file mode 100644
index 000000000..165325fb7
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch-badname/xmake-addons.lua
@@ -0,0 +1,2 @@
+-- the `addon` name is reserved for the addon references
+add_addons("addon")
diff --git a/tests/actions/addon/projects/autofetch-badname/xmake.lua b/tests/actions/addon/projects/autofetch-badname/xmake.lua
new file mode 100644
index 000000000..0d94cf61c
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch-badname/xmake.lua
@@ -0,0 +1,2 @@
+target("test")
+ set_kind("phony")
diff --git a/tests/actions/addon/projects/autofetch/xmake-addons.lua b/tests/actions/addon/projects/autofetch/xmake-addons.lua
new file mode 100644
index 000000000..a85e0fe11
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch/xmake-addons.lua
@@ -0,0 +1 @@
+add_addons("custom-include")
diff --git a/tests/actions/addon/projects/autofetch/xmake.lua b/tests/actions/addon/projects/autofetch/xmake.lua
new file mode 100644
index 000000000..4eaca0785
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch/xmake.lua
@@ -0,0 +1,5 @@
+-- the addon is installed automatically, so we can use its includes file here
+includes("@addon/custom-include/check")
+
+target("test")
+ set_kind("phony")
diff --git a/tests/actions/addon/test.lua b/tests/actions/addon/test.lua
index 3542c907b..4db9c0ddf 100644
--- a/tests/actions/addon/test.lua
+++ b/tests/actions/addon/test.lua
@@ -114,6 +114,32 @@ function _config_project(content)
return _run_project(content, {"config", "-y"})
end
+-- copy the given fixture project to a temporary directory and run the given function in it
+--
+-- @note we cannot run them in place, they would generate the lock and the build files,
+-- @see tests/actions/addon/projects
+--
+function _with_project(name, func)
+ local projectdir = os.tmpfile() .. ".addon-project"
+ os.tryrm(projectdir)
+ os.mkdir(projectdir)
+ os.cp(path.join(os.scriptdir(), "projects", name, "*"), projectdir)
+ local oldir = os.cd(projectdir)
+ try
+ {
+ function ()
+ func(projectdir)
+ end,
+ finally
+ {
+ function ()
+ os.cd(oldir)
+ os.tryrm(projectdir)
+ end
+ }
+ }
+end
+
-- only the payloads should be installed, our own files should not
function test_install(t)
_with_addons({"custom-toolchain"}, function ()
@@ -223,6 +249,38 @@ target("test")
end)
end
+-- the addons which a project declares in `xmake-addons.lua` are installed automatically
+--
+-- @note they must be installed before loading the project, it may use their includes files
+function test_autofetch(t)
+ local recipes = {["custom-include"] = ("set_sourcedir(%q)"):format(_addondir("custom-include"))}
+ _with_repo(recipes, function ()
+ _remove("custom-include")
+ _with_project("autofetch", function (projectdir)
+
+ -- it should be installed when loading the project, so that its includes file can be found
+ t:require(os.iorunv("xmake", {"config", "-y"}):find("custom-include: includes check is loaded", 1, true))
+
+ -- and it should be locked
+ local lockfile = path.join(projectdir, "xmake-addons.lock")
+ t:require(os.isfile(lockfile))
+ t:require(io.load(lockfile)["custom-include"] ~= nil)
+
+ -- we should not install it again
+ t:require_not(os.iorunv("xmake", {"config", "-y"}):find("install custom-include", 1, true))
+ end)
+ end)
+end
+
+-- the addons file only declares the addons, it cannot reference them
+function test_autofetch_invalid(t)
+ for _, name in ipairs({"autofetch-badinclude", "autofetch-badname"}) do
+ _with_project(name, function ()
+ t:require_not(try { function () os.runv("xmake", {"config", "-y"}); return true end })
+ end)
+ end
+end
+
-- the addons can be installed from a repository, by plain name and by repo@name
function test_install_from_repo(t)
local recipes = {["custom-plugin"] = ("set_sourcedir(%q)"):format(_addondir("custom-plugin"))}
diff --git a/xmake/core/base/interpreter.lua b/xmake/core/base/interpreter.lua
index ede772de0..01b689fe6 100644
--- a/xmake/core/base/interpreter.lua
+++ b/xmake/core/base/interpreter.lua
@@ -884,6 +884,36 @@ end
--
-- the root api will affect these scopes
--
+-- get the root file name of the included directories, e.g. includes("subdir") -> subdir/xmake.lua
+function interpreter:includes_rootfilename()
+ return self._PRIVATE._INCLUDES_ROOTFILENAME or "xmake.lua"
+end
+
+-- set the root file name of the included directories
+--
+-- e.g. interp:includes_rootfilename_set("xmake-addons.lua") -> includes("subdir") -> subdir/xmake-addons.lua
+--
+function interpreter:includes_rootfilename_set(filename)
+ self._PRIVATE._INCLUDES_ROOTFILENAME = filename
+end
+
+-- can we include the referenced files? e.g. includes("@builtin/check"), includes("@addon/esp32/board")
+function interpreter:includes_references()
+ return self._PRIVATE._INCLUDES_REFERENCES ~= false
+end
+
+-- enable/disable the referenced files of includes()
+--
+-- @param enabled enable them or not
+-- @param hint the extra hint of the error message
+--
+-- @note the addons file is loaded before the addons are installed, so it cannot reference them
+--
+function interpreter:includes_references_set(enabled, hint)
+ self._PRIVATE._INCLUDES_REFERENCES = enabled
+ self._PRIVATE._INCLUDES_REFERENCES_HINT = hint
+end
+
function interpreter:rootscope_set(scope_kind)
assert(self and self._PRIVATE)
self._PRIVATE._ROOTSCOPE = scope_kind
@@ -1826,6 +1856,12 @@ function interpreter:api_builtin_includes(...)
local subpaths_matched = {}
for _, subpath in ipairs(subpaths) do
local found = false
+ -- the referenced files are not always available, e.g. the addons file
+ if subpath:startswith("@") and not self:includes_references() then
+ local hint = self._PRIVATE._INCLUDES_REFERENCES_HINT
+ os.raise("includes(%s): the referenced files are not supported in %s!%s",
+ subpath, path.filename(curfile), hint and ("\n" .. hint) or "")
+ end
-- attempt to find files from programdir/includes/*.lua
-- e.g. includes("@builtin/check")
if subpath:startswith("@builtin/") then
@@ -1848,7 +1884,7 @@ function interpreter:api_builtin_includes(...)
files = os.files(subpath)
else
-- @see https://github.com/xmake-io/xmake/issues/6026
- files = os.files(path.join(subpath, "xmake.lua"))
+ files = os.files(path.join(subpath, self:includes_rootfilename()))
end
if files and #files > 0 then
table.join2(subpaths_matched, files)
diff --git a/xmake/core/base/task.lua b/xmake/core/base/task.lua
index a901490eb..2cd1fa58a 100644
--- a/xmake/core/base/task.lua
+++ b/xmake/core/base/task.lua
@@ -426,6 +426,12 @@ function task._is_conflicting(taskname, taskfile, filepath)
return true
end
+-- clear the loaded tasks, e.g. some addons may be installed just now
+function task.clear()
+ task._TASKS = nil
+ task._DIRECTORIES = nil
+end
+
-- get all registered tasks
--
-- @return the tasks table {name = task, ...}
diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua
index 586a3ea15..6d56205fd 100644
--- a/xmake/core/package/addon.lua
+++ b/xmake/core/package/addon.lua
@@ -656,6 +656,17 @@ function addon.remove(name, opt)
return true
end
+-- reload the addons registry and the caches which are built from it
+--
+-- @note we need it if the addons have been installed by another process,
+-- e.g. the addons which a project declares, @see core/project/project.lua
+--
+function addon.reload()
+ addon._ADDONS = nil
+ addon._MANIFESTS = nil
+ addon._GLOBALMODULES = nil
+end
+
-- rescan the install directory and rebuild the registry
--
-- it's only used to repair the registry file, e.g. the user removed some addon directories manually
diff --git a/xmake/core/project/addons.lua b/xmake/core/project/addons.lua
new file mode 100644
index 000000000..994d52ec0
--- /dev/null
+++ b/xmake/core/project/addons.lua
@@ -0,0 +1,188 @@
+--!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 addons.lua
+--
+
+-- define module
+local addons = addons or {}
+
+-- load modules
+local os = require("base/os")
+local io = require("base/io")
+local path = require("base/path")
+local table = require("base/table")
+local addon = require("package/addon")
+
+-- the file which declares the addons of a project, e.g. <projectdir>/xmake-addons.lua
+--
+-- it's loaded before the project file, so that the addons are always installed when
+-- we load the project, e.g. includes("@addon/esp32-devel/board")
+--
+-- e.g.
+-- add_addons("esp32-devel 1.0.x")
+-- add_addons("serial-tools", {optional = true})
+--
+function addons.filename()
+ return "xmake-addons.lua"
+end
+
+function addons.file(projectdir)
+ return path.join(projectdir or os.projectdir(), addons.filename())
+end
+
+-- the lock file of the declared addons, e.g. <projectdir>/xmake-addons.lock
+--
+-- @note it's independent of `xmake-requires.lock`, the addons are always locked,
+-- they provide the build scripts and we should never change them silently
+--
+function addons.lockfile(projectdir)
+ return path.join(projectdir or os.projectdir(), "xmake-addons.lock")
+end
+
+-- get the apis of the addons file
+--
+-- @note it only declares which addons this project needs, the addon resources
+-- are always referenced from the project file, e.g. add_rules("@addon/esp32-devel/app")
+--
+function addons.apis()
+ return {
+ values = {
+ "add_addons"
+ }
+ }
+end
+
+-- the interpreter of the addons file
+function addons._interpreter()
+ local interp = addons._INTERPRETER
+ if interp == nil then
+ -- we need to load it lazily, the interpreter also depends on the addon module
+ local interpreter = require("base/interpreter")
+ interp = interpreter.new()
+ interp:api_define(addons.apis())
+ -- the sub-projects declare their addons in this file too,
+ -- e.g. includes("sub") -> sub/xmake-addons.lua
+ interp:includes_rootfilename_set(addons.filename())
+ -- and we cannot reference the addons here, they have not been installed yet,
+ -- e.g. includes("@addon/esp32-devel/board")
+ interp:includes_references_set(false, "please move it to the project file(xmake.lua)!")
+ addons._INTERPRETER = interp
+ end
+ return interp
+end
+
+-- load the declared addons of the given project directory
+--
+-- @return the addons information and errors, it will be nil if this project declares nothing,
+-- e.g. {addons = {"esp32-devel 1.0.x"}, addons_extra = {...}}
+--
+function addons.load(projectdir)
+ local filepath = addons.file(projectdir)
+ if not os.isfile(filepath) then
+ return
+ end
+
+ -- enter the project directory, the include paths are relative to it,
+ -- e.g. includes("sub")
+ local oldir, errors = os.cd(path.directory(filepath))
+ if not oldir then
+ return nil, errors
+ end
+
+ local rootinfo
+ local interp = addons._interpreter()
+ local ok, errors = interp:load(filepath)
+ if ok then
+ rootinfo, errors = interp:make("root", true, true)
+ end
+ os.cd(oldir)
+ if not rootinfo then
+ return nil, errors
+ end
+
+ local addonsinfo = {addons = table.wrap(rootinfo:get("addons")),
+ addons_extra = rootinfo:extraconf("addons")}
+
+ -- check the declared addons
+ local declared = {}
+ for _, requirestr in ipairs(addonsinfo.addons) do
+
+ -- this file is loaded before the addons are installed, so it cannot reference them
+ if requirestr:startswith("@") then
+ return nil, string.format("%s: cannot reference the addon resources(%s) here, please move it to the project file(xmake.lua)!",
+ filepath, requirestr)
+ end
+
+ local name = requirestr:split("%s")[1]
+ if name == "addon" or name == "self" then
+ return nil, string.format("%s: the addon name(%s) is reserved by xmake for the addon references, please rename it!",
+ filepath, name)
+ end
+ if name == "." or name == ".." or name:find("[/\\:]") then
+ return nil, string.format("%s: invalid addon name(%s)!", filepath, name)
+ end
+
+ -- we can only install one version of an addon for a project
+ if declared[name] then
+ return nil, string.format("%s: the addon(%s) is declared twice, e.g. `%s` and `%s`, please merge them!",
+ filepath, name, declared[name], requirestr)
+ end
+ declared[name] = requirestr
+ end
+ return addonsinfo
+end
+
+-- get the locked addons, e.g. {["esp32-devel"] = {version = "1.0.3"}}
+function addons.locked(projectdir)
+ local lockfile = addons.lockfile(projectdir)
+ if os.isfile(lockfile) then
+ local lockinfo = io.load(lockfile)
+ if lockinfo then
+ lockinfo.__meta__ = nil
+ return lockinfo
+ end
+ end
+end
+
+-- are all the declared addons installed already?
+--
+-- @note we need to check it in-process for every command which loads the project,
+-- so we only check the locked versions here, the installer will resolve them again
+--
+function addons.satisfied(addonsinfo, projectdir)
+ local locked = addons.locked(projectdir)
+ if not locked then
+ return false
+ end
+ local installed = addon.addons()
+ for _, requirestr in ipairs(addonsinfo.addons) do
+ local name = requirestr:split("%s")[1]
+ local lockinfo = locked[name]
+ if not lockinfo then
+ return false
+ end
+ local addoninfo = installed[addon.dirname(name)]
+ if not addoninfo or addoninfo.version ~= lockinfo.version then
+ return false
+ end
+ end
+ return true
+end
+
+-- return module
+return addons
diff --git a/xmake/core/project/project.lua b/xmake/core/project/project.lua
index 803bb5d9b..db269eda9 100644
--- a/xmake/core/project/project.lua
+++ b/xmake/core/project/project.lua
@@ -45,6 +45,8 @@ local option = require("project/option")
local policy = require("project/policy")
local project_package = require("project/package")
local deprecated_project = require("project/deprecated/project")
+local addon = require("package/addon")
+local addons = require("project/addons")
local package = require("package/package")
local platform = require("platform/platform")
local toolchain = require("tool/toolchain")
@@ -227,6 +229,69 @@ function project._api_add_toolchaindirs(interp, ...)
end
end
+-- install the addons which this project declares
+--
+-- @note we cannot install them here, we are loading the project, so we do it in a
+-- sub-process, @see xmake/modules/private/addons/main.lua
+--
+function project._install_addons()
+ -- @note we need to cache the result, the project may be loaded many times,
+ -- otherwise the failure would be ignored by the next load
+ if not project._ADDONS_CHECKED then
+ project._ADDONS_CHECKED = true
+ project._ADDONS_OK, project._ADDONS_ERRORS = project._do_install_addons()
+ end
+ return project._ADDONS_OK, project._ADDONS_ERRORS
+end
+
+-- do install the addons which this project declares
+function project._do_install_addons()
+ if os.getenv("XMAKE_SKIP_ADDONS") then
+ return true
+ end
+
+ -- this project declares nothing? or they have been installed already
+ local addonsinfo, errors = addons.load()
+ if errors then
+ return false, errors
+ end
+ if not addonsinfo or #addonsinfo.addons == 0 or addons.satisfied(addonsinfo) then
+ return true
+ end
+
+ -- @note we run it in a temporary directory, otherwise it would load this project again
+ --
+ -- @note we may be called when building the option menu, the command line has not
+ -- been parsed yet, so we can only get the common flags from the raw arguments
+ --
+ local argv = {"lua"}
+ local flags = {["-y"] = "--yes", ["--yes"] = "--yes",
+ ["-v"] = "--verbose", ["--verbose"] = "--verbose",
+ ["-D"] = "--diagnosis", ["--diagnosis"] = "--diagnosis"}
+ local flags_added = {}
+ for _, arg in ipairs(xmake._COMMAND_ARGV or {}) do
+ local flag = flags[arg]
+ if flag and not flags_added[flag] then
+ table.insert(argv, flag)
+ flags_added[flag] = true
+ end
+ end
+ table.insert(argv, "private.addons")
+ table.insert(argv, os.projectdir())
+ local envs = os.getenvs()
+ envs.XMAKE_SKIP_ADDONS = "y"
+ local ok, errors = os.execv(os.programfile(), argv, {curdir = os.tmpdir(), envs = envs})
+ if ok ~= 0 then
+ return false, errors or "install the addons of this project failed!"
+ end
+
+ -- we have loaded the registry and its caches before installing them, so we need to reload it
+ addon.reload()
+ rule.clear()
+ task.clear()
+ return true
+end
+
-- load the project file
function project._load(force, disable_filter)
@@ -235,6 +300,14 @@ function project._load(force, disable_filter)
return true
end
+ -- install the addons which this project declares in `xmake-addons.lua` first,
+ -- it may use their rules, toolchains and includes files,
+ -- e.g. includes("@addon/esp32-devel/board")
+ local ok, errors = project._install_addons()
+ if not ok then
+ return false, errors
+ end
+
-- enter the project directory
local oldir, errors = os.cd(os.projectdir())
if not oldir then
diff --git a/xmake/core/sandbox/modules/import/core/project/addons.lua b/xmake/core/sandbox/modules/import/core/project/addons.lua
new file mode 100644
index 000000000..9e866bd06
--- /dev/null
+++ b/xmake/core/sandbox/modules/import/core/project/addons.lua
@@ -0,0 +1,48 @@
+--!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 addons.lua
+--
+
+-- define module
+local sandbox_core_project_addons = sandbox_core_project_addons or {}
+
+-- load modules
+local addons = require("project/addons")
+local raise = require("sandbox/modules/raise")
+
+-- inherit some builtin interfaces
+sandbox_core_project_addons.file = addons.file
+sandbox_core_project_addons.lockfile = addons.lockfile
+sandbox_core_project_addons.locked = addons.locked
+sandbox_core_project_addons.satisfied = addons.satisfied
+
+-- load the declared addons of the given project directory
+--
+-- @param projectdir the project directory
+-- @return the addons information, it will be nil if this project declares nothing
+--
+function sandbox_core_project_addons.load(projectdir)
+ local addonsinfo, errors = addons.load(projectdir)
+ if errors then
+ raise(errors)
+ end
+ return addonsinfo
+end
+
+-- return module
+return sandbox_core_project_addons
diff --git a/xmake/modules/private/action/require/impl/install_packages.lua b/xmake/modules/private/action/require/impl/install_packages.lua
index 7ab247351..2d633109e 100644
--- a/xmake/modules/private/action/require/impl/install_packages.lua
+++ b/xmake/modules/private/action/require/impl/install_packages.lua
@@ -211,6 +211,8 @@ function _get_confirm(packages, opt)
-- show tips
if opt.toolchain then
cprint("${bright color.warning}note: ${clear}install or modify (m) these ${bright}toolchain${clear} packages first (pass -y to skip confirm)?")
+ elseif opt.packagekind == "addon" then
+ cprint("${bright color.warning}note: ${clear}install or modify (m) these ${bright}addons${clear} (pass -y to skip confirm)?")
else
cprint("${bright color.warning}note: ${clear}install or modify (m) these packages (pass -y to skip confirm)?")
end
diff --git a/xmake/modules/private/action/require/impl/package.lua b/xmake/modules/private/action/require/impl/package.lua
index 65d5f4153..1105384c8 100644
--- a/xmake/modules/private/action/require/impl/package.lua
+++ b/xmake/modules/private/action/require/impl/package.lua
@@ -1702,6 +1702,9 @@ function get_configs_str(package)
end
if requireinfo.kind then
table.insert(configs, requireinfo.kind)
+ elseif package:is_addon() then
+ -- @note the kind is only set for the dependencies, e.g. add_deps("foo", {kind = "addon"})
+ table.insert(configs, "addon")
end
local ignored_configs_for_buildhash = hashset.from(requireinfo.ignored_configs_for_buildhash or {})
local configs_overrided = requireinfo.configs_overrided or {}
diff --git a/xmake/modules/private/addons/main.lua b/xmake/modules/private/addons/main.lua
new file mode 100644
index 000000000..43a623e20
--- /dev/null
+++ b/xmake/modules/private/addons/main.lua
@@ -0,0 +1,92 @@
+--!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 main.lua
+--
+
+-- imports
+import("core.base.option")
+import("core.package.addon")
+import("core.project.addons")
+
+-- the format version of the addons lock file
+local LOCKVERSION = "1.0"
+
+-- get the requires of the declared addons
+--
+-- @note we always install the locked versions, the declared versions are only used
+-- to resolve them when there is no lock yet, @see _lock_addons
+--
+function _get_requires(addonsinfo, locked)
+ local requires = {}
+ for _, requirestr in ipairs(addonsinfo.addons) do
+ local name = requirestr:split("%s")[1]
+ local lockinfo = locked and locked[name]
+ if lockinfo and lockinfo.version then
+ requirestr = name .. " " .. lockinfo.version
+ end
+ table.insert(requires, requirestr)
+ end
+ return requires
+end
+
+-- lock the installed addons, so that we always get the same ones
+--
+-- @note we get the installed versions from the addons registry, they are
+-- registered when installing them, @see core/package/addon.lua
+--
+function _lock_addons(projectdir, addonsinfo)
+ local lockinfo = {}
+ -- @note we need to reload the registry, they have been installed by another process
+ local installed = addon.addons({force = true})
+ for _, requirestr in ipairs(addonsinfo.addons) do
+ local name = requirestr:split("%s")[1]
+ local addoninfo = installed[addon.dirname(name)]
+ if addoninfo then
+ lockinfo[name] = {version = addoninfo.version}
+ end
+ end
+ lockinfo.__meta__ = {version = LOCKVERSION}
+ io.save(addons.lockfile(projectdir), lockinfo, {orderkeys = true})
+end
+
+-- install the addons which the given project declares in its `xmake-addons.lua`
+--
+-- @note we are called from a sub-process, the project cannot be loaded until its
+-- addons are installed, @see core/project/project.lua
+--
+function main(projectdir)
+ projectdir = projectdir or os.projectdir()
+ local addonsinfo = addons.load(projectdir)
+ if not addonsinfo or #addonsinfo.addons == 0 then
+ return
+ end
+
+ -- install them with xrepo, it installs the packages in its own working directory,
+ -- so we need not a project here
+ local argv = {"lua", "private.xrepo", "install", "--addon"}
+ for _, name in ipairs({"yes", "verbose", "diagnosis"}) do
+ if option.get(name) then
+ table.insert(argv, "--" .. name)
+ end
+ end
+ table.join2(argv, _get_requires(addonsinfo, addons.locked(projectdir)))
+ os.execv(os.programfile(), argv)
+
+ -- and lock them, so that the other users get the same versions
+ _lock_addons(projectdir, addonsinfo)
+end