summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2026-08-15 10:38:51 +0800
committerGitHub <[email protected]>2026-08-15 10:38:51 +0800
commite4314dc3b2f483ef97b397ed5cf11fcdd397c9a0 (patch)
treea28db860b0f7af1a5a66bf2ed29c87126a29324a
parent6c246c12a09524780e5bfdc53bc27a892e4793a9 (diff)
parent045ce8a7a62e38357af0fd250675f279f14beafe (diff)
Merge pull request #7702 from xmake-io/preload
Auto fetch addons
-rw-r--r--tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua2
-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-build/src/main.c11
-rw-r--r--tests/actions/addon/projects/autofetch-build/xmake-addons.lua2
-rw-r--r--tests/actions/addon/projects/autofetch-build/xmake.lua11
-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.lua142
-rw-r--r--xmake/actions/addon/main.lua78
-rw-r--r--xmake/actions/addon/xmake.lua5
-rw-r--r--xmake/core/base/interpreter.lua38
-rw-r--r--xmake/core/base/semver.lua10
-rw-r--r--xmake/core/base/task.lua6
-rw-r--r--xmake/core/package/addon.lua210
-rw-r--r--xmake/core/project/addons.lua229
-rw-r--r--xmake/core/project/project.lua133
-rw-r--r--xmake/core/sandbox/modules/import/core/base/semver.lua9
-rw-r--r--xmake/core/sandbox/modules/import/core/package/addon.lua4
-rw-r--r--xmake/core/sandbox/modules/import/core/project/addons.lua52
-rw-r--r--xmake/core/sandbox/modules/import/core/sandbox/module.lua8
-rw-r--r--xmake/modules/private/action/addon/impl/install_addons.lua133
-rw-r--r--xmake/modules/private/action/addon/impl/xrepo.lua57
-rw-r--r--xmake/modules/private/action/require/impl/actions/install.lua4
-rw-r--r--xmake/modules/private/action/require/impl/install_packages.lua2
-rw-r--r--xmake/modules/private/action/require/impl/package.lua3
28 files changed, 1063 insertions, 100 deletions
diff --git a/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua b/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua
index 443690792..c0ed37914 100644
--- a/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua
+++ b/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua
@@ -9,7 +9,7 @@ function main(opt)
for _, name in ipairs({"gcc", "clang", "cc"}) do
local program = find_program(name, opt)
if program then
- return {program = program}
+ return program
end
end
end
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-build/src/main.c b/tests/actions/addon/projects/autofetch-build/src/main.c
new file mode 100644
index 000000000..112afe090
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch-build/src/main.c
@@ -0,0 +1,11 @@
+// the addons must provide the option and the toolchain of this project
+#ifndef MYOPTION
+# error the option of the custom-include addon is not found!
+#endif
+#ifndef MY_C6000
+# error the toolchain of the custom-toolchain addon is not used!
+#endif
+
+int main(int argc, char** argv) {
+ return 0;
+}
diff --git a/tests/actions/addon/projects/autofetch-build/xmake-addons.lua b/tests/actions/addon/projects/autofetch-build/xmake-addons.lua
new file mode 100644
index 000000000..2091e3b7f
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch-build/xmake-addons.lua
@@ -0,0 +1,2 @@
+-- the addons which this project needs, they are installed automatically when we load it
+add_addons("custom-include", "custom-rule", "custom-toolchain")
diff --git a/tests/actions/addon/projects/autofetch-build/xmake.lua b/tests/actions/addon/projects/autofetch-build/xmake.lua
new file mode 100644
index 000000000..bad3f8928
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch-build/xmake.lua
@@ -0,0 +1,11 @@
+-- the option comes from the includes file of an addon
+includes("@addon/custom-include/check")
+
+target("hello")
+ set_kind("binary")
+ add_files("src/main.c")
+ add_rules("@addon/custom-rule/hello")
+ set_toolchains("@addon/custom-toolchain/my-c6000")
+ if has_config("myoption") then
+ add_defines("MYOPTION")
+ end
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..d59ae6f4b 100644
--- a/tests/actions/addon/test.lua
+++ b/tests/actions/addon/test.lua
@@ -29,10 +29,14 @@ function _with_addons(names, func)
func,
finally
{
- function ()
+ -- @note try() swallows the errors if we do not re-raise them here
+ function (ok, errors)
for _, name in ipairs(names) do
_remove(name)
end
+ if not ok then
+ raise(errors)
+ end
end
}
}
@@ -54,6 +58,14 @@ function _with_repo(recipes, func)
local cachefile = path.join(global.cachedir(), "repository")
local cache = os.isfile(cachefile) and io.load(cachefile) or {}
cache.repositories = cache.repositories or {}
+
+ -- a killed test run may leave its temporary repository registered, and a dangling
+ -- repository breaks every following xrepo command, so we drop them here
+ for name, dirs in pairs(cache.repositories) do
+ if name:startswith("addon-test-repo-") and not os.isdir(dirs[1]) then
+ cache.repositories[name] = nil
+ end
+ end
cache.repositories[reponame] = {repodir}
io.save(cachefile, cache)
os.tryrm(path.join(global.cachedir(), "quick_search"))
@@ -65,7 +77,8 @@ function _with_repo(recipes, func)
end,
finally
{
- function ()
+ -- @note try() swallows the errors if we do not re-raise them here
+ function (ok, errors)
for name, _ in pairs(recipes) do
_remove(name)
end
@@ -76,6 +89,9 @@ function _with_repo(recipes, func)
io.save(cachefile, cache)
os.tryrm(path.join(global.cachedir(), "quick_search"))
os.tryrm(repodir)
+ if not ok then
+ raise(errors)
+ end
end
}
}
@@ -114,6 +130,36 @@ 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
+ {
+ -- @note try() swallows the errors if we do not re-raise them here
+ function (ok, errors)
+ os.cd(oldir)
+ os.tryrm(projectdir)
+ if not ok then
+ raise(errors)
+ end
+ end
+ }
+ }
+end
+
-- only the payloads should be installed, our own files should not
function test_install(t)
_with_addons({"custom-toolchain"}, function ()
@@ -223,6 +269,92 @@ 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,
+ -- and we should tell the user why we install something, it may need to confirm and download
+ local output = os.iorunv("xmake", {"config", "-y"})
+ t:require(output:find("custom-include: includes check is loaded", 1, true))
+ t:require(output:find("this project needs the addons", 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
+
+-- every command builds the option menu, which merges the project tasks in a best-effort way,
+-- so the commands which need not the project should never install its addons
+function test_autofetch_skipped_for_option_menu(t)
+ local recipes = {["custom-include"] = ("set_sourcedir(%q)"):format(_addondir("custom-include"))}
+ _with_repo(recipes, function ()
+ _remove("custom-include")
+ _with_project("autofetch", function (projectdir)
+ os.runv("xmake", {"addon", "--list"})
+ os.runv("xmake", {"lua", "-c", "print(\"hello\")"})
+ t:require_not(os.isfile(path.join(projectdir, "xmake-addons.lock")))
+ end)
+ end)
+end
+
+-- a complete project which declares its addons in `xmake-addons.lua` and builds with them,
+-- @see tests/actions/addon/projects/autofetch-build
+function test_autofetch_build(t)
+
+ -- @note the compiler of the custom toolchain is just the host one,
+ -- so we can only build it if there is one
+ if not (find_program("gcc") or find_program("clang") or find_program("cc")) then
+ return
+ end
+
+ local names = {"custom-include", "custom-rule", "custom-toolchain"}
+ local recipes = {}
+ for _, name in ipairs(names) do
+ recipes[name] = ("set_sourcedir(%q)"):format(_addondir(name))
+ end
+ _with_repo(recipes, function ()
+ for _, name in ipairs(names) do
+ _remove(name)
+ end
+ _with_project("autofetch-build", function (projectdir)
+
+ -- all of them should be installed when loading the project, and it should build
+ -- with their includes file, rule and toolchain, @see src/main.c
+ local output = os.iorunv("xmake", {"build", "-y"})
+ t:require(output:find("custom-include: includes check is loaded", 1, true))
+ t:require(output:find("custom-rule: hello from custom-rule: hello", 1, true))
+ t:require(output:find("build ok", 1, true))
+
+ -- and all of them should be locked
+ local lockinfo = io.load(path.join(projectdir, "xmake-addons.lock"))
+ for _, name in ipairs(names) do
+ t:require(lockinfo[name] ~= nil)
+ end
+ 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"))}
@@ -235,8 +367,12 @@ function test_install_from_repo(t)
t:require(os.iorunv("xmake", {"hello_addon"}):find("hello from custom-plugin", 1, true))
-- it should be searchable, and the addons should not be found by the package search
+ --
+ -- @note we cannot run the `xrepo` program here, it may not be in the PATH, e.g. on the ci,
+ -- and `xrepo search` is just a wrapper of it
+ --
t:require(os.iorunv("xmake", {"addon", "--search", "custom-plugin"}):find("custom-plugin", 1, true))
- t:require_not(os.iorunv("xrepo", {"search", "custom-plugin"}):find("custom-plugin", 1, true))
+ t:require_not(os.iorunv("xmake", {"lua", "private.xrepo", "search", "custom-plugin"}):find("custom-plugin", 1, true))
end)
end
diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua
index 7af106c05..29df38b0a 100644
--- a/xmake/actions/addon/main.lua
+++ b/xmake/actions/addon/main.lua
@@ -22,6 +22,8 @@
import("core.base.option")
import("core.package.addon")
import("devel.git")
+import("private.action.addon.impl.install_addons")
+import("private.action.addon.impl.xrepo", {alias = "xrepo_addon"})
import("private.action.require.impl.environment")
import("private.action.require.impl.search_packages")
@@ -49,30 +51,10 @@ function _get_addondir(name, version)
return addondir
end
--- run the given xrepo action for the addons, e.g. install, remove, search
-function _xrepo(action, names)
- local argv = {"lua", "private.xrepo", action, "--addon"}
- -- we need to pass the common options to the sub-process, e.g. -y, -v, -D
- if option.get("yes") then
- table.insert(argv, "-y")
- end
- if option.get("verbose") then
- table.insert(argv, "-v")
- end
- if option.get("diagnosis") then
- table.insert(argv, "-D")
- end
- if option.get("force") then
- table.insert(argv, "--force")
- end
- table.join2(argv, names)
- os.execv(os.programfile(), argv)
-end
-
-- install an addon from the given repository or the first repository containing it
function _install_from_repo(name, reponame)
_check_addon_name(name)
- _xrepo("install", {reponame and (reponame .. "@" .. name) or name})
+ xrepo_addon("install", {reponame and (reponame .. "@" .. name) or name}, {force = option.get("force")})
end
-- install a single addon from a source directory (as the given name, default to the directory name)
@@ -225,15 +207,41 @@ function _install()
end
-- remove the given installed addons
+--
+-- @note `xrepo remove --addon` removes them in the same way,
+-- @see xmake/modules/private/xrepo/action/remove.lua
+--
function _remove()
- local names = assert(option.get("addons"), "please specify the addon name to be removed!")
- _xrepo("remove", names)
+ local names = option.get("addons")
+ local force = option.get("force")
+
+ -- remove all the installed addons? e.g. xmake addon --remove --all
+ if option.get("all") then
+ names = table.keys(addon.addons())
+ if #names == 0 then
+ wprint("no installed addons!")
+ return
+ end
+ -- the dependencies between them do not matter, they are all removed
+ force = true
+ end
+
+ assert(names, "please specify the addon name to be removed!")
+ for _, name in ipairs(names) do
+ addon.remove(name, {force = force})
+ cprint("${color.success}remove ${bright}%s${clear} ok!", name)
+ end
+end
+
+-- upgrade the addons which the current project declares in its `xmake-addons.lua`
+function _upgrade()
+ install_addons(os.projectdir(), {upgrade = true})
end
-- search the addons from the repositories
function _search()
local patterns = assert(option.get("addons"), "please specify the addon name pattern to be searched!")
- _xrepo("search", patterns)
+ xrepo_addon("search", patterns)
end
-- collect the installed addons from the addons registry
@@ -243,8 +251,12 @@ end
--
function _collect_installed_addons()
local entries = {}
- for name, addoninfo in pairs(addon.rescan()) do
+ for name, addoninfo in pairs(addon.addons()) do
+ -- an addon can be installed with several versions, we show the active one,
+ -- the projects can lock the other ones, @see core/project/addons.lua
+ local versions = addon.versions(name)
table.insert(entries, {name = name, version = addoninfo.version,
+ versions = #versions > 1 and versions or nil,
description = addoninfo.description, payloads = addoninfo.payloads})
end
table.sort(entries, function (a, b) return a.name < b.name end)
@@ -285,7 +297,11 @@ function _list()
if #installed > 0 then
for _, entry in ipairs(installed) do
exclude[entry.name] = true
- _print_addon(entry, string.format(" ${dim}(%s)${clear}", table.concat(entry.payloads, ", ")))
+ local suffix = string.format(" ${dim}(%s)${clear}", table.concat(entry.payloads, ", "))
+ if entry.versions then
+ suffix = suffix .. string.format(" ${dim}[installed: %s]${clear}", table.concat(entry.versions, ", "))
+ end
+ _print_addon(entry, suffix)
end
else
print(" (none)")
@@ -308,22 +324,16 @@ function _list()
end
end
--- clear all installed addons
-function _clear()
- addon.clear()
- cprint("${color.success}clear all installed addons ok!")
-end
-
function main()
if option.get("install") then
_install()
elseif option.get("remove") then
_remove()
+ elseif option.get("upgrade") then
+ _upgrade()
elseif option.get("list") then
_list()
elseif option.get("search") then
_search()
- elseif option.get("clear") then
- _clear()
end
end
diff --git a/xmake/actions/addon/xmake.lua b/xmake/actions/addon/xmake.lua
index 3c814cc6d..a0266aa4f 100644
--- a/xmake/actions/addon/xmake.lua
+++ b/xmake/actions/addon/xmake.lua
@@ -29,7 +29,8 @@ task("addon")
{'r', "remove", "k", nil, "Remove the given installed addons."},
{'s', "search", "k", nil, "Search the addons from the repositories."},
{'l', "list", "k", nil, "List all installed addons."},
- {'c', "clear", "k", nil, "Clear all installed addons."},
+ {'u', "upgrade", "k", nil, "Upgrade the addons which the current project declares."},
+ {nil, "all", "k", nil, "Remove all installed addons, e.g. xmake addon --remove --all"},
{'f', "force", "k", nil, "Force to remove the addons, even if they are depended on by the others."},
{nil, "addons", "vs", nil, "The addon paths, urls or names.",
"e.g.",
@@ -40,6 +41,8 @@ task("addon")
" $ xmake addon --install xmake-repo@serial-monitor",
" $ xmake addon --install serial-monitor",
" $ xmake addon --remove serial-monitor",
+ " $ xmake addon --remove --all",
+ " $ xmake addon --upgrade",
" $ xmake addon --search serial",
" $ xmake addon --list"}
}
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/semver.lua b/xmake/core/base/semver.lua
index 7c2b6ea8d..fc989e18d 100644
--- a/xmake/core/base/semver.lua
+++ b/xmake/core/base/semver.lua
@@ -232,5 +232,15 @@ function semver.match(str, pos, pattern)
end
end
+-- is a valid semantic version? e.g. "1.2.3", "v1.2.3-beta"
+function semver.is_valid(version)
+ return semver.parse(version) ~= nil
+end
+
+-- is a valid semantic version range? e.g. ">=1.0 <2.0", "^1.2", "master || >1.4"
+function semver.is_valid_range(range)
+ return semver.satisfies("1.0", range) ~= nil
+end
+
-- return module: semver
return semver
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..4a4c8f50a 100644
--- a/xmake/core/package/addon.lua
+++ b/xmake/core/package/addon.lua
@@ -70,15 +70,16 @@ function addon._registryfile()
return path.join(addon.installdir(), "addons.conf")
end
--- save the given addons to the registry file
-function addon._save(addons)
- addon._ADDONS = addons
+-- save the given registry to the registry file
+function addon._save(registry)
+ addon._REGISTRY = registry
+ addon._ADDONS = nil
local registryfile = addon._registryfile()
-- we need not create an empty registry file if no addons are installed
- if next(addons) == nil and not os.isfile(registryfile) then
+ if table.empty(registry) and not os.isfile(registryfile) then
return
end
- local ok, errors = io.save(registryfile, addons)
+ local ok, errors = io.save(registryfile, registry)
if not ok then
utils.warning(errors)
end
@@ -182,10 +183,15 @@ end
function addon._parents(name)
local dirname = addon.dirname(name)
local parents
- for otherdirname, addoninfo in pairs(addon.addons()) do
- if otherdirname ~= dirname and table.contains(addoninfo.deps or {}, dirname) then
- parents = parents or {}
- table.insert(parents, otherdirname)
+ for otherdirname, entry in pairs(addon._registry()) do
+ if otherdirname ~= dirname then
+ for _, addoninfo in pairs(entry.versions or {}) do
+ if table.contains(addoninfo.deps or {}, dirname) then
+ parents = parents or {}
+ table.insert(parents, otherdirname)
+ break
+ end
+ end
end
end
if parents then
@@ -194,14 +200,27 @@ function addon._parents(name)
return parents
end
--- unregister the given addon
-function addon._unregister(name)
+-- unregister the given addon or only one of its versions
+function addon._unregister(name, version)
local dirname = addon.dirname(name)
- local addons = addon.addons()
- if addons[dirname] then
- addons[dirname] = nil
- addon._save(addons)
+ local registry = addon._registry()
+ local entry = registry[dirname]
+ if entry == nil then
+ return
end
+ if version then
+ entry.versions[version] = nil
+ if entry.active == version then
+ -- we need to select the other one deterministically
+ entry.active = addon.versions(name)[1]
+ end
+ if table.empty(entry.versions) then
+ registry[dirname] = nil
+ end
+ else
+ registry[dirname] = nil
+ end
+ addon._save(registry)
end
-- get the apis of the addon manifest
@@ -275,6 +294,26 @@ function addon.manifest(sourcedir)
return manifest
end
+-- get a working directory which has no project
+--
+-- we need it to run the sub-processes of the addons, e.g. `xrepo install --addon`,
+-- otherwise they would load the project of the current directory again
+--
+-- @note we cannot use `os.tmpdir()` directly, it is shared by all the commands,
+-- e.g. a stray `xmake.lua` in it would break the isolation
+--
+-- @note we can share it between the processes, we only use it as the working directory
+-- and never write anything into it, @see private/action/addon/impl/xrepo.lua
+--
+function addon.workdir()
+ local workdir = path.join(os.tmpdir(), "addons", "working")
+ if not os.isdir(workdir) then
+ -- it may be created by the other processes at the same time, we can ignore it
+ os.mkdir(workdir)
+ end
+ return workdir
+end
+
-- the install directory of addons, e.g. ~/.xmake/addons
function addon.installdir()
return path.join(global.directory(), "addons")
@@ -400,7 +439,64 @@ function addon.resolve_reference(reference, sep, kind, opt)
return {dir = payloaddir, name = name, addon = addonname}
end
--- get all installed addons
+-- get the registry of the installed addons
+--
+-- an addon can be installed with several versions at the same time, e.g. the projects
+-- may lock the different versions of it, so we save all of them
+--
+-- @return the registry, e.g. {["esp32"] = {active = "1.0.3", versions = {["1.0.3"] = {...}}}}
+--
+function addon._registry(opt)
+ local registry = addon._REGISTRY
+ if opt and opt.force then
+ registry = nil
+ end
+ if registry == nil then
+ registry = {}
+ local registryfile = addon._registryfile()
+ if os.isfile(registryfile) then
+ registry = io.load(registryfile) or {}
+ end
+ -- migrate the old registry, it only saved one version for each addon
+ for dirname, addoninfo in pairs(registry) do
+ if addoninfo.versions == nil then
+ registry[dirname] = {active = addoninfo.version,
+ versions = {[addoninfo.version] = addoninfo}}
+ end
+ end
+ addon._REGISTRY = registry
+ addon._ADDONS = nil
+ end
+ return registry
+end
+
+-- pin the active version of the given addon for this process
+--
+-- @note a project locks the versions of its addons, so we need to activate them
+-- when we load it, @see core/project/addons.lua
+--
+function addon.pin(name, version)
+ local pinned = addon._PINNED
+ if pinned == nil then
+ pinned = {}
+ addon._PINNED = pinned
+ end
+ pinned[addon.dirname(name)] = version
+ addon._ADDONS = nil
+end
+
+-- get all the installed versions of the given addon, e.g. {"1.0.2", "1.0.3"}
+function addon.versions(name)
+ local versions = {}
+ local addoninfo = addon._registry()[addon.dirname(name)]
+ for version, _ in pairs(addoninfo and addoninfo.versions or {}) do
+ table.insert(versions, version)
+ end
+ table.sort(versions)
+ return versions
+end
+
+-- get all installed addons, only the active version of each addon
--
-- @param opt the options, e.g. {force = true}, we need it to reload the registry
-- if the addons have been installed by another process
@@ -408,15 +504,20 @@ end
-- @return the addons table, e.g. {["hello-world"] = {version = "latest", payloads = {"plugins"}}}
--
function addon.addons(opt)
- local addons = addon._ADDONS
if opt and opt.force then
- addons = nil
+ addon._registry({force = true})
end
+ local addons = addon._ADDONS
if addons == nil then
addons = {}
- local registryfile = addon._registryfile()
- if os.isfile(registryfile) then
- addons = io.load(registryfile) or {}
+ local pinned = addon._PINNED or {}
+ for dirname, addoninfo in pairs(addon._registry()) do
+ -- the project may lock another version of it, @see addon.pin
+ local version = pinned[dirname] or addoninfo.active
+ local versioninfo = addoninfo.versions and addoninfo.versions[version]
+ if versioninfo then
+ addons[dirname] = versioninfo
+ end
end
addon._ADDONS = addons
end
@@ -592,6 +693,8 @@ function addon.register(name, version, opt)
name = name ~= dirname and name or nil,
description = opt.description,
deps = opt.deps,
+ -- where it comes from, e.g. {url = ..., commit = ..., branch = ...}
+ repo = opt.repo,
-- the deps which the addon itself declares in its manifest, they are
-- recorded whenever this addon has one, so that the repositories can
-- check that the manifest and the package recipe are kept in sync
@@ -608,9 +711,17 @@ function addon.register(name, version, opt)
return false, errors
end
- local addons = addon.addons()
- addons[dirname] = addoninfo
- addon._save(addons)
+ -- we can install several versions of an addon at the same time,
+ -- and the version which we install now is always the active one
+ local registry = addon._registry()
+ local entry = registry[dirname]
+ if entry == nil or entry.versions == nil then
+ entry = {versions = {}}
+ registry[dirname] = entry
+ end
+ entry.versions[version] = addoninfo
+ entry.active = version
+ addon._save(registry)
return true
end
@@ -656,21 +767,34 @@ 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._REGISTRY = nil
+ 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
--
function addon.rescan()
- local oldaddons = addon.addons()
- local addons = {}
+ local oldregistry = addon._registry()
+ local registry = {}
for _, versiondir in ipairs(os.dirs(path.join(addon.installdir(), "*", "*"))) do
local payloads = addon.payloads_of(versiondir)
if #payloads > 0 then
local dirname = path.filename(path.directory(versiondir))
local version = path.filename(versiondir)
- local oldaddoninfo = oldaddons[dirname]
+ local oldentry = oldregistry[dirname]
+ local oldaddoninfo = oldentry and oldentry.versions and oldentry.versions[version]
local description, deps
- if oldaddoninfo and oldaddoninfo.version == version then
+ if oldaddoninfo then
-- we need to keep them, we cannot get them from the installed payloads
description = oldaddoninfo.description
deps = oldaddoninfo.deps
@@ -685,24 +809,22 @@ function addon.rescan()
description = manifest.description or description
end
end
- addons[dirname] = {version = version, name = name or (oldaddoninfo and oldaddoninfo.name),
- description = description, deps = deps, payloads = payloads,
- plugins = addon._plugins_of(versiondir), templates = addon._templates_of(versiondir)}
+ local entry = registry[dirname]
+ if entry == nil then
+ entry = {versions = {}}
+ registry[dirname] = entry
+ end
+ entry.versions[version] = {version = version, name = name or (oldaddoninfo and oldaddoninfo.name),
+ description = description, deps = deps, payloads = payloads,
+ plugins = addon._plugins_of(versiondir), templates = addon._templates_of(versiondir)}
+ -- we keep the active version if it's still installed, otherwise we use the last one
+ if entry.active == nil or (oldentry and oldentry.active == version) then
+ entry.active = version
+ end
end
end
- addon._save(addons)
- return addons
-end
-
--- clear all installed addons
-function addon.clear()
- local installdir = addon.installdir()
- if os.isdir(installdir) then
- os.rmdir(installdir)
- end
- addon._ADDONS = {}
- addon._MANIFESTS = nil
- addon._GLOBALMODULES = nil
+ addon._save(registry)
+ return addon.addons()
end
-- return module
diff --git a/xmake/core/project/addons.lua b/xmake/core/project/addons.lua
new file mode 100644
index 000000000..3b105840e
--- /dev/null
+++ b/xmake/core/project/addons.lua
@@ -0,0 +1,229 @@
+--!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 semver = require("base/semver")
+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})
+-- add_repositories("myrepo [email protected]:me/myrepo.git")
+--
+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 format version of the addons lock file
+--
+-- @see xmake/core/project/project.lua, project.requireslock_version()
+--
+function addons.lockfile_version()
+ return "1.0"
+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"
+ -- the repositories which provide them, e.g. add_repositories("myrepo [email protected]:me/myrepo.git")
+ , "add_repositories"
+ }
+ }
+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"),
+ repositories = table.wrap(rootinfo:get("repositories"))}
+
+ -- 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 = addons.requirename(requirestr)
+ 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
+
+-- split the given declaration into the name and the version range
+--
+-- @note the version range can contain spaces, so everything after the name belongs to it,
+-- e.g. "esp32-devel 1.0.x", "esp32-devel >=1.0 <2.0", "esp32-devel master || >1.4",
+-- @see xmake/modules/private/utils/package.lua
+--
+function addons.requirename(requirestr)
+ local splitinfo = requirestr:split("%s+", {limit = 2})
+ return splitinfo[1], splitinfo[2]
+end
+
+-- is the locked version still valid for the given declaration?
+--
+-- @note the declaration is authoritative, the lock only pins a version inside it,
+-- so we need to resolve it again if the user has changed the declared version
+--
+function addons.locked_valid(requirestr, lockinfo)
+ if not lockinfo or not lockinfo.version then
+ return false
+ end
+ local _, range = addons.requirename(requirestr)
+ if range then
+ -- @note we can only compare the semantic versions, e.g. the local addons are always `latest`
+ return semver.is_valid(lockinfo.version) and semver.satisfies(lockinfo.version, range)
+ end
+ return true
+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 = addons.requirename(requirestr)
+ local lockinfo = locked[name]
+ if not addons.locked_valid(requirestr, 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..3b975cccd 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,11 +229,121 @@ 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/action/addon/impl/install_addons.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
+
+-- activate the addon versions which this project locks
+--
+-- @note an addon can be installed with several versions at the same time, the other
+-- projects may lock the other versions of it, @see core/package/addon.lua
+--
+function project._pin_addons()
+ for name, lockinfo in pairs(addons.locked() or {}) do
+ if lockinfo.version then
+ addon.pin(name, lockinfo.version)
+ end
+ end
+end
+
+-- do install the addons which this project declares
+function project._do_install_addons()
+
+ -- this project declares nothing?
+ local addonsinfo, errors = addons.load()
+ if errors then
+ return false, errors
+ end
+ if not addonsinfo or #addonsinfo.addons == 0 then
+ return true
+ end
+
+ -- they have been installed already?
+ project._pin_addons()
+ if addons.satisfied(addonsinfo) then
+ return true
+ end
+
+ -- tell the user why we are installing something, it may need to confirm and download,
+ -- e.g. `xmake --help` in a project directory which declares some addons
+ utils.cprint("${color.warning}note: ${clear}%s: this project needs the addons(${bright}%s${clear}), installing them ..",
+ addons.filename(), table.concat(addonsinfo.addons, ", "))
+ if baseoption.get("help") then
+ -- the help menu also shows the options which the addons provide, but the user
+ -- did not ask for an installation, so we tell them how to skip it
+ utils.cprint("${dim}we can run it outside of the project directory to skip the installation${clear}")
+ end
+
+ -- @note we run it in a working directory which has no project, @see addon.workdir(),
+ -- 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.action.addon.impl.install_addons")
+ table.insert(argv, os.projectdir())
+ local ok, errors = os.execv(os.programfile(), argv, {curdir = addon.workdir()})
+ 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()
+ project._pin_addons()
+ rule.clear()
+ task.clear()
+ return true
+end
+
-- load the project file
-function project._load(force, disable_filter)
+--
+-- @param opt the options
+-- - force: load the project file again even if it has been loaded
+-- - disable_filter: disable the interpreter filter, e.g. `$(plat)`
+-- - skip_addons: do not install the addons which this project declares
+--
+function project._load(opt)
+ opt = opt or {}
+
+ -- 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")
+ --
+ -- @note we need to check it before the cache, the project file may have been loaded
+ -- already without them, e.g. by the option menu
+ --
+ if not opt.skip_addons then
+ local ok, errors = project._install_addons()
+ if not ok then
+ return false, errors
+ end
+ end
-- has already been loaded?
- if project._memcache():get("rootinfo") and not force then
+ if project._memcache():get("rootinfo") and not opt.force then
return true
end
@@ -261,13 +373,13 @@ function project._load(force, disable_filter)
end
-- load the root info of the project
- local rootinfo, errors = project._load_scope("root", true, not disable_filter)
+ local rootinfo, errors = project._load_scope("root", true, not opt.disable_filter)
if not rootinfo then
return false, errors
end
-- load the root info of the target
- local rootinfo_target, errors = project._load_scope("root.target", true, not disable_filter)
+ local rootinfo_target, errors = project._load_scope("root.target", true, not opt.disable_filter)
if not rootinfo_target then
return false, errors
end
@@ -311,6 +423,11 @@ function project._load_scope(scope_kind, deduplicate, enable_filter)
end
-- load tasks
+--
+-- @note we should not install the addons which this project declares here, the option menu
+-- merges the project tasks in a best-effort way and every command builds it,
+-- e.g. `xmake lua`, `xmake addon --remove --all`, @see xmake/core/main.lua
+--
function project._load_tasks()
-- the project file is not found?
@@ -319,7 +436,7 @@ function project._load_tasks()
end
-- load the project file first and disable filter
- local ok, errors = project._load(true, true)
+ local ok, errors = project._load({force = true, disable_filter = true, skip_addons = true})
if not ok then
return nil, errors
end
@@ -400,7 +517,7 @@ function project._load_targets()
-- load all requires first and reload the project file to ensure has_package() works for targets
local requires = project.required_packages()
- local ok, errors = project._load(true)
+ local ok, errors = project._load({force = true})
if not ok then
return nil, errors
end
@@ -481,7 +598,7 @@ function project._load_options(disable_filter)
end
-- reload the project file to ensure `if is_plat() then add_packagedirs() end` works
- local ok, errors = project._load(true, disable_filter)
+ local ok, errors = project._load({force = true, disable_filter = disable_filter})
if not ok then
return nil, errors
end
@@ -1129,7 +1246,7 @@ function project.requires_str()
if not requires_str then
-- reload the project file to handle `has_config()`
- local ok, errors = project._load(true)
+ local ok, errors = project._load({force = true})
if not ok then
os.raise(errors)
end
diff --git a/xmake/core/sandbox/modules/import/core/base/semver.lua b/xmake/core/sandbox/modules/import/core/base/semver.lua
index 45ba8b74f..04df315eb 100644
--- a/xmake/core/sandbox/modules/import/core/base/semver.lua
+++ b/xmake/core/sandbox/modules/import/core/base/semver.lua
@@ -50,15 +50,10 @@ function sandbox_core_base_semver.match(str, pos, pattern)
end
-- is valid version?
-function sandbox_core_base_semver.is_valid(version)
- return semver.parse(version) ~= nil
-end
+sandbox_core_base_semver.is_valid = semver.is_valid
-- is valid version range?
-function sandbox_core_base_semver.is_valid_range(range)
- local ok = semver.satisfies("1.0", range)
- return ok ~= nil
-end
+sandbox_core_base_semver.is_valid_range = semver.is_valid_range
-- compare two version strings
--
diff --git a/xmake/core/sandbox/modules/import/core/package/addon.lua b/xmake/core/sandbox/modules/import/core/package/addon.lua
index b297347c6..74610c693 100644
--- a/xmake/core/sandbox/modules/import/core/package/addon.lua
+++ b/xmake/core/sandbox/modules/import/core/package/addon.lua
@@ -27,6 +27,7 @@ local raise = require("sandbox/modules/raise")
-- inherit some builtin interfaces
sandbox_core_package_addon.installdir = addon.installdir
+sandbox_core_package_addon.workdir = addon.workdir
sandbox_core_package_addon.dirname = addon.dirname
sandbox_core_package_addon.owner = addon.owner
sandbox_core_package_addon.is_reference = addon.is_reference
@@ -35,9 +36,10 @@ sandbox_core_package_addon.payloadinfos = addon.payloadinfos
sandbox_core_package_addon.payloads_of = addon.payloads_of
sandbox_core_package_addon.payloadroot = addon.payloadroot
sandbox_core_package_addon.addons = addon.addons
+sandbox_core_package_addon.versions = addon.versions
+sandbox_core_package_addon.pin = addon.pin
sandbox_core_package_addon.addondir = addon.addondir
sandbox_core_package_addon.rescan = addon.rescan
-sandbox_core_package_addon.clear = addon.clear
-- get the manifest of the given addon directory, e.g. <sourcedir>/addon.lua
--
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..542cbaf4f
--- /dev/null
+++ b/xmake/core/sandbox/modules/import/core/project/addons.lua
@@ -0,0 +1,52 @@
+--!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.filename = addons.filename
+sandbox_core_project_addons.lockfile = addons.lockfile
+sandbox_core_project_addons.lockfile_version = addons.lockfile_version
+sandbox_core_project_addons.locked = addons.locked
+sandbox_core_project_addons.locked_valid = addons.locked_valid
+sandbox_core_project_addons.requirename = addons.requirename
+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/core/sandbox/modules/import/core/sandbox/module.lua b/xmake/core/sandbox/modules/import/core/sandbox/module.lua
index 4cf419b44..294cf7556 100644
--- a/xmake/core/sandbox/modules/import/core/sandbox/module.lua
+++ b/xmake/core/sandbox/modules/import/core/sandbox/module.lua
@@ -452,6 +452,14 @@ end
-- find module
function core_sandbox_module.find(name)
+
+ -- an addon can export some modules as the global modules, they are also visible here,
+ -- e.g. find_toolname() looks for `detect.tools.find_xxx` with it, @see addon.globalmodules()
+ local globalmodulesdir = addon.globalmodules()[name]
+ if globalmodulesdir and core_sandbox_module._find(globalmodulesdir, name) then
+ return true
+ end
+
for _, moduledir in ipairs(core_sandbox_module.directories()) do
if (core_sandbox_module._find(moduledir, name)) then
return true
diff --git a/xmake/modules/private/action/addon/impl/install_addons.lua b/xmake/modules/private/action/addon/impl/install_addons.lua
new file mode 100644
index 000000000..a20ed740b
--- /dev/null
+++ b/xmake/modules/private/action/addon/impl/install_addons.lua
@@ -0,0 +1,133 @@
+--!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 install_addons.lua
+--
+
+-- imports
+import("core.package.addon")
+import("core.project.addons")
+import("private.action.addon.impl.xrepo", {alias = "xrepo_addon"})
+
+-- get the requires of the declared addons
+--
+-- @note we install the locked versions, but the declaration is authoritative, so we
+-- resolve them again if the user has changed it or upgrades them
+--
+function _get_requires(addonsinfo, locked)
+ local requires = {}
+ for _, requirestr in ipairs(addonsinfo.addons) do
+ local name = addons.requirename(requirestr)
+ local lockinfo = locked and locked[name]
+ if addons.locked_valid(requirestr, lockinfo) 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 = {}
+ local locked = addons.locked(projectdir) or {}
+ -- @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 = addons.requirename(requirestr)
+ local addoninfo = installed[addon.dirname(name)]
+ if addoninfo then
+ local oldversion = locked[name] and locked[name].version
+ if oldversion and oldversion ~= addoninfo.version then
+ cprint("${color.success}upgrade ${bright}%s${clear}: %s -> %s", name, oldversion, addoninfo.version)
+ end
+ -- we lock the repository too, so that the other users get it from the same source,
+ -- @see xmake/modules/private/action/require/impl/lock_packages.lua
+ lockinfo[name] = {version = addoninfo.version, repo = addoninfo.repo}
+ end
+ end
+ lockinfo.__meta__ = {version = addons.lockfile_version()}
+
+ -- @note we need to write it deterministically, the key order of a lua table is random,
+ -- otherwise the lock file would change even if nothing changed,
+ -- @see xmake/modules/private/action/require/impl/lock_packages.lua
+ local content = string.serialize(lockinfo, {orderkeys = true})
+ local tmpfile = os.tmpfile()
+ io.writefile(tmpfile, content, {encoding = "binary"})
+
+ -- and we only write it if the content is different, so we can keep the file time
+ os.cp(tmpfile, addons.lockfile(projectdir), {copy_if_different = true})
+ os.rm(tmpfile)
+end
+
+-- install the addons which the given project declares in its `xmake-addons.lua`
+--
+-- @note we are also called from a sub-process, the project cannot be loaded until
+-- its addons are installed, @see core/project/project.lua
+--
+function main(projectdir, opt)
+ opt = opt or {}
+ projectdir = projectdir or os.projectdir()
+ local addonsinfo = addons.load(projectdir)
+ if not addonsinfo or #addonsinfo.addons == 0 then
+ return
+ end
+
+ -- upgrade them? we need to resolve the declared versions again
+ local locked = not opt.upgrade and addons.locked(projectdir) or nil
+
+ -- install them with xrepo, it installs the packages in its own working directory,
+ -- so we need not a project here
+
+ -- this project declares its own repositories? we pass them to xrepo,
+ -- they are only used by this installation, we do not register them globally
+ local rcfile
+ if #addonsinfo.repositories > 0 then
+ rcfile = os.tmpfile() .. ".lua"
+ local file = io.open(rcfile, "w")
+ for _, repo in ipairs(addonsinfo.repositories) do
+ file:print("add_repositories(%q)", repo)
+ end
+ file:close()
+ end
+
+ try
+ {
+ function ()
+ xrepo_addon("install", _get_requires(addonsinfo, locked), {includes = rcfile})
+ end,
+ finally
+ {
+ -- @note try() swallows the errors if we do not re-raise them here
+ function (ok, errors)
+ if rcfile then
+ os.tryrm(rcfile)
+ end
+ if not ok then
+ raise(errors)
+ end
+ end
+ }
+ }
+
+ -- and lock them, so that the other users get the same versions
+ _lock_addons(projectdir, addonsinfo)
+end
diff --git a/xmake/modules/private/action/addon/impl/xrepo.lua b/xmake/modules/private/action/addon/impl/xrepo.lua
new file mode 100644
index 000000000..46ada2d01
--- /dev/null
+++ b/xmake/modules/private/action/addon/impl/xrepo.lua
@@ -0,0 +1,57 @@
+--!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 xrepo.lua
+--
+
+-- imports
+import("core.base.option")
+import("core.package.addon")
+
+-- run the given xrepo action for the addons
+--
+-- @note xrepo installs the packages in its own working project, so it works anywhere,
+-- and we need not implement the download/dependencies/confirm logic again
+--
+-- @param action the action name, e.g. "install", "search"
+-- @param names the addon names, urls or require strings, e.g. {"esp32-devel 1.0.x"}
+-- @param opt the options, e.g. {force = true, includes = "/tmp/xxx.lua"}
+--
+-- @note we always run it in a working directory which has no project, @see addon.workdir()
+--
+function main(action, names, opt)
+ opt = opt or {}
+ local argv = {"lua", "private.xrepo", action, "--addon"}
+
+ -- we need to pass the common options to the sub-process, e.g. -y, -v, -D
+ for _, name in ipairs({"yes", "verbose", "diagnosis"}) do
+ if option.get(name) then
+ table.insert(argv, "--" .. name)
+ end
+ end
+ if opt.force then
+ table.insert(argv, "--force")
+ end
+
+ -- the extra lua configuration files, e.g. the repositories which a project declares
+ if opt.includes then
+ table.insert(argv, "--includes=" .. opt.includes)
+ end
+
+ table.join2(argv, names)
+ os.execv(os.programfile(), argv, {curdir = opt.curdir or addon.workdir()})
+end
diff --git a/xmake/modules/private/action/require/impl/actions/install.lua b/xmake/modules/private/action/require/impl/actions/install.lua
index 65750c1bc..7ddb66cca 100644
--- a/xmake/modules/private/action/require/impl/actions/install.lua
+++ b/xmake/modules/private/action/require/impl/actions/install.lua
@@ -318,8 +318,12 @@ function _register_addon(package)
end
end
+ -- we also record where it comes from, so that the projects can lock it,
+ -- @see xmake/modules/private/action/addon/impl/install_addons.lua
+ local repo = package:repo()
addon.register(package:name(), package:version_str() or "latest",
{description = description, deps = deps,
+ repo = repo and {url = repo:url(), commit = repo:commit(), branch = repo:branch()} or nil,
manifest_deps = manifest and manifest.deps or nil,
globalmodules = manifest and #manifest.globalmodules > 0 and manifest.globalmodules or nil})
end
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 {}