summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2026-08-15 17:57:22 +0800
committerGitHub <[email protected]>2026-08-15 17:57:22 +0800
commit0a32ec18d737259052271fa3be5cfdb50dbd1d15 (patch)
tree50be742aed502e6f5432ff841766bec4e38ff16d
parent069474769dcfb01898f2c7066c1bf71d5da79125 (diff)
parent67ffcd8a481a5c6c51da8044bfe1ebed980b1e56 (diff)
Merge pull request #7696 from xmake-io/addon
Add addon support for plugins
-rw-r--r--tests/actions/addon/custom-include/addon.lua2
-rw-r--r--tests/actions/addon/custom-include/includes/check/xmake.lua4
-rw-r--r--tests/actions/addon/custom-module/addon.lua2
-rw-r--r--tests/actions/addon/custom-module/modules/greeting.lua3
-rw-r--r--tests/actions/addon/custom-plugin/addon.lua2
-rw-r--r--tests/actions/addon/custom-plugin/plugins/hello_addon/main.lua5
-rw-r--r--tests/actions/addon/custom-plugin/plugins/hello_addon/xmake.lua5
-rw-r--r--tests/actions/addon/custom-rule/addon.lua2
-rw-r--r--tests/actions/addon/custom-rule/modules/greeting.lua3
-rw-r--r--tests/actions/addon/custom-rule/rules/hello/xmake.lua6
-rw-r--r--tests/actions/addon/custom-template/addon.lua2
-rw-r--r--tests/actions/addon/custom-template/templates/c/customaddon/hello/src/main.c6
-rw-r--r--tests/actions/addon/custom-template/templates/c/customaddon/hello/xmake.lua5
-rw-r--r--tests/actions/addon/custom-toolchain/addon.lua6
-rw-r--r--tests/actions/addon/custom-toolchain/src/modules/core/tools/mycl6x.lua12
-rw-r--r--tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua15
-rw-r--r--tests/actions/addon/custom-toolchain/src/toolchains/my-c6000/xmake.lua19
-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.c14
-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.lua450
-rw-r--r--tests/plugins/repository/test.lua118
-rw-r--r--xmake/actions/addon/main.lua330
-rw-r--r--xmake/actions/addon/xmake.lua49
-rw-r--r--xmake/actions/create/main.lua40
-rw-r--r--xmake/actions/create/template.lua75
-rw-r--r--xmake/actions/require/xmake.lua4
-rw-r--r--xmake/core/base/interpreter.lua82
-rw-r--r--xmake/core/base/os.lua17
-rw-r--r--xmake/core/base/semver.lua10
-rw-r--r--xmake/core/base/task.lua63
-rw-r--r--xmake/core/package/addon.lua774
-rw-r--r--xmake/core/package/package.lua106
-rw-r--r--xmake/core/project/addons.lua228
-rw-r--r--xmake/core/project/project.lua140
-rw-r--r--xmake/core/project/rule.lua229
-rw-r--r--xmake/core/sandbox/modules/import/core/base/semver.lua9
-rw-r--r--xmake/core/sandbox/modules/import/core/package/addon.lua99
-rw-r--r--xmake/core/sandbox/modules/import/core/project/addons.lua52
-rw-r--r--xmake/core/sandbox/modules/import/core/project/project.lua4
-rw-r--r--xmake/core/sandbox/modules/import/core/sandbox/module.lua62
-rw-r--r--xmake/core/tool/toolchain.lua60
-rw-r--r--xmake/modules/package/manager/xmake/search_package.lua4
-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.lua50
-rw-r--r--xmake/modules/private/action/require/impl/install_packages.lua4
-rw-r--r--xmake/modules/private/action/require/impl/package.lua37
-rw-r--r--xmake/modules/private/action/require/impl/repository.lua2
-rw-r--r--xmake/modules/private/action/require/install.lua8
-rw-r--r--xmake/modules/private/action/require/search.lua8
-rw-r--r--xmake/modules/private/check/checkers/api/package/kind.lua4
-rw-r--r--xmake/modules/private/xrepo/action/install.lua7
-rw-r--r--xmake/modules/private/xrepo/action/remove.lua18
-rw-r--r--xmake/modules/private/xrepo/action/search.lua6
-rw-r--r--xmake/modules/private/xrepo/quick_search/cache.lua68
-rw-r--r--xmake/plugins/plugin/main.lua1
-rw-r--r--xmake/plugins/plugin/xmake.lua2
64 files changed, 3293 insertions, 257 deletions
diff --git a/tests/actions/addon/custom-include/addon.lua b/tests/actions/addon/custom-include/addon.lua
new file mode 100644
index 000000000..71d01b5c2
--- /dev/null
+++ b/tests/actions/addon/custom-include/addon.lua
@@ -0,0 +1,2 @@
+addon("custom-include")
+ set_description("the addon which provides a custom includes file")
diff --git a/tests/actions/addon/custom-include/includes/check/xmake.lua b/tests/actions/addon/custom-include/includes/check/xmake.lua
new file mode 100644
index 000000000..953f7ba73
--- /dev/null
+++ b/tests/actions/addon/custom-include/includes/check/xmake.lua
@@ -0,0 +1,4 @@
+option("myoption")
+ set_default(true)
+ set_description("The option of the custom-include addon.")
+print("custom-include: includes check is loaded")
diff --git a/tests/actions/addon/custom-module/addon.lua b/tests/actions/addon/custom-module/addon.lua
new file mode 100644
index 000000000..604dbc645
--- /dev/null
+++ b/tests/actions/addon/custom-module/addon.lua
@@ -0,0 +1,2 @@
+addon("custom-module")
+ set_description("the addon which provides a custom module")
diff --git a/tests/actions/addon/custom-module/modules/greeting.lua b/tests/actions/addon/custom-module/modules/greeting.lua
new file mode 100644
index 000000000..843dddfc2
--- /dev/null
+++ b/tests/actions/addon/custom-module/modules/greeting.lua
@@ -0,0 +1,3 @@
+function main(name)
+ return "hello from custom-module: " .. name
+end
diff --git a/tests/actions/addon/custom-plugin/addon.lua b/tests/actions/addon/custom-plugin/addon.lua
new file mode 100644
index 000000000..ba4515c78
--- /dev/null
+++ b/tests/actions/addon/custom-plugin/addon.lua
@@ -0,0 +1,2 @@
+addon("custom-plugin")
+ set_description("the addon which provides a custom plugin")
diff --git a/tests/actions/addon/custom-plugin/plugins/hello_addon/main.lua b/tests/actions/addon/custom-plugin/plugins/hello_addon/main.lua
new file mode 100644
index 000000000..0cf3a0386
--- /dev/null
+++ b/tests/actions/addon/custom-plugin/plugins/hello_addon/main.lua
@@ -0,0 +1,5 @@
+import("core.base.option")
+
+function main()
+ print("hello from custom-plugin: %s", option.get("name") or "world")
+end
diff --git a/tests/actions/addon/custom-plugin/plugins/hello_addon/xmake.lua b/tests/actions/addon/custom-plugin/plugins/hello_addon/xmake.lua
new file mode 100644
index 000000000..7976a1d0e
--- /dev/null
+++ b/tests/actions/addon/custom-plugin/plugins/hello_addon/xmake.lua
@@ -0,0 +1,5 @@
+task("hello_addon")
+ set_category("plugin")
+ set_menu {usage = "xmake hello_addon", description = "Say hello from the addon.",
+ options = {{'n', "name", "kv", nil, "Set the name."}}}
+ on_run("main")
diff --git a/tests/actions/addon/custom-rule/addon.lua b/tests/actions/addon/custom-rule/addon.lua
new file mode 100644
index 000000000..2e9b68ab5
--- /dev/null
+++ b/tests/actions/addon/custom-rule/addon.lua
@@ -0,0 +1,2 @@
+addon("custom-rule")
+ set_description("the addon which provides a custom rule")
diff --git a/tests/actions/addon/custom-rule/modules/greeting.lua b/tests/actions/addon/custom-rule/modules/greeting.lua
new file mode 100644
index 000000000..f247d21ca
--- /dev/null
+++ b/tests/actions/addon/custom-rule/modules/greeting.lua
@@ -0,0 +1,3 @@
+function main(name)
+ return "hello from custom-rule: " .. name
+end
diff --git a/tests/actions/addon/custom-rule/rules/hello/xmake.lua b/tests/actions/addon/custom-rule/rules/hello/xmake.lua
new file mode 100644
index 000000000..07638387d
--- /dev/null
+++ b/tests/actions/addon/custom-rule/rules/hello/xmake.lua
@@ -0,0 +1,6 @@
+rule("hello")
+ on_load(function (target)
+ -- the addon code refers to its own resources with `@self`
+ import("@self.greeting")
+ print("custom-rule: %s", greeting(target:name()))
+ end)
diff --git a/tests/actions/addon/custom-template/addon.lua b/tests/actions/addon/custom-template/addon.lua
new file mode 100644
index 000000000..f01eda9c9
--- /dev/null
+++ b/tests/actions/addon/custom-template/addon.lua
@@ -0,0 +1,2 @@
+addon("custom-template")
+ set_description("the addon which provides a custom project template")
diff --git a/tests/actions/addon/custom-template/templates/c/customaddon/hello/src/main.c b/tests/actions/addon/custom-template/templates/c/customaddon/hello/src/main.c
new file mode 100644
index 000000000..115a2a792
--- /dev/null
+++ b/tests/actions/addon/custom-template/templates/c/customaddon/hello/src/main.c
@@ -0,0 +1,6 @@
+#include <stdio.h>
+
+int main(int argc, char** argv) {
+ printf("hello from the custom-template addon!\n");
+ return 0;
+}
diff --git a/tests/actions/addon/custom-template/templates/c/customaddon/hello/xmake.lua b/tests/actions/addon/custom-template/templates/c/customaddon/hello/xmake.lua
new file mode 100644
index 000000000..33c36ec7e
--- /dev/null
+++ b/tests/actions/addon/custom-template/templates/c/customaddon/hello/xmake.lua
@@ -0,0 +1,5 @@
+add_rules("mode.debug", "mode.release")
+
+target("${TARGET_NAME}")
+ set_kind("binary")
+ add_files("src/*.c")
diff --git a/tests/actions/addon/custom-toolchain/addon.lua b/tests/actions/addon/custom-toolchain/addon.lua
new file mode 100644
index 000000000..8537b3272
--- /dev/null
+++ b/tests/actions/addon/custom-toolchain/addon.lua
@@ -0,0 +1,6 @@
+addon("custom-toolchain")
+ set_description("the addon which provides a custom toolchain")
+ set_sourcedir("src")
+ -- the tool modules are imported with their plain names by the internal calls,
+ -- e.g. find_tool("mycl6x"), import("core.tools.mycl6x")
+ add_globalmodules("core.tools.mycl6x", "detect.tools.find_mycl6x")
diff --git a/tests/actions/addon/custom-toolchain/src/modules/core/tools/mycl6x.lua b/tests/actions/addon/custom-toolchain/src/modules/core/tools/mycl6x.lua
new file mode 100644
index 000000000..972242c98
--- /dev/null
+++ b/tests/actions/addon/custom-toolchain/src/modules/core/tools/mycl6x.lua
@@ -0,0 +1,12 @@
+-- the tool module of the my-c6000 toolchain
+--
+-- @note our compiler behaves like gcc, it's just a wrapper of the host one, so we only
+-- inherit it here, a real one implements the tool interfaces itself, e.g. `init`,
+-- `nf_define`, `compargv`, `link`, @see tests/apis/custom_toolchain
+--
+inherit("core.tools.gcc")
+
+-- init it
+function init(self)
+ self:set("cxflags", "-DMY_C6000_TOOL")
+end
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
new file mode 100644
index 000000000..c0ed37914
--- /dev/null
+++ b/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua
@@ -0,0 +1,15 @@
+import("lib.detect.find_program")
+
+-- find the compiler of the my-c6000 toolchain
+--
+-- @note it's just the host compiler, we only need a real program to build with,
+-- the point of this test is that this finder itself comes from the addon
+--
+function main(opt)
+ for _, name in ipairs({"gcc", "clang", "cc"}) do
+ local program = find_program(name, opt)
+ if program then
+ return program
+ end
+ end
+end
diff --git a/tests/actions/addon/custom-toolchain/src/toolchains/my-c6000/xmake.lua b/tests/actions/addon/custom-toolchain/src/toolchains/my-c6000/xmake.lua
new file mode 100644
index 000000000..9b023bac7
--- /dev/null
+++ b/tests/actions/addon/custom-toolchain/src/toolchains/my-c6000/xmake.lua
@@ -0,0 +1,19 @@
+-- a custom toolchain distributed as an addon
+--
+-- @see tests/apis/custom_toolchain for the same toolchain maintained inside a project,
+-- distributing it as an addon is the recommended way
+--
+toolchain("my-c6000")
+ set_kind("standalone")
+ set_description("the custom toolchain of the tests")
+
+ set_toolset("cc", "mycl6x")
+ set_toolset("ld", "mycl6x")
+
+ on_check(function (toolchain)
+ return import("lib.detect.find_tool")("mycl6x")
+ end)
+
+ on_load(function (toolchain)
+ toolchain:add("cxflags", "-DMY_C6000")
+ 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..43509bfeb
--- /dev/null
+++ b/tests/actions/addon/projects/autofetch-build/src/main.c
@@ -0,0 +1,14 @@
+// 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
+#ifndef MY_C6000_TOOL
+# error the tool module 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
new file mode 100644
index 000000000..192230b08
--- /dev/null
+++ b/tests/actions/addon/test.lua
@@ -0,0 +1,450 @@
+import("core.base.global")
+import("lib.detect.find_program")
+
+-- the addon fixtures, each of them provides one kind of payload only
+--
+-- @note they are independent from each other, @see tests/actions/addon/custom-*
+
+function _addondir(name)
+ return path.join(os.scriptdir(), name)
+end
+
+function _remove(name)
+ try { function () os.runv("xmake", {"addon", "--remove", "--force", name}) end }
+end
+
+-- install the given addon fixture from its local directory
+function _install(name)
+ _remove(name)
+ os.runv("xmake", {"addon", "--install", _addondir(name)})
+end
+
+-- run the given function with the addons installed, we always remove them again
+function _with_addons(names, func)
+ for _, name in ipairs(names) do
+ _install(name)
+ end
+ try
+ {
+ func,
+ finally
+ {
+ -- @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
+ }
+ }
+end
+
+-- create a temporary repository which indexes the given addon fixtures, and register it
+--
+-- @note the recipes only point at the fixtures with `set_sourcedir`, we need not generate any payload
+function _with_repo(recipes, func)
+ local suffix = path.filename(os.tmpfile()):gsub("[^%w]", "")
+ local reponame = "addon-test-repo-" .. suffix
+ local repodir = os.tmpfile() .. ".addon-repo"
+ for name, body in pairs(recipes) do
+ io.writefile(path.join(repodir, "addons", name:sub(1, 1), name, "xmake.lua"),
+ ("package(%q)\n set_kind(\"addon\")\n set_description(\"the addon fixture of the tests\")\n %s\n"):format(name, body))
+ end
+
+ -- register the repository and clear the quick search cache, it will be rebuilt on the next search
+ 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"))
+
+ try
+ {
+ function ()
+ func(reponame)
+ end,
+ finally
+ {
+ -- @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
+ local cache = os.isfile(cachefile) and io.load(cachefile) or {}
+ if cache.repositories then
+ cache.repositories[reponame] = nil
+ end
+ io.save(cachefile, cache)
+ os.tryrm(path.join(global.cachedir(), "quick_search"))
+ os.tryrm(repodir)
+ if not ok then
+ raise(errors)
+ end
+ end
+ }
+ }
+end
+
+-- run the given command in a temporary project and return its output
+function _run_project(content, argv, opt)
+ opt = opt or {}
+ local projectdir = os.tmpfile() .. ".addon-project"
+ os.tryrm(projectdir)
+ io.writefile(path.join(projectdir, "xmake.lua"), content)
+ for file, filecontent in pairs(opt.files or {}) do
+ io.writefile(path.join(projectdir, file), filecontent)
+ end
+ local oldir = os.cd(projectdir)
+ local out, errors
+ try
+ {
+ function () out = os.iorunv("xmake", argv) end,
+ catch { function (e) errors = e end },
+ finally
+ {
+ function ()
+ os.cd(oldir)
+ os.tryrm(projectdir)
+ end
+ }
+ }
+ if errors then
+ raise(errors)
+ end
+ return out
+end
+
+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 ()
+
+ -- it has the `src` layout, e.g. set_sourcedir("src")
+ local installdir = path.join(global.directory(), "addons", "custom-toolchain", "latest")
+ t:require(os.isdir(path.join(installdir, "toolchains")))
+ t:require(os.isdir(path.join(installdir, "modules")))
+ for _, ourfile in ipairs({"src", "addon.lua"}) do
+ t:require_not(os.exists(path.join(installdir, ourfile)))
+ end
+
+ -- the addon describes itself, we should get its description from the manifest
+ t:require(os.iorunv("xmake", {"addon", "--list"}):find("the addon which provides a custom toolchain", 1, true))
+ end)
+end
+
+-- an addon can provide a plugin, e.g. xmake hello_addon
+function test_plugin(t)
+ _with_addons({"custom-plugin"}, function ()
+ t:require(os.iorunv("xmake", {"hello_addon", "-n", "xmake"}):find("hello from custom-plugin: xmake", 1, true))
+ end)
+
+ -- it should not be runnable after removing it
+ t:require_not(try { function () os.runv("xmake", {"hello_addon"}); return true end })
+end
+
+-- an addon can provide a rule, e.g. add_rules("@addon/custom-rule/hello")
+function test_rule(t)
+ local projectfile = [[
+target("test")
+ set_kind("phony")
+ add_rules("@addon/custom-rule/hello")
+]]
+ _with_addons({"custom-rule"}, function ()
+ -- the rule imports a module of its own addon with `@self`
+ t:require(_config_project(projectfile):find("custom-rule: hello from custom-rule: test", 1, true))
+ end)
+
+ -- it should fail if the addon which provides it is not installed
+ t:require_not(try { function () _config_project(projectfile); return true end })
+end
+
+-- an addon can provide a module, e.g. import("@addon.custom-module.greeting")
+function test_module(t)
+ _with_addons({"custom-module"}, function ()
+ local script = "import(\"@addon.custom-module.greeting\"); print(greeting(\"xmake\"))"
+ t:require(os.iorunv("xmake", {"lua", "-c", script}):find("hello from custom-module: xmake", 1, true))
+
+ -- the addon modules are namespaced, they cannot be imported with their plain names
+ t:require_not(try { function () os.runv("xmake", {"lua", "-c", "import(\"greeting\")"}); return true end })
+ end)
+end
+
+-- an addon can provide an includes file, e.g. includes("@addon/custom-include/check")
+function test_include(t)
+ _with_addons({"custom-include"}, function ()
+ local out = _config_project([[
+includes("@addon/custom-include/check")
+target("test")
+ set_kind("phony")
+]])
+ t:require(out:find("custom-include: includes check is loaded", 1, true))
+ end)
+end
+
+-- an addon can provide a project template, e.g. xmake create -t customaddon.hello
+function test_template(t)
+ _with_addons({"custom-template"}, function ()
+ t:require(os.iorunv("xmake", {"create", "--list", "-l", "c"}):find("customaddon.hello", 1, true))
+
+ local projectdir = os.tmpfile() .. ".addon-project"
+ os.tryrm(projectdir)
+ os.runv("xmake", {"create", "-l", "c", "-t", "customaddon.hello", "-P", projectdir})
+ t:require(os.isfile(path.join(projectdir, "src", "main.c")))
+ os.tryrm(projectdir)
+ end)
+end
+
+-- an addon can provide a toolchain and its tool modules
+--
+-- @see tests/apis/custom_toolchain for the same toolchain maintained inside a project
+function test_toolchain(t)
+ _with_addons({"custom-toolchain"}, function ()
+
+ -- the toolchain can be loaded with the addon name
+ local script = "import(\"core.tool.toolchain\"); print(toolchain.load(\"@addon/custom-toolchain/my-c6000\"):get(\"description\"))"
+ t:require(os.iorunv("xmake", {"lua", "-c", script}):find("the custom toolchain of the tests", 1, true))
+
+ -- its tool modules are exported by the manifest, so the internal calls can import them
+ -- with their plain names, e.g. add_globalmodules("core.tools.mycl6x")
+ --
+ -- @note there is no builtin `mycl6x`, so they can only come from this addon
+ local script2 = "import(\"core.tools.mycl6x\"); assert(mycl6x.compargv, \"invalid tool module!\"); " ..
+ "import(\"lib.detect.find_tool\"); assert(find_tool(\"mycl6x\"), \"mycl6x not found!\")"
+ os.runv("xmake", {"lua", "-c", script2})
+
+ -- and a project can build with it, the flags of the toolchain and of its tool module
+ -- are both checked by the source file
+ --
+ -- @note its compiler is just the host one, so we can only build with it if there is one
+ if find_program("gcc") or find_program("clang") or find_program("cc") then
+ _run_project([[
+target("test")
+ set_kind("binary")
+ set_toolchains("@addon/custom-toolchain/my-c6000")
+ add_files("src/main.c")
+]], {"build", "-y"}, {files = {["src/main.c"] = [[
+#ifndef MY_C6000
+# error the flags of the addon toolchain are not used!
+#endif
+#ifndef MY_C6000_TOOL
+# error the flags of its tool module are not used!
+#endif
+int main(int argc, char** argv) { return 0; }
+]]}})
+ end
+ 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")),
+ ["custom-module"] = ("set_sourcedir(%q)"):format(_addondir("custom-module"))}
+ _with_repo(recipes, function (reponame)
+ os.runv("xmake", {"addon", "--install", "-y", "custom-plugin"})
+ t:require(os.iorunv("xmake", {"hello_addon"}):find("hello from custom-plugin", 1, true))
+
+ os.runv("xmake", {"addon", "--remove", "custom-plugin"})
+ os.runv("xmake", {"addon", "--install", "-y", reponame .. "@custom-plugin"})
+ 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("xmake", {"lua", "private.xrepo", "search", "custom-plugin"}):find("custom-plugin", 1, true))
+
+ -- and several of them can be installed in one shot, they are resolved and installed together
+ os.runv("xmake", {"addon", "--remove", "--force", "custom-plugin"})
+ os.runv("xmake", {"addon", "--install", "-y", "custom-plugin", reponame .. "@custom-module"})
+ t:require(os.iorunv("xmake", {"hello_addon"}):find("hello from custom-plugin", 1, true))
+ t:require(os.iorunv("xmake", {"lua", "-c", "import(\"@addon.custom-module.greeting\"); print(greeting(\"xmake\"))"})
+ :find("hello from custom-module: xmake", 1, true))
+ end)
+end
+
+-- an addon can depend on the other addons, they are installed and activated together
+function test_addon_deps(t)
+ local recipes = {
+ ["custom-module"] = ("set_sourcedir(%q)"):format(_addondir("custom-module")),
+ ["custom-plugin"] = ("set_sourcedir(%q)\n add_deps(\"custom-module\", {kind = \"addon\"})"):format(_addondir("custom-plugin"))
+ }
+ _with_repo(recipes, function ()
+ os.runv("xmake", {"addon", "--install", "-y", "custom-plugin"})
+ local out = os.iorunv("xmake", {"addon", "--list"})
+ t:require(out:find("custom-plugin", 1, true))
+ t:require(out:find("custom-module", 1, true))
+
+ -- we cannot remove the dependency, it's depended on by the other addon
+ t:require_not(try { function () os.runv("xmake", {"addon", "--remove", "custom-module"}); return true end })
+ end)
+end
+
+-- the plugins and the templates are not namespaced, the conflicts should be rejected when installing
+function test_install_conflicts(t)
+ _with_addons({"custom-plugin"}, function ()
+
+ -- this addon provides the same plugin name
+ local clonedir = os.tmpfile() .. ".addon-clone"
+ os.tryrm(clonedir)
+ io.writefile(path.join(clonedir, "addon.lua"), "addon(\"custom-plugin-clone\")\n")
+ io.writefile(path.join(clonedir, "plugins", "hello_addon", "xmake.lua"),
+ "task(\"hello_addon\")\n set_category(\"plugin\")\n set_menu {}\n on_run(function () end)\n")
+ t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", clonedir}); return true end })
+ os.tryrm(clonedir)
+
+ -- and the other commands should still work
+ t:require(os.iorunv("xmake", {"hello_addon"}):find("hello from custom-plugin", 1, true))
+ end)
+end
+
+-- the invalid installs should fail, and the `addon` name is reserved for the addon references
+function test_invalid(t)
+ t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "addon-test-missing"}); return true end })
+ t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "somerepo@.."}); return true end })
+
+ -- the addon name in its manifest must match the package name which distributes it
+ local recipes = {["custom-plugin-badname"] = ("set_sourcedir(%q)"):format(_addondir("custom-plugin"))}
+ _with_repo(recipes, function ()
+ t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "custom-plugin-badname"}); return true end })
+ end)
+
+ -- the `addon` name is reserved
+ t:require_not(try { function () _config_project([[
+add_requires("addon")
+target("test")
+ set_kind("phony")
+]]); return true end })
+end
diff --git a/tests/plugins/repository/test.lua b/tests/plugins/repository/test.lua
deleted file mode 100644
index 7b1619d5e..000000000
--- a/tests/plugins/repository/test.lua
+++ /dev/null
@@ -1,118 +0,0 @@
-import("core.base.global")
-
--- write a minimal plugin that prints its name when run
-function _write_plugin(dir, name)
- io.writefile(path.join(dir, "xmake.lua"), string.format([[
-task("%s")
- set_category("plugin")
- on_run("main")
- set_menu {usage = "xmake %s", description = "say hello from %s"}
-]], name, name, name))
- io.writefile(path.join(dir, "main.lua"), string.format([[function main() print("%s") end]], name))
-end
-
--- write a plugin package description, the plugin sources are placed in its `src` directory
---
--- plugins in a repository are described as packages, e.g. <repodir>/plugins/<first-letter>/<name>/xmake.lua
-function _write_plugin_package(dir, name)
- io.writefile(path.join(dir, "xmake.lua"), string.format([[
-package("%s")
- set_kind("plugin")
- set_description("say hello from %s")
- set_sourcedir(path.join(os.scriptdir(), "src"))
-]], name, name))
- _write_plugin(path.join(dir, "src"), name)
-end
-
--- create a temporary plugin repository (packages layout: plugins/<first-letter>/<name>) and register it
---
--- @return reponame, names, cleanup
-function _mock_repo(basenames)
- local suffix = path.filename(os.tmpfile()):gsub("[^%w]", "")
- local reponame = "plugin-test-repo-" .. suffix
- local repodir = os.tmpfile() .. ".plugin-repo"
- local names = {}
- for _, base in ipairs(basenames) do
- local name = base .. "-" .. suffix
- _write_plugin_package(path.join(repodir, "plugins", name:sub(1, 1), name), name)
- table.insert(names, name)
- end
-
- -- register the repository into the cache
- local cachefile = path.join(global.cachedir(), "repository")
- local cache = os.isfile(cachefile) and io.load(cachefile) or {}
- cache.repositories = cache.repositories or {}
- cache.repositories[reponame] = {repodir}
- io.save(cachefile, cache)
-
- local function cleanup()
- for _, name in ipairs(names) do
- os.tryrm(path.join(global.directory(), "plugins", name))
- end
- local cache = os.isfile(cachefile) and io.load(cachefile) or {}
- if cache.repositories then
- cache.repositories[reponame] = nil
- end
- io.save(cachefile, cache)
- os.tryrm(repodir)
- end
- return reponame, names, cleanup
-end
-
--- install a plugin from a repository, by plain name and by repo@name
-function test_install_from_repo(t)
- local reponame, names, cleanup = _mock_repo({"hello"})
- local name = names[1]
-
- -- install by plain name (searched across all repositories)
- os.runv("xmake", {"plugin", "--install", "-y", name})
- t:require(os.iorunv("xmake", {name}):find(name, 1, true))
-
- -- reinstall by repo@name
- os.runv("xmake", {"plugin", "--remove", name})
- os.runv("xmake", {"plugin", "--install", "-y", reponame .. "@" .. name})
- t:require(os.iorunv("xmake", {name}):find(name, 1, true))
-
- os.runv("xmake", {"plugin", "--remove", name})
- cleanup()
-end
-
--- install a plugin from a local directory, then remove it
-function test_install_from_local(t)
- local suffix = path.filename(os.tmpfile()):gsub("[^%w]", "")
- local name = "hello-local-" .. suffix
- local dir = path.join(os.tmpfile() .. ".plugin-local", name)
- _write_plugin(dir, name)
-
- os.runv("xmake", {"plugin", "--install", dir})
- t:require(os.iorunv("xmake", {name}):find(name, 1, true))
-
- -- the removed plugin should no longer be runnable
- os.runv("xmake", {"plugin", "--remove", name})
- t:require_not(try { function () os.runv("xmake", {name}); return true end })
-
- os.tryrm(path.directory(dir))
-end
-
--- --list shows the built-in, installed and available plugins
-function test_list(t)
- local reponame, names, cleanup = _mock_repo({"hello", "world"})
-
- -- install the first plugin, leave the second only available
- os.runv("xmake", {"plugin", "--install", "-y", names[1]})
- local out = os.iorunv("xmake", {"plugin", "--list"})
- t:require(out:find("the built-in plugins:", 1, true))
- t:require(out:find("project", 1, true))
- t:require(out:find(names[1], 1, true))
- t:require(out:find(names[2], 1, true))
- t:require(out:find("xmake plugin --install " .. names[2], 1, true))
-
- os.runv("xmake", {"plugin", "--remove", names[1]})
- cleanup()
-end
-
--- invalid installs should fail
-function test_install_invalid(t)
- t:require_not(try { function () os.runv("xmake", {"plugin", "--install", "-y", "plugin-test-missing"}); return true end })
- t:require_not(try { function () os.runv("xmake", {"plugin", "--install", "-y", "somerepo@.."}); return true end })
-end
diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua
new file mode 100644
index 000000000..bf6f27f5d
--- /dev/null
+++ b/xmake/actions/addon/main.lua
@@ -0,0 +1,330 @@
+--!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("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")
+
+-- validate an addon directory name
+function _check_addon_name(name)
+ assert(type(name) == "string" and name ~= "" and name ~= "." and not name:find("..", 1, true) and not name:find("[/\\:]"), "invalid addon name(%s)!", name)
+ return name
+end
+
+-- get addon directory in ~/.xmake/addons
+function _get_addondir(name, version)
+ local addondir = addon.installdir()
+ if name then
+ addondir = path.join(addondir, addon.dirname(_check_addon_name(name)))
+ if version then
+ addondir = path.join(addondir, version)
+ end
+ end
+ return addondir
+end
+
+-- install the given addons from the repositories which provide them
+--
+-- @note we install them in one shot, xrepo resolves and installs them in parallel
+--
+function _install_from_repo(names)
+ for _, name in ipairs(names) do
+ -- e.g. myrepo@myaddon
+ _check_addon_name(name:split("@", {plain = true, limit = 2})[2] or name)
+ end
+ xrepo_addon("install", names, {force = option.get("force")})
+end
+
+-- install a single addon from a source directory (as the given name, default to the directory name)
+function _install_from_local(dir, name)
+ assert(os.isdir(dir), "addon path(%s) not found!", dir)
+
+ -- we need to normalize it first, e.g. `/path/to/myaddon/` -> `/path/to/myaddon`,
+ -- otherwise we cannot get the addon name from the directory
+ dir = path.normalize(path.absolute(dir))
+
+ -- we only install the payload directories, the addon repository may have its own files,
+ -- e.g. tests, ci scripts and documents, and they can also be placed in the `src` subdirectory
+ local payloadroot = addon.payloadroot(dir)
+ assert(payloadroot, "addon path(%s): no payload directory found, e.g. ${bright}plugins${clear}!", dir)
+
+ -- the addon describes itself? its manifest is always authoritative,
+ -- otherwise we can only guess its name from the directory name
+ local manifest = addon.manifest(dir)
+ if manifest then
+ name = manifest.name
+ end
+ name = name or path.filename(dir)
+
+ -- the addons installed from a local directory or a git url have no semantic version
+ local version = "latest"
+ local dstdir = _get_addondir(name, version)
+ assert(not os.isdir(dstdir), "addon(%s) already exists!", name)
+
+ -- the addons which it depends on are not installed by the local installation,
+ -- so we need to install them from the repositories first
+ if manifest then
+ for _, dep in ipairs(manifest.deps) do
+ if not addon.addons()[addon.dirname(dep)] then
+ _install_from_repo({dep})
+ end
+ end
+ end
+
+ for _, payloaddir in ipairs(addon.payloads_of(payloadroot)) do
+ os.vcp(path.join(payloadroot, payloaddir), path.join(dstdir, payloaddir))
+ end
+
+ -- we need to roll back the installed payloads if it cannot be registered, e.g. the name conflicts
+ try
+ {
+ function ()
+ addon.register(name, version, manifest and {
+ description = manifest.description,
+ deps = #manifest.deps > 0 and manifest.deps or nil,
+ globalmodules = #manifest.globalmodules > 0 and manifest.globalmodules or nil})
+ end,
+ catch
+ {
+ function (errors)
+ os.tryrm(dstdir)
+ -- we need to remove the addon directory too if no other version is installed
+ local addondir = path.directory(dstdir)
+ if #os.filedirs(path.join(addondir, "*")) == 0 then
+ os.tryrm(addondir)
+ end
+ raise(errors)
+ end
+ }
+ }
+ cprint("${color.success}install ${bright}%s${clear} ok!", name)
+end
+
+-- install a single addon from a git url or github shortcut, e.g. https://github.com/xmake-addons/serial-monitor
+function _install_from_git(url)
+ local branch
+ if url:startswith("github:") then
+ local i = url:find("#", 1, true)
+ if i then
+ branch = url:sub(i + 1)
+ url = url:sub(1, i - 1)
+ end
+ url = git.asgiturl(url)
+ end
+ local name = (path.filename(url):gsub("%.git$", ""))
+ local tmpdir = os.tmpfile() .. ".dir"
+
+ -- we need git to clone it, it will be installed first if it's not found
+ --
+ -- @note we cannot enter it for the other install paths, it may install packages
+ -- and that needs a project, but `xmake addon` can be run anywhere
+ --
+ environment.enter()
+ try
+ {
+ function ()
+ git.clone(url, {verbose = option.get("verbose"), branch = branch, outputdir = tmpdir})
+ os.tryrm(path.join(tmpdir, ".git"))
+ _install_from_local(tmpdir, name)
+ end,
+ finally
+ {
+ -- we always need to remove the temporary clone directory,
+ -- @note try() swallows the errors if we do not re-raise them here
+ function (ok, errors)
+ os.tryrm(tmpdir)
+ if not ok then
+ raise(errors)
+ end
+ end
+ }
+ }
+ environment.leave()
+end
+
+-- does the given name come from a repository? e.g. `myaddon`, `myrepo@myaddon`
+function _is_from_repo(name)
+ -- github shortcut: github:user/repo or github:user/repo#branch
+ if name:startswith("github:") then
+ return false
+ end
+
+ -- local directory
+ --
+ -- @note we need to check it before the git url, `git.asgiturl` also accepts
+ -- the local paths with a trailing separator, e.g. `/path/to/myaddon/`
+ if os.isdir(name) then
+ return false
+ end
+ return not git.asgiturl(name)
+end
+
+-- install addons
+function _install()
+ local names = assert(option.get("addons"), "please specify the addons to be installed!")
+
+ -- the addons from the repositories are installed in one shot, the others one by one
+ local requires = {}
+ for _, name in ipairs(names) do
+ if _is_from_repo(name) then
+ table.insert(requires, name)
+ elseif os.isdir(name) then
+ _install_from_local(name)
+ else
+ _install_from_git(name)
+ end
+ end
+ if #requires > 0 then
+ _install_from_repo(requires)
+ end
+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 = 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_addon("search", patterns)
+end
+
+-- collect the installed addons from the addons registry
+function _collect_installed_addons()
+ local entries = {}
+ for name, addoninfo in table.orderpairs(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
+ return entries
+end
+
+-- print an addon entry, e.g. -> serial-monitor v1.0.1: monitor the serial port output (in xmake-repo)
+function _print_addon(entry, suffix)
+ local title = entry.name
+ if entry.version then
+ title = title .. " " .. entry.version
+ end
+ local description = entry.description and (": " .. entry.description) or ""
+ cprint(" ${color.dump.reference}->${clear} ${color.dump.string}%s${clear}%s%s", title, description, suffix or "")
+end
+
+-- get the addons in the repositories, we reuse the packages search here
+function _collect_repo_addons(exclude)
+ local entries = {}
+ for _, results in table.orderpairs(search_packages({"*"}, {kind = "addon", description = false})) do
+ for _, result in ipairs(results) do
+ if not exclude[result.name] then
+ table.insert(entries, result)
+ end
+ end
+ end
+ table.sort(entries, function (a, b) return a.name < b.name end)
+ return entries
+end
+
+-- list all addons
+function _list()
+
+ -- show the installed addons
+ local installed = _collect_installed_addons()
+ local exclude = {}
+ cprint("${bright}the installed addons:${clear}")
+ if #installed > 0 then
+ for _, entry in ipairs(installed) do
+ exclude[entry.name] = true
+ 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)")
+ end
+
+ -- show the addons in the repositories, we only show the first ones if there are too many
+ local listlimit = 10
+ local avail = _collect_repo_addons(exclude)
+ cprint("${bright}the available addons:${clear} ${dim}(run `xmake addon --install <name>` to install," ..
+ " `--search <pattern>` to search)${clear}")
+ if #avail > 0 then
+ for idx, entry in ipairs(avail) do
+ if idx > listlimit then
+ cprint(" ${dim}... and %d more${clear}", #avail - listlimit)
+ break
+ end
+ _print_addon(entry, entry.reponame and string.format(" ${dim}(in %s)${clear}", entry.reponame) or nil)
+ end
+ else
+ print(" (none)")
+ end
+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()
+ end
+end
diff --git a/xmake/actions/addon/xmake.lua b/xmake/actions/addon/xmake.lua
new file mode 100644
index 000000000..a0266aa4f
--- /dev/null
+++ b/xmake/actions/addon/xmake.lua
@@ -0,0 +1,49 @@
+--!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 xmake.lua
+--
+
+task("addon")
+ set_category("action")
+ on_run("main")
+ set_menu {
+ usage = "xmake addon [options]",
+ description = "Manage addons of xmake.",
+ options = {
+ {'i', "install", "k", nil, "Install addons."},
+ {'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."},
+ {'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.",
+ " $ xmake addon --install https://github.com/myrepo/serial-monitor",
+ " $ xmake addon --install github:myrepo/serial-monitor",
+ " $ xmake addon --install github:myrepo/serial-monitor#dev",
+ " $ xmake addon --install /tmp/my-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/actions/create/main.lua b/xmake/actions/create/main.lua
index 89a4791a6..f3135fadb 100644
--- a/xmake/actions/create/main.lua
+++ b/xmake/actions/create/main.lua
@@ -25,6 +25,19 @@ import("actions.create.template", {rootdir = os.programdir()})
-- validate template component against path traversal
function _validate_template_component(name, value)
+
+ -- the qualified template id of an addon, e.g. @addon/basic-templates/verilator.console
+ if name == "template id" and value:startswith("@addon/") then
+ local rest = value:sub(#"@addon/" + 1)
+ local pos = rest:find("/", 1, true)
+ if not pos then
+ raise("invalid %s: %s, it should be `@addon/<addon>/<template>`!", name, value)
+ end
+ _validate_template_component("addon name", rest:sub(1, pos - 1))
+ _validate_template_component("template id", rest:sub(pos + 1))
+ return
+ end
+
if #value == 0 or value == "." or value == ".."
or value:find("/", 1, true) or value:find("\\", 1, true)
or value:find(":", 1, true) or value:find("\0", 1, true) then
@@ -79,26 +92,12 @@ function _list_templates(lang_filter)
languages = {lang_filter}
end
- -- map a template directory to its root meta (repo/global/builtin)
- local rootinfo_of = function (dir)
- if not dir then
- return
- end
- dir = path.absolute(dir)
- for _, info in ipairs(rootinfos) do
- local rootdir = path.absolute(info.dir)
- if dir == rootdir or dir:startswith(rootdir .. path.sep()) then
- return info
- end
- end
- end
-
local sourcekey_of = function (info)
if not info then
return "unknown"
end
- if info.kind == "repo" then
- return "repo:" .. info.name
+ if info.kind == "addon" then
+ return info.kind .. ":" .. info.name
end
return info.kind or "unknown"
end
@@ -109,7 +108,7 @@ function _list_templates(lang_filter)
if templates and #templates > 0 then
for _, name in ipairs(templates) do
local sourcedir = template.templatedir(lang, name)
- local info = rootinfo_of(sourcedir)
+ local info = template.rootinfo_of(sourcedir)
local key = sourcekey_of(info)
local group = groups[key]
if not group then
@@ -140,9 +139,8 @@ function _list_templates(lang_filter)
local group = groups[key]
if group then
local info = group.info
- if info and info.kind == "repo" then
- local branch = info.branch and (" " .. info.branch) or ""
- cprint("${bright}%s${reset}: %s%s", info.name, info.url or "", branch)
+ if info and info.kind == "addon" then
+ cprint("${bright}%s${reset}: addon %s", info.name, info.version or "")
else
cprint("${bright}%s${reset}", (info and info.kind) or "unknown")
end
@@ -195,7 +193,7 @@ function _create_project(lang, templateid, targetname)
-- create project
local sourcedir = template.templatedir(lang, templateid)
if not sourcedir then
- raise("template(%s/%s): not found!\nyou can try:\n - xrepo update-repo (update repositories)\n - xmake create --list (show available templates)", lang, templateid)
+ raise("template(%s/%s): not found!\nyou can try:\n - xmake addon --search templates (search the addons which provide templates)\n - xmake create --list (show available templates)", lang, templateid)
end
-- get the builtin variables
diff --git a/xmake/actions/create/template.lua b/xmake/actions/create/template.lua
index e18015514..ac39e2ff8 100644
--- a/xmake/actions/create/template.lua
+++ b/xmake/actions/create/template.lua
@@ -22,7 +22,7 @@
import("core.base.global")
import("core.base.hashset")
import("core.language.language")
-import("core.package.repository")
+import("core.package.addon")
-- some builtin template variables in xmake.lua
function builtinvars(targetname)
@@ -33,20 +33,16 @@ end
-- get all template roots with extra meta information
--
-- priority:
--- 1. repo: <global-repo>/templates
+-- 1. addon: <globaldir>/addons/<name>/<version>/templates
-- 2. global: <globaldir>/templates
-- 3. builtin: <programdir>/templates
function rootinfos()
local results = {}
- -- get template directories from global repositories
- local repos = repository.repositories({global = true, network = false})
- if repos then
- for _, repo in ipairs(repos) do
- local templatesdir = path.join(repo:directory(), "templates")
- if os.isdir(templatesdir) then
- table.insert(results, {kind = "repo", name = repo:name(), url = repo:url(), branch = repo:branch(), dir = templatesdir})
- end
+ -- get template directories from the installed addons
+ for _, addoninfo in ipairs(addon.payloadinfos("templates")) do
+ if os.isdir(addoninfo.dir) then
+ table.insert(results, {kind = "addon", name = addoninfo.name, version = addoninfo.version, dir = addoninfo.dir})
end
end
@@ -62,6 +58,24 @@ function rootinfos()
return results
end
+-- get the root information of the given template directory
+--
+-- @param templatedir the template directory, e.g. <programdir>/templates/c/console
+-- @return the root information, @see rootinfos
+--
+function rootinfo_of(templatedir)
+ if not templatedir then
+ return
+ end
+ templatedir = path.absolute(templatedir)
+ for _, info in ipairs(rootinfos()) do
+ local rootdir = path.absolute(info.dir)
+ if templatedir == rootdir or templatedir:startswith(rootdir .. path.sep()) then
+ return info
+ end
+ end
+end
+
-- get template root directories
function rootdirs()
local results = {}
@@ -88,6 +102,23 @@ end
function templatedir(lang, templateid)
assert(lang)
assert(templateid)
+
+ -- the qualified template of an addon, e.g. @addon/basic-templates/verilator.console
+ --
+ -- @note the template ids are not namespaced, we only need it to disambiguate the conflicts
+ --
+ if templateid:startswith("@addon/") then
+ local referenceinfo = addon.resolve_reference(templateid, "/", "templates")
+ local subdir = _templateid_subdir(referenceinfo.name)
+ if subdir then
+ local dir = path.join(referenceinfo.dir, lang, subdir)
+ if os.isfile(path.join(dir, "xmake.lua")) then
+ return dir
+ end
+ end
+ return
+ end
+
for _, rootdir in ipairs(rootdirs()) do
local subdir = _templateid_subdir(templateid)
if subdir then
@@ -159,11 +190,21 @@ function languages()
return results
end
+-- get the reference of the given template, e.g. @addon/basic-templates/verilator.console
+function _template_reference(rootinfo, templateid)
+ if rootinfo.kind == "addon" then
+ return string.format("@addon/%s/%s", rootinfo.name, templateid)
+ end
+ return path.join(rootinfo.dir, templateid)
+end
+
-- get all templates for the given language
function templates(lang)
assert(lang)
local found = hashset.new()
- for _, rootdir in ipairs(rootdirs()) do
+ local providers = {}
+ for _, rootinfo in ipairs(rootinfos()) do
+ local rootdir = rootinfo.dir
local templateroot = path.join(rootdir, lang)
local configfiles = os.files(path.join(templateroot, "**", "xmake.lua"))
if configfiles then
@@ -187,7 +228,17 @@ function templates(lang)
end
if ok then
table.insert(accepted, item.dir)
- found:insert((item.relpath:gsub("[/\\]", ".")))
+ local templateid = (item.relpath:gsub("[/\\]", "."))
+ -- the templates are not namespaced, so we need to report the conflicts of the addons,
+ -- otherwise we do not know which template will be used
+ local provider = providers[templateid]
+ if provider and (provider.kind == "addon" or rootinfo.kind == "addon") then
+ wprint("template(%s/%s) conflicts, we will use the first one!\n -> %s\n -> %s\nplease use the qualified template id to disambiguate them.",
+ lang, templateid, _template_reference(provider, templateid), _template_reference(rootinfo, templateid))
+ else
+ providers[templateid] = rootinfo
+ found:insert(templateid)
+ end
end
end
end
diff --git a/xmake/actions/require/xmake.lua b/xmake/actions/require/xmake.lua
index 6b04f2621..b800c4528 100644
--- a/xmake/actions/require/xmake.lua
+++ b/xmake/actions/require/xmake.lua
@@ -39,7 +39,9 @@ task("require")
{nil, "linkjobs", "kv", nil, "Set the number of parallel link jobs."},
{nil, "shallow", "k", nil, "Does not install or download dependent packages."},
{nil, "build", "k", nil, "Always build and install packages from source."},
- {nil, "plugin", "k", nil, "Install plugin packages from <repository>/plugins/."},
+ {nil, "addon", "k", nil, "Install addon packages from <repository>/addons/."},
+ {nil, "plugin", "k", nil, "Install plugin packages from <repository>/plugins/.",
+ "(deprecated, please use --addon instead)"},
{'l', "list", "k", nil, "List all package dependencies in project.",
"e.g.",
" $ xmake require --list"},
diff --git a/xmake/core/base/interpreter.lua b/xmake/core/base/interpreter.lua
index 83bbdf593..01b689fe6 100644
--- a/xmake/core/base/interpreter.lua
+++ b/xmake/core/base/interpreter.lua
@@ -30,6 +30,7 @@ local string = require("base/string")
local hashset = require("base/hashset")
local scopeinfo = require("base/scopeinfo")
local deprecated = require("base/deprecated")
+local addon = require("package/addon")
local sandbox = require("sandbox/sandbox")
-- the rules to reword the raw lua error messages into friendly ones, {pattern, replacement}
@@ -883,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
@@ -1786,6 +1817,35 @@ function interpreter:api_builtin_set_xmakever(minver)
end
-- the builtin api: includes()
+-- find the include files of the builtin includes, e.g. includes("@builtin/check")
+function interpreter:_find_builtin_includes(subpath)
+ local builtin_path = subpath:sub(#"@builtin/" + 1)
+ if builtin_path:endswith(".lua") then
+ return os.files(path.join(os.programdir(), "includes", builtin_path))
+ end
+ return os.files(path.join(os.programdir(), "includes", builtin_path, "xmake.lua"))
+end
+
+-- find the include files of the addons, e.g. includes("@addon/esp32/check"), includes("@self/check")
+function interpreter:_find_addon_includes(subpath)
+ local referenceinfo, errors = addon.resolve_reference(subpath, "/", "includes", {scriptdir = self:scriptdir()})
+ if not referenceinfo then
+ os.raise(errors)
+ end
+ local addon_path = referenceinfo.name
+ local files
+ if addon_path:endswith(".lua") then
+ files = os.files(path.join(referenceinfo.dir, addon_path))
+ else
+ files = os.files(path.join(referenceinfo.dir, addon_path, "xmake.lua"))
+ end
+ -- the addon is installed, but it does not provide this file, we cannot ignore it
+ if not files or #files == 0 then
+ os.raise("includes(%s) not found!", subpath)
+ end
+ return files
+end
+
function interpreter:api_builtin_includes(...)
assert(self and self._PRIVATE and self._PRIVATE._ROOTDIR and self._PRIVATE._MTIMES)
local curfile = self._PRIVATE._CURFILE
@@ -1796,21 +1856,27 @@ 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
- local builtin_path = subpath:sub(10)
- local files
- if builtin_path:endswith(".lua") then
- files = os.files(path.join(os.programdir(), "includes", builtin_path))
- else
- files = os.files(path.join(os.programdir(), "includes", builtin_path, "xmake.lua"))
- end
+ local files = self:_find_builtin_includes(subpath)
if files and #files > 0 then
table.join2(subpaths_matched, files)
found = true
end
end
+ -- attempt to find files from the includes of the addons
+ -- e.g. includes("@addon/esp32/check"), includes("@self/check")
+ if not found and addon.is_reference(subpath, "/") then
+ table.join2(subpaths_matched, self:_find_addon_includes(subpath))
+ found = true
+ end
-- find the given files from the project directory
if not found then
local files
@@ -1818,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/os.lua b/xmake/core/base/os.lua
index 005f18a70..89be6d638 100644
--- a/xmake/core/base/os.lua
+++ b/xmake/core/base/os.lua
@@ -423,8 +423,14 @@ function os.match(pattern, mode, opt)
return os._async_task().match(pattern, mode)
end
- -- extract callback
- local callback = type(opt) == "function" and opt or (type(opt) == "table" and opt.callback or nil)
+ -- extract callback and the maximum recursion level
+ local callback, maxrecursion
+ if type(opt) == "function" then
+ callback = opt
+ elseif type(opt) == "table" then
+ callback = opt.callback
+ maxrecursion = opt.recursion
+ end
-- support path instance
pattern = tostring(pattern)
@@ -493,7 +499,9 @@ function os.match(pattern, mode, opt)
-- limit recursion level: src/*/*.c
local recursion = 0
if pattern:find("**", 1, true) then
- recursion = -1
+ -- we can also limit the recursion level of `**`, it may be very slow
+ -- in a deep directory tree, e.g. os.files("src/**.c", {recursion = 2})
+ recursion = maxrecursion or -1
else
-- "src/*/*.c" -> "*/" -> recursion level: 1
-- "src/*/main.c" -> "*/" -> recursion level: 1
@@ -504,6 +512,9 @@ function os.match(pattern, mode, opt)
recursion = seps
end
end
+ if maxrecursion and recursion > maxrecursion then
+ recursion = maxrecursion
+ end
end
-- convert pattern to a lua pattern
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 d6f581cf9..2cd1fa58a 100644
--- a/xmake/core/base/task.lua
+++ b/xmake/core/base/task.lua
@@ -28,6 +28,7 @@ local string = require("base/string")
local global = require("base/global")
local hashset = require("base/hashset")
local interpreter = require("base/interpreter")
+local addon = require("package/addon")
local sandbox = require("sandbox/sandbox")
local config = require("project/config")
local sandbox_os = require("sandbox/modules/os")
@@ -81,13 +82,20 @@ end
-- the directories of tasks
function task._directories()
- local dirs = {
- path.join(global.directory(), "plugins"),
- path.join(os.programdir(), "plugins"),
- path.join(os.programdir(), "actions")}
- local plugindirs = os.getenv("XMAKE_PLUGIN_DIRS")
- if plugindirs then
- table.insert(dirs, 1, plugindirs)
+ local dirs = task._DIRECTORIES
+ if dirs == nil then
+ dirs = {
+ path.join(global.directory(), "plugins"),
+ path.join(os.programdir(), "plugins"),
+ path.join(os.programdir(), "actions")}
+
+ -- add the plugins of the installed addons, e.g. ~/.xmake/addons/<name>/<version>/plugins
+ --
+ -- we get them from the addons registry file directly,
+ -- so we do not need to scan the whole addons directory on startup
+ --
+ table.join2(dirs, addon.payloads("plugins"))
+ task._DIRECTORIES = dirs
end
return dirs
end
@@ -391,6 +399,39 @@ function task.new(name, info)
return instance
end
+-- is the given plugin conflicting with the loaded one?
+--
+-- the plugins are not namespaced, so we need to report the conflicts of the addons,
+-- otherwise we do not know which plugin will be run
+--
+-- @param taskname the task name
+-- @param taskfile the task file of the loaded plugin, it will be nil if it's the first one
+-- @param filepath the task file of the plugin which we are loading
+--
+function task._is_conflicting(taskname, taskfile, filepath)
+ if not taskfile then
+ return false
+ end
+
+ -- we only report it if one of them comes from an addon, the builtin plugins
+ -- and the plugins in the global directory are always overridable
+ local addondir = path.absolute(addon.installdir())
+ if not path.absolute(taskfile):startswith(addondir) and not path.absolute(filepath):startswith(addondir) then
+ return false
+ end
+
+ -- @note we cannot raise errors here, otherwise all the commands will be broken,
+ -- and the user cannot even remove the conflicting addons
+ utils.warning("plugin(%s) conflicts, we will use the first one!\n -> %s\n -> %s", 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, ...}
@@ -402,6 +443,7 @@ function task.tasks()
-- load tasks
local tasks = {}
+ local taskfiles = {}
local dirs = task._directories()
for _, dir in ipairs(dirs) do
local files = os.files(path.join(dir, "*", "xmake.lua"))
@@ -409,7 +451,12 @@ function task.tasks()
for _, filepath in ipairs(files) do
local results, errors = task._load(filepath)
if results then
- table.join2(tasks, results)
+ for taskname, taskinfo in pairs(results) do
+ if not task._is_conflicting(taskname, taskfiles[taskname], filepath) then
+ taskfiles[taskname] = filepath
+ tasks[taskname] = taskinfo
+ end
+ end
else
os.raise(errors)
end
diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua
new file mode 100644
index 000000000..15786c62f
--- /dev/null
+++ b/xmake/core/package/addon.lua
@@ -0,0 +1,774 @@
+--!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 addon.lua
+--
+
+-- define module
+local addon = addon or {}
+
+-- load modules
+local os = require("base/os")
+local io = require("base/io")
+local path = require("base/path")
+local table = require("base/table")
+local utils = require("base/utils")
+local global = require("base/global")
+
+-- the payload directories of an addon
+--
+-- an addon can provide any subset of them, e.g. only `plugins`
+--
+-- @note only `plugins` is activated for now, the others are reserved
+--
+function addon._payloaddirs()
+ return {"plugins", "rules", "toolchains", "platforms", "modules", "templates", "themes", "includes"}
+end
+
+-- the manifest file of an addon, e.g. <sourcedir>/addon.lua
+--
+-- an addon describes itself in this file, so its name and layout never depend on
+-- the package name of the repository which distributes it
+--
+function addon._manifestfile(sourcedir)
+ return path.join(sourcedir, "addon.lua")
+end
+
+-- the interpreter of the addon manifest
+function addon._interpreter()
+ local interp = addon._INTERPRETER
+ if interp == nil then
+ -- we need to load it lazily, the interpreter also depends on this module
+ local interpreter = require("base/interpreter")
+ interp = interpreter.new()
+ interp:api_define(addon.apis())
+ addon._INTERPRETER = interp
+ end
+ return interp
+end
+
+-- the registry file of the installed addons, e.g. ~/.xmake/addons/addons.conf
+--
+-- we save all installed addons to this file when installing/removing them,
+-- so we do not need to scan the whole addons directory on startup
+--
+function addon._registryfile()
+ return path.join(addon.installdir(), "addons.conf")
+end
+
+-- 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 table.empty(registry) and not os.isfile(registryfile) then
+ return
+ end
+ local ok, errors = io.save(registryfile, registry)
+ if not ok then
+ utils.warning(errors)
+ end
+end
+
+-- get the payload directory of the given addon
+--
+-- @param name the addon name, e.g. "esp32"
+-- @param kind the payload kind, e.g. "rules", "modules"
+-- @return the directory, e.g. ~/.xmake/addons/esp32/v1.0.0/rules
+--
+function addon._payloaddir(name, kind)
+ local dirname = addon.dirname(name)
+ local addoninfo = addon.addons()[dirname]
+ if addoninfo and table.contains(addoninfo.payloads or {}, kind) then
+ return path.join(addon.installdir(), dirname, addoninfo.version, kind)
+ end
+end
+
+-- get the plugin task names of the given addon directory
+--
+-- the plugins are not namespaced, we need them to check the conflicts
+--
+function addon._plugins_of(addondir)
+ local plugins = {}
+ for _, filepath in ipairs(os.files(path.join(addondir, "plugins", "*", "xmake.lua"))) do
+ local content = io.readfile(filepath)
+ if content then
+ for taskname in content:gmatch("task%s*%(%s*\"(.-)\"") do
+ table.insert(plugins, taskname)
+ end
+ end
+ end
+ return plugins
+end
+
+-- get the template ids of the given addon directory, e.g. {"c/console"}
+--
+-- the templates are not namespaced, we need them to check the conflicts
+--
+function addon._templates_of(addondir)
+ local templates = {}
+ local templatesdir = path.join(addondir, "templates")
+ for _, langdir in ipairs(os.dirs(path.join(templatesdir, "*"))) do
+ local lang = path.filename(langdir)
+ local accepted = {}
+ for _, filepath in ipairs(os.files(path.join(langdir, "**", "xmake.lua"))) do
+ local dir = path.directory(filepath)
+ local relpath = path.relative(dir, langdir)
+ if relpath and relpath ~= "." then
+ local nested = false
+ for _, root in ipairs(accepted) do
+ if dir:startswith(root .. path.sep()) then
+ nested = true
+ break
+ end
+ end
+ if not nested then
+ table.insert(accepted, dir)
+ table.insert(templates, lang .. "/" .. (relpath:gsub("[/\\]", ".")))
+ end
+ end
+ end
+ end
+ return templates
+end
+
+-- check the conflicts of the plugins and templates, they are not namespaced
+--
+-- @param dirname the addon directory name
+-- @param addoninfo the addon information, @see addon.register
+--
+-- @return the errors if there are some conflicts
+--
+function addon._check_conflicts(dirname, addoninfo)
+ local kindnames = {plugins = "plugin", templates = "template", globalmodules = "global module"}
+ for _, kind in ipairs({"plugins", "templates", "globalmodules"}) do
+ for _, name in ipairs(addoninfo[kind] or {}) do
+ for otherdirname, otheraddoninfo in pairs(addon.addons()) do
+ if otherdirname ~= dirname and table.contains(otheraddoninfo[kind] or {}, name) then
+ return string.format("%s(%s) conflicts, it has been provided by the addon(%s)!\nplease remove one of them, e.g. xmake addon --remove %s",
+ kindnames[kind], name, otherdirname, otherdirname)
+ end
+ end
+ end
+ end
+
+ -- the global modules can also conflict with the builtin and the user modules
+ for _, name in ipairs(addoninfo.globalmodules or {}) do
+ local modulepath = (name:gsub("%.", "/")) .. ".lua"
+ for _, moduledir in ipairs({os.programdir(), global.directory()}) do
+ if os.isfile(path.join(moduledir, "modules", modulepath)) then
+ return string.format("global module(%s) conflicts, it has been provided by %s!\nplease rename it in the addon manifest.",
+ name, moduledir == os.programdir() and "xmake" or path.join(moduledir, "modules"))
+ end
+ end
+ end
+end
+
+-- get the addons which depend on the given addon
+function addon._parents(name)
+ local dirname = addon.dirname(name)
+ local parents
+ for otherdirname, entry in table.orderpairs(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
+ return parents
+end
+
+-- unregister the given addon or only one of its versions
+function addon._unregister(name, version)
+ local dirname = addon.dirname(name)
+ 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
+function addon.apis()
+ return {
+ values = {
+ -- addon.set_xxx
+ "addon.set_description"
+ , "addon.set_homepage"
+ , "addon.set_license"
+ , "addon.set_sourcedir"
+ -- addon.add_xxx
+ , "addon.add_deps"
+ , "addon.add_globalmodules"
+ }
+ }
+end
+
+-- get the manifest of the given addon directory
+--
+-- @param sourcedir the addon source or install directory, which contains `addon.lua`
+--
+-- @return the manifest, e.g. {name = "esp32-devel", description = "...", sourcedir = "src", deps = {"serial-tools"}}
+-- it will be nil if this addon does not describe itself
+--
+function addon.manifest(sourcedir)
+
+ -- we may resolve a lot of `@self` references, so we need to cache them
+ local manifests = addon._MANIFESTS
+ if manifests == nil then
+ manifests = {}
+ addon._MANIFESTS = manifests
+ end
+ local cachekey = path.absolute(sourcedir)
+ local cacheinfo = manifests[cachekey]
+ if cacheinfo ~= nil then
+ return cacheinfo or nil
+ end
+
+ local manifestfile = addon._manifestfile(sourcedir)
+ if not os.isfile(manifestfile) then
+ manifests[cachekey] = false
+ return
+ end
+ local interp = addon._interpreter()
+ local ok, errors = interp:load(manifestfile)
+ if not ok then
+ return nil, errors
+ end
+ local results, errors = interp:make("addon", true, true)
+ if not results then
+ return nil, errors
+ end
+ local manifest
+ for name, addoninfo in pairs(results) do
+ if manifest then
+ return nil, string.format("%s: only one addon() scope is allowed!", manifestfile)
+ end
+ manifest = {name = name,
+ description = addoninfo:get("description"),
+ homepage = addoninfo:get("homepage"),
+ license = addoninfo:get("license"),
+ sourcedir = addoninfo:get("sourcedir"),
+ deps = table.wrap(addoninfo:get("deps")),
+ globalmodules = table.wrap(addoninfo:get("globalmodules"))}
+ end
+ if not manifest then
+ return nil, string.format("%s: no addon() scope found!", manifestfile)
+ end
+ manifests[cachekey] = manifest
+ 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")
+end
+
+-- get the directory name of the given addon name, e.g. "myns::foo" -> "myns_foo"
+function addon.dirname(name)
+ return (name:lower():gsub("::", "_"))
+end
+
+-- is the given reference an addon reference?
+--
+-- e.g. "@addon/esp32/flash", "@self/flash", "@addon.esp32.sdkconfig", "@self.sdkconfig"
+--
+function addon.is_reference(reference, sep)
+ return reference:startswith("@addon" .. sep) or reference:startswith("@self" .. sep)
+end
+
+-- get the addon which owns the given script directory
+--
+-- it's used to resolve the `@self` references inside an addon,
+-- so that the addon code never needs to know its own installed name
+--
+-- @param scriptdir the script directory, e.g. ~/.xmake/addons/esp32/v1.0.0/rules/flash
+-- it will be the directory of the caller script by default
+-- @return the addon name and its root directory, e.g. esp32, ~/.xmake/addons/esp32/v1.0.0
+--
+function addon.owner(scriptdir)
+ if not scriptdir then
+ -- we can get it from the sandbox of the caller script, e.g. the rule script of an addon
+ local sandbox = require("sandbox/sandbox")
+ local instance = sandbox.instance()
+ scriptdir = instance and instance:rootdir()
+ end
+ if not scriptdir then
+ return
+ end
+ scriptdir = path.absolute(scriptdir)
+
+ -- the installed addons, e.g. ~/.xmake/addons/<name>/<version>/...
+ local installdir = path.absolute(addon.installdir())
+ if scriptdir:startswith(installdir .. path.sep()) then
+ local parts = path.split(path.relative(scriptdir, installdir))
+ if #parts >= 2 then
+ local addondir = path.join(installdir, parts[1], parts[2])
+ -- the registry keeps the raw addon name, the directory name is only
+ -- its normalized form, e.g. "myns::foo" -> "myns_foo"
+ local addoninfo = addon.addons()[parts[1]]
+ return addoninfo and addoninfo.name or parts[1], addondir
+ end
+ return
+ end
+
+ -- the addon source directory, we can also run the addon code in place when developing it
+ local dir = scriptdir
+ while dir and #dir > 0 do
+ -- the addon describes itself? we get its name from the manifest
+ local manifest = addon.manifest(dir)
+ if manifest then
+ return manifest.name, dir
+ end
+ -- otherwise we can only guess it from the payload directories
+ for _, payloaddir in ipairs(addon._payloaddirs()) do
+ if os.isdir(path.join(dir, payloaddir)) then
+ return path.filename(dir), dir
+ end
+ end
+ local parentdir = path.directory(dir)
+ if not parentdir or parentdir == dir then
+ break
+ end
+ dir = parentdir
+ end
+end
+
+-- resolve the given addon reference to its payload directory
+--
+-- the addon resources are referenced with the addon name from the outside,
+-- and with `@self` from the addon code itself, e.g.
+--
+-- add_rules("@addon/esp32/flash"), import("@addon.esp32.sdkconfig") -- from a project
+-- add_rules("@self/flash"), import("@self.sdkconfig") -- from the addon itself
+--
+-- @param reference the reference, e.g. "@addon/esp32/flash", "@self.sdkconfig"
+-- @param sep the separator, e.g. "/", "."
+-- @param kind the payload kind, e.g. "rules", "modules"
+-- @param opt the options, e.g. {scriptdir = "..."}, it's used to resolve `@self`
+--
+-- @return the reference information and errors,
+-- e.g. {dir = "~/.xmake/addons/esp32/v1.0.0/rules", name = "flash", addon = "esp32"}
+--
+function addon.resolve_reference(reference, sep, kind, opt)
+ opt = opt or {}
+
+ -- resolve the `@self` reference from the addon which owns the current script
+ if reference:startswith("@self" .. sep) then
+ local name = reference:sub(#("@self" .. sep) + 1)
+ if name == "" then
+ return nil, string.format("invalid addon reference(%s)!", reference)
+ end
+ local addonname, addondir = addon.owner(opt.scriptdir)
+ if not addondir then
+ return nil, string.format("%s: cannot resolve `@self`, it can only be used inside an addon!", reference)
+ end
+ return {dir = path.join(addondir, kind), name = name, addon = addonname}
+ end
+
+ -- resolve the `@addon` reference, the addon name is always required
+ local prefix = "@addon" .. sep
+ if not reference:startswith(prefix) then
+ return
+ end
+ local pos = reference:find(sep, #prefix + 1, true)
+ local addonname = pos and reference:sub(#prefix + 1, pos - 1)
+ local name = pos and reference:sub(pos + 1)
+ if not addonname or addonname == "" or not name or name == "" then
+ return nil, string.format("invalid addon reference(%s), it should be `@addon%s<addon>%s<name>`", reference, sep, sep)
+ end
+ local payloaddir = addon._payloaddir(addonname, kind)
+ if not payloaddir then
+ return nil, string.format("%s not found!\nplease install the addon which provides it first: xmake addon --install %s", reference, addonname)
+ end
+ return {dir = payloaddir, name = name, addon = addonname}
+end
+
+-- 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 addoninfo = addon._registry()[addon.dirname(name)]
+ return table.orderkeys(addoninfo and addoninfo.versions or {})
+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
+--
+-- @return the addons table, e.g. {["hello-world"] = {version = "latest", payloads = {"plugins"}}}
+--
+function addon.addons(opt)
+ if opt and opt.force then
+ addon._registry({force = true})
+ end
+ local addons = addon._ADDONS
+ if addons == nil then
+ addons = {}
+ 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
+ return addons
+end
+
+-- get the install directory of the given addon, e.g. ~/.xmake/addons/<name>/<version>
+function addon.addondir(name, version)
+ local dirname = addon.dirname(name)
+ if version == nil then
+ local addoninfo = addon.addons()[dirname]
+ if addoninfo == nil then
+ return nil
+ end
+ version = addoninfo.version
+ end
+ return path.join(addon.installdir(), dirname, version)
+end
+
+-- get the modules which the installed addons export as the global modules
+--
+-- they are declared in the addon manifest, e.g. add_globalmodules("core.tools.esptool"),
+-- so that they can be imported with their plain names by the internal calls,
+-- e.g. import("core.tools.esptool"), find_tool("esptool")
+--
+-- @return the modules table, e.g. {["core.tools.esptool"] = "~/.xmake/addons/esp32/v1.0.0/modules"}
+--
+function addon.globalmodules()
+ local globalmodules = addon._GLOBALMODULES
+ if globalmodules == nil then
+ globalmodules = {}
+ for dirname, addoninfo in pairs(addon.addons()) do
+ for _, name in ipairs(addoninfo.globalmodules or {}) do
+ globalmodules[name] = path.join(addon.installdir(), dirname, addoninfo.version, "modules")
+ end
+ end
+ addon._GLOBALMODULES = globalmodules
+ end
+ return globalmodules
+end
+
+-- get the payload directories of the given kind from all installed addons
+--
+-- @param kind the payload kind, e.g. "plugins", "rules"
+-- @return the directories, e.g. {"~/.xmake/addons/hello-world/latest/plugins"}
+--
+-- @note we do not check if these directories exist, the callers will just ignore the invalid ones
+--
+function addon.payloads(kind)
+ local payloads = {}
+ for _, payloadinfo in ipairs(addon.payloadinfos(kind)) do
+ table.insert(payloads, payloadinfo.dir)
+ end
+ return payloads
+end
+
+-- get the payload information of the given kind from all installed addons
+--
+-- @param kind the payload kind, e.g. "plugins", "rules"
+-- @return the payload infos, e.g. {{name = "hello-world", version = "latest", dir = "~/.xmake/addons/hello-world/latest/plugins"}}
+--
+function addon.payloadinfos(kind)
+ local payloadinfos = {}
+ for name, addoninfo in table.orderpairs(addon.addons()) do
+ if table.contains(addoninfo.payloads or {}, kind) then
+ table.insert(payloadinfos, {
+ name = name,
+ version = addoninfo.version,
+ dir = path.join(addon.installdir(), name, addoninfo.version, kind)})
+ end
+ end
+ return payloadinfos
+end
+
+-- get the payload root directory of the given addon source directory
+--
+-- an addon repository has its own files, e.g. tests, ci scripts and documents,
+-- so its payloads can be placed in the `src` subdirectory, and we only install them
+--
+-- e.g.
+-- esp32-devel/src/{plugins,rules,toolchains,templates} -- with the `src` layout
+-- hello-world/{plugins} -- without it, for the simple addons
+--
+-- @param sourcedir the addon source directory
+-- @return the payload root directory, it will be nil if no payload is found
+--
+function addon.payloadroot(sourcedir)
+
+ -- the addon can set its payload root directory explicitly, e.g. set_sourcedir("src")
+ local manifest = addon.manifest(sourcedir)
+ if manifest and manifest.sourcedir then
+ local payloadroot = path.join(sourcedir, manifest.sourcedir)
+ if #addon.payloads_of(payloadroot) > 0 then
+ return payloadroot
+ end
+ return
+ end
+
+ local srcdir = path.join(sourcedir, "src")
+ if #addon.payloads_of(srcdir) > 0 then
+ return srcdir
+ end
+ if #addon.payloads_of(sourcedir) > 0 then
+ return sourcedir
+ end
+end
+
+-- get the payload directories of the given addon directory, e.g. {"plugins", "rules"}
+function addon.payloads_of(addondir)
+ local payloads = {}
+ for _, payloaddir in ipairs(addon._payloaddirs()) do
+ if os.isdir(path.join(addondir, payloaddir)) then
+ table.insert(payloads, payloaddir)
+ end
+ end
+ return payloads
+end
+
+-- get the default on_install script of addon packages
+--
+-- we only install the payload directories of this addon, e.g. plugins, rules, toolchains, ...
+--
+function addon.installscript()
+ return function (package)
+ local sourcedir = os.curdir()
+
+ -- the addon name is its identity, e.g. the install directory, the registry key
+ -- and the `@addon/<name>/xxx` references, so the package must be distributed with the same name
+ local manifest, errors = addon.manifest(sourcedir)
+ if errors then
+ os.raise(errors)
+ end
+ if manifest and addon.dirname(manifest.name) ~= addon.dirname(package:name()) then
+ os.raise("addon(%s) does not match the package name(%s) in the repository!\nplease fix the package recipe or the addon manifest.",
+ manifest.name, package:name())
+ end
+
+ local payloadroot = addon.payloadroot(sourcedir)
+ if not payloadroot then
+ os.raise("addon(%s): no payload directory found, e.g. plugins!", package:name())
+ end
+ for _, payloaddir in ipairs(addon.payloads_of(payloadroot)) do
+ os.cp(path.join(payloadroot, payloaddir), package:installdir())
+ end
+
+ -- we need not install the manifest, the package manifest(manifest.txt) and the addons
+ -- registry already have all the information, we just pass it to the registration
+ if manifest then
+ package:data_set("addon.manifest", manifest)
+ end
+ end
+end
+
+-- register the given installed addon
+--
+-- @param name the addon name
+-- @param version the addon version, e.g. "1.0.1", "latest"
+-- @param opt the options, e.g. {description = "...", deps = {"foo"}}
+--
+-- @note the addon manifest(addon.lua) is only read when installing, everything which
+-- is needed later is recorded here, so we never parse it again
+--
+-- @return true or false and errors
+--
+function addon.register(name, version, opt)
+ opt = opt or {}
+ local dirname = addon.dirname(name)
+ local addondir = path.join(addon.installdir(), dirname, version)
+ local addoninfo = {version = version,
+ -- we need to keep the raw name, the directory name is only its
+ -- normalized form, e.g. "myns::foo" -> "myns_foo"
+ 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
+ manifest_deps = opt.manifest_deps,
+ globalmodules = opt.globalmodules,
+ payloads = addon.payloads_of(addondir),
+ plugins = addon._plugins_of(addondir),
+ templates = addon._templates_of(addondir)}
+
+ -- we need to check the conflicts of the plugins and templates first,
+ -- they are not namespaced and we do not know which one will be used
+ local errors = addon._check_conflicts(dirname, addoninfo)
+ if errors then
+ return false, errors
+ end
+
+ -- 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
+
+-- remove the given installed addon
+--
+-- @param name the addon name
+-- @return true or false and errors
+--
+function addon.remove(name, opt)
+ opt = opt or {}
+ local dirname = addon.dirname(name)
+ local installdir = path.join(addon.installdir(), dirname)
+ if not os.isdir(installdir) then
+ return false, string.format("addon(%s) not found!", name)
+ end
+
+ -- we cannot remove it if the other addons depend on it
+ if not opt.force then
+ local parents = addon._parents(name)
+ if parents then
+ return false, string.format("addon(%s) cannot be removed, it's depended on by the addon(%s)!\nplease remove them first, or pass --force to remove it anyway",
+ name, table.concat(parents, ", "))
+ end
+ end
+ -- we need to remove the symlinks first, we cannot remove them recursively,
+ -- otherwise the linked files would be removed too
+ --
+ -- e.g. the user may link the install directory to the addon source directory when developing it
+ for _, versiondir in ipairs(os.dirs(path.join(installdir, "*"))) do
+ if os.islink(versiondir) then
+ os.rmfile(versiondir)
+ end
+ end
+ if os.islink(installdir) then
+ return os.rmfile(installdir)
+ end
+
+ local ok, errors = os.rm(installdir)
+ if not ok then
+ return false, errors
+ end
+ addon._unregister(name)
+ 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
+
+-- return module
+return addon
diff --git a/xmake/core/package/package.lua b/xmake/core/package/package.lua
index e014d9578..78268351e 100644
--- a/xmake/core/package/package.lua
+++ b/xmake/core/package/package.lua
@@ -37,6 +37,7 @@ local interpreter = require("base/interpreter")
local select_script = require("base/private/select_script")
local is_cross = require("base/private/is_cross")
local memcache = require("cache/memcache")
+local addon = require("package/addon")
local toolchain = require("tool/toolchain")
local compiler = require("tool/compiler")
local linker = require("tool/linker")
@@ -620,7 +621,19 @@ function _instance:is_toolchain()
return self:kind() == "toolchain"
end
+-- is addon package?
+--
+-- it will be installed to `~/.xmake/addons/<name>/<version>` and
+-- it can provide plugins, rules, toolchains, templates and modules for xmake
+--
+function _instance:is_addon()
+ return self:kind() == "addon"
+end
+
-- is plugin package?
+--
+-- @note this kind is deprecated, please use the `addon` kind instead
+--
function _instance:is_plugin()
return self:kind() == "plugin"
end
@@ -922,7 +935,15 @@ function _instance:installdir(...)
installdir = self:get("installdir")
if not installdir then
local name = self:name():lower():gsub("::", "_")
- if self:is_plugin() then
+ if self:is_addon() then
+ -- e.g. ~/.xmake/addons/<name>/<version>
+ local version_str = self:version_str() or "latest"
+ if os.is_host("windows") then
+ version_str = version_str:gsub("[>=<|%*]", "")
+ end
+ installdir = addon.addondir(self:name(), version_str)
+ elseif self:is_plugin() then
+ -- deprecated, @see the `addon` kind
installdir = path.join(global.directory(), "plugins", name)
else
if self:is_local() then
@@ -1194,10 +1215,6 @@ function _instance:_rawenvs()
end
end
- -- add plugin env for on_test
- if self:is_plugin() then
- envs.XMAKE_PLUGIN_DIRS = path.directory(self:installdir())
- end
self._RAWENVS = envs
end
return envs
@@ -2580,6 +2597,79 @@ function _instance:_generate_build_configs(configs, opt)
return configs
end
+-- has the given payloads? (only for the addon packages)
+--
+-- @param opt the payloads to be checked, the value can be a string or a list, e.g.
+-- {rules = "app", toolchains = "esp32", plugins = "monitor",
+-- templates = "c/esp32.hello", modules = {"private.board", "private.flasher"}}
+--
+-- @return true, or false and errors
+--
+-- e.g.
+--
+-- on_test(function (package)
+-- assert(package:has_addon({rules = "app", toolchains = "esp32"}))
+-- end)
+--
+function _instance:has_addon(opt)
+ if not self:is_addon() then
+ return false, string.format("package(%s) is not an addon!", self:name())
+ end
+
+ -- the addon should be registered after installing it
+ local addoninfo = addon.addons()[addon.dirname(self:name())]
+ if not addoninfo then
+ return false, string.format("addon(%s) is not installed!", self:name())
+ end
+ if opt == nil then
+ return true
+ end
+
+ local checkers = {}
+
+ -- the rules are namespaced, e.g. @addon/esp32-devel/app
+ --
+ -- @note we need to reload the global rules, this addon may be installed just now
+ checkers.rules = function (name)
+ local rule = require("project/rule")
+ rule.clear()
+ return rule.rules()["@addon/" .. self:name() .. "/" .. name] ~= nil
+ end
+
+ -- the toolchains are namespaced too, e.g. @addon/esp32-devel/esp32
+ checkers.toolchains = function (name)
+ return toolchain.load("@addon/" .. self:name() .. "/" .. name) ~= nil
+ end
+
+ -- the plugins and the templates are not namespaced, we get them from the addons registry,
+ -- the task list of this process has been loaded before installing this addon
+ checkers.plugins = function (name)
+ return table.contains(addoninfo.plugins or {}, name)
+ end
+ checkers.templates = function (name)
+ return table.contains(addoninfo.templates or {}, name)
+ end
+
+ -- the modules are files, e.g. modules/serial.lua, modules/private/board.lua
+ checkers.modules = function (name)
+ local modulepath = path.join(self:installdir(), "modules", (name:gsub("%.", "/")))
+ return os.isfile(modulepath .. ".lua") or os.isdir(modulepath)
+ end
+
+ for kind, names in pairs(opt) do
+ local checker = checkers[kind]
+ if not checker then
+ return false, string.format("unknown addon payload(%s), it should be one of rules, toolchains, plugins, templates and modules!", kind)
+ end
+ for _, name in ipairs(table.wrap(names)) do
+ if not checker(name) then
+ return false, string.format("%s(%s) not found in the addon(%s)!", kind, name, self:name())
+ end
+ end
+ end
+ return true
+end
+
-- has the given c funcs?
--
-- @param funcs the funcs
@@ -3226,7 +3316,13 @@ function package.load_from_repository(packagename, packagedir, opt)
return nil, string.format("%s: package(%s) not found!", scriptpath, packagename)
end
+ -- we need set the default on_install script if it's addon package
+ if packageinfo:get("kind") == "addon" and not packageinfo:get("install") then
+ packageinfo:set("install", addon.installscript())
+ end
+
-- we need set the default on_install script if it's plugin package
+ -- @note the plugin kind is deprecated, please use the addon kind instead
if packageinfo:get("kind") == "plugin" and not packageinfo:get("install") then
-- only one code line, we can directly omit the sandbox wrapper.
local on_install = function (pkg)
diff --git a/xmake/core/project/addons.lua b/xmake/core/project/addons.lua
new file mode 100644
index 000000000..169b7d033
--- /dev/null
+++ b/xmake/core/project/addons.lua
@@ -0,0 +1,228 @@
+--!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", "serial-tools")
+-- 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 d1030f130..4f82ccd53 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,124 @@ 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 opt.skip_addons then
+ -- we do not install them here, but we still need to use the locked versions
+ project._pin_addons()
+ else
+ 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 +376,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 +426,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 +439,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 +520,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 +601,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 +1249,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
@@ -1259,6 +1379,10 @@ end
function project.toolchain(name, opt)
opt = opt or {}
local parseinfo = toolchain.parsename(name) -- we need to ignore `@packagename`
+ -- the addon toolchains are only loaded from the addons, e.g. set_toolchains("@addon/esp32/xtensa")
+ if parseinfo.addon_prefix then
+ return nil
+ end
local toolchain_name = parseinfo.name
local info = project._toolchains()[toolchain_name]
if info == nil and opt.namespace then
diff --git a/xmake/core/project/rule.lua b/xmake/core/project/rule.lua
index 332844ca4..5b3b3aa78 100644
--- a/xmake/core/project/rule.lua
+++ b/xmake/core/project/rule.lua
@@ -31,6 +31,7 @@ local global = require("base/global")
local interpreter = require("base/interpreter")
local instance_deps = require("base/private/instance_deps")
local select_script = require("base/private/select_script")
+local addon = require("package/addon")
local config = require("project/config")
local sandbox = require("sandbox/sandbox")
local sandbox_os = require("sandbox/modules/os")
@@ -223,6 +224,11 @@ function rule._directories()
}
end
+-- the rule directories of the installed addons, e.g. ~/.xmake/addons/<name>/<version>/rules
+function rule._addon_directories()
+ return addon.payloadinfos("rules")
+end
+
-- the interpreter
function rule._interpreter()
@@ -453,9 +459,205 @@ function rule.new(name, info, opt)
return instance
end
+-- get the file which declares the given rule, e.g. c.build -> <rulesdir>/c++/xmake.lua
+--
+-- most of the rule names match their directory name, so we only need it for the few
+-- which do not, e.g. c.build, win.sdk.resource, and we get them by scanning the rule
+-- declarations, which is much cheaper than interpreting all the rule files
+--
+function rule._rulefile(name)
+ local rulefiles = rule._RULEFILES
+ if rulefiles == nil then
+
+ -- @note we only limit the fast paths, a rule file which is nested deeper than this
+ -- is still found by the full load, @see rule.rules
+ local maxrecursion = 4
+ rulefiles = {}
+ for _, dir in ipairs(rule._directories()) do
+ for _, filepath in ipairs(os.files(path.join(dir, "**/xmake.lua"), {recursion = maxrecursion})) do
+ local content = io.readfile(filepath)
+ if content then
+ for rulename in content:gmatch("rule%s*%(%s*\"(.-)\"%s*%)") do
+ rulefiles[rulename] = filepath
+ end
+ end
+ end
+ end
+ rule._RULEFILES = rulefiles
+ end
+ return rulefiles[name]
+end
+
+-- load the rules which may provide the given rule name
+--
+-- there are hundreds of builtin rules, but a project only uses a few of them,
+-- so we do not load all of them, we only load the group which may provide it,
+-- e.g. mode.debug -> <rulesdir>/mode/**/xmake.lua
+--
+function rule._load_ondemand(name)
+
+ -- it has been loaded with another group? e.g. c.build.pcheader comes with the c++ group
+ local loaded = rule._LOADED
+ if loaded == nil then
+ loaded = {}
+ rule._LOADED = loaded
+ end
+ local instance = loaded[name]
+ if instance then
+ return instance
+ end
+
+ local groups = rule._GROUPS
+ if groups == nil then
+ groups = {}
+ rule._GROUPS = groups
+ end
+
+ -- the rules of an addon are always referenced with its name,
+ -- e.g. add_rules("@addon/esp32/flash"), so we only load this addon
+ local maxrecursion = 4
+ local groupkey, files, opt
+ if name:startswith("@addon/") then
+ local referenceinfo = addon.resolve_reference(name, "/", "rules")
+ if not referenceinfo then
+ return
+ end
+ groupkey = "@addon/" .. referenceinfo.addon
+ files = os.files(path.join(referenceinfo.dir, "**/xmake.lua"), {recursion = maxrecursion})
+ opt = {prefix = groupkey .. "/"}
+ else
+ -- the group is the first part of the rule name, and it's usually the directory
+ -- name of its rules, e.g. mode.debug -> <rulesdir>/mode
+ groupkey = name:split(".", {plain = true})[1]
+ files = {}
+ for _, dir in ipairs(rule._directories()) do
+ local groupdir = path.join(dir, groupkey)
+ table.join2(files, os.files(path.join(groupdir, "xmake.lua")))
+ table.join2(files, os.files(path.join(groupdir, "**/xmake.lua"), {recursion = maxrecursion}))
+ end
+
+ -- the rule name does not match its directory name? we can only get its file
+ -- from the rule declarations, e.g. c.build -> <rulesdir>/c++/xmake.lua
+ if #files == 0 then
+ local rulefile = rule._rulefile(name)
+ if not rulefile then
+ return
+ end
+ groupkey = rulefile
+ files = {rulefile}
+ end
+ end
+
+ if not groups[groupkey] then
+ local ruleinfos = {}
+ rule._load_rulefiles(ruleinfos, files, opt)
+ for rulename, ruleinfo in pairs(ruleinfos) do
+ loaded[rulename] = rule.new(rulename, ruleinfo)
+ end
+ groups[groupkey] = true
+ end
+ return loaded[name]
+end
+
+-- report the missing addon of the given rule reference, e.g. add_rules("@addon/esp32/flash")
+--
+-- it's either not installed at all, or it's installed but does not provide this rule
+--
+function rule._raise_addon_notfound(name)
+ local referenceinfo, errors = addon.resolve_reference(name, "/", "rules")
+ if errors then
+ os.raise(errors)
+ end
+ os.raise("rule(%s) not found!\nplease install the addon which provides it first: xmake addon --install %s",
+ name, referenceinfo and referenceinfo.addon or "<addon>")
+end
+
+-- clear the loaded rules, e.g. some addons may be installed just now
+function rule.clear()
+ rule._RULES = nil
+ rule._GROUPS = nil
+ rule._LOADED = nil
+ rule._RULEFILES = nil
+end
+
-- get the given global rule
+--
+-- @param name the rule name, the rules of the installed addons need the
+-- `@addon/<addon>/` prefix, e.g. "@addon/esp32/flash"
+--
function rule.rule(name)
- return rule.rules()[name]
+ local instance
+ if rule._RULES then
+ -- all the rules have been loaded, e.g. rule.rules()
+ instance = rule._RULES[name]
+ else
+ -- we only load the rules which may provide it
+ instance = rule._load_ondemand(name)
+ if instance == nil then
+ -- @note the rule name may not match its directory name, so we need
+ -- to load all the rules to be sure that it does not exist
+ instance = rule.rules()[name]
+ end
+ end
+ if instance == nil and name:startswith("@addon/") then
+ rule._raise_addon_notfound(name)
+ end
+ return instance
+end
+
+-- load the rules from the given directory
+function rule._load_rules(ruleinfos, dir, opt)
+ -- @note we may load a group directory directly, e.g. <rulesdir>/mode/xmake.lua
+ rule._load_rulefiles(ruleinfos, table.join(os.files(path.join(dir, "xmake.lua")),
+ os.files(path.join(dir, "**/xmake.lua"))), opt)
+end
+
+-- load the rules from the given files
+function rule._load_rulefiles(ruleinfos, files, opt)
+ opt = opt or {}
+ if files then
+ for _, filepath in ipairs(files) do
+ local results, errors = rule._load(filepath)
+ if results then
+ for rulename, ruleinfo in pairs(results) do
+ -- the addon rules are always referenced with the addon name,
+ -- e.g. add_rules("@addon/esp32/flash")
+ local fullname = (opt.prefix or "") .. rulename
+ if opt.prefix and ruleinfos[fullname] == nil then
+ -- the addon rules can depend on the other rules of the same addon,
+ -- e.g. add_deps("@self/base") -> add_deps("@addon/<addon>/base")
+ rule._replace_selfdeps(ruleinfo, opt.prefix)
+ end
+ ruleinfos[fullname] = ruleinfo
+ end
+ else
+ os.raise(errors)
+ end
+ end
+ end
+end
+
+-- replace the `@self/` dependencies of the addon rules with the full names
+function rule._replace_selfdeps(ruleinfo, prefix)
+ local deps = {}
+ local replace = function (depname)
+ if depname:startswith("@self/") then
+ return prefix .. depname:sub(#"@self/" + 1)
+ end
+ return depname
+ end
+ for _, depname in ipairs(table.wrap(ruleinfo:get("deps"))) do
+ table.insert(deps, replace(depname))
+ end
+ if #deps > 0 then
+ ruleinfo:set("deps", table.unwrap(deps))
+ end
+ for depname, extraconf in pairs(table.wrap(ruleinfo:extraconf("deps"))) do
+ local newname = replace(depname)
+ if newname ~= depname then
+ ruleinfo:extraconf_set("deps", newname, extraconf)
+ end
+ end
end
-- get global rules
@@ -463,26 +665,21 @@ function rule.rules()
local rules = rule._RULES
if rules == nil then
local ruleinfos = {}
- local dirs = rule._directories()
- for _, dir in ipairs(dirs) do
- local files = os.files(path.join(dir, "**/xmake.lua"))
- if files then
- for _, filepath in ipairs(files) do
- local results, errors = rule._load(filepath)
- if results then
- table.join2(ruleinfos, results)
- else
- os.raise(errors)
- end
- end
- end
+ for _, dir in ipairs(rule._directories()) do
+ rule._load_rules(ruleinfos, dir)
+ end
+ for _, addoninfo in ipairs(rule._addon_directories()) do
+ rule._load_rules(ruleinfos, addoninfo.dir, {prefix = "@addon/" .. addoninfo.name .. "/"})
end
-- make rule instances
+ --
+ -- @note we reuse the rules which have been loaded on demand,
+ -- otherwise we would have two instances of the same rule
+ local loaded = rule._LOADED or {}
rules = {}
for rulename, ruleinfo in pairs(ruleinfos) do
- local instance = rule.new(rulename, ruleinfo)
- rules[rulename] = instance
+ rules[rulename] = loaded[rulename] or rule.new(rulename, ruleinfo)
end
rule._RULES = rules
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
new file mode 100644
index 000000000..9fc7c9f0c
--- /dev/null
+++ b/xmake/core/sandbox/modules/import/core/package/addon.lua
@@ -0,0 +1,99 @@
+--!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 addon.lua
+--
+
+-- define module
+local sandbox_core_package_addon = sandbox_core_package_addon or {}
+
+-- load modules
+local addon = require("package/addon")
+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
+sandbox_core_package_addon.payloads = addon.payloads
+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
+
+-- get the manifest of the given addon directory, e.g. <sourcedir>/addon.lua
+--
+-- @param sourcedir the addon source or install directory
+-- @return the manifest, it will be nil if this addon does not describe itself
+--
+function sandbox_core_package_addon.manifest(sourcedir)
+ local manifest, errors = addon.manifest(sourcedir)
+ if errors then
+ raise(errors)
+ end
+ return manifest
+end
+
+-- resolve the given addon reference, e.g. `@addon/<addon>/<name>`, `@self/<name>`
+--
+-- @param reference the addon reference
+-- @param sep the separator, e.g. "/", "."
+-- @param kind the payload kind, e.g. "rules", "modules"
+-- @param opt the options, e.g. {scriptdir = "..."}
+--
+-- @return the reference information, e.g. {dir = "...", name = "flash", addon = "esp32"}
+--
+function sandbox_core_package_addon.resolve_reference(reference, sep, kind, opt)
+ local referenceinfo, errors = addon.resolve_reference(reference, sep, kind, opt)
+ if errors then
+ raise(errors)
+ end
+ return referenceinfo
+end
+
+-- register the given installed addon
+--
+-- @param name the addon name
+-- @param version the addon version, e.g. "1.0.1", "latest"
+-- @param opt the options, e.g. {description = "...", deps = {"foo"}}
+--
+function sandbox_core_package_addon.register(name, version, opt)
+ local ok, errors = addon.register(name, version, opt)
+ if not ok then
+ raise(errors)
+ end
+end
+
+-- remove the given installed addon
+--
+-- @param name the addon name
+-- @param opt the options, e.g. {force = true}
+--
+function sandbox_core_package_addon.remove(name, opt)
+ local ok, errors = addon.remove(name, opt)
+ if not ok then
+ raise(errors)
+ end
+end
+
+-- return module
+return sandbox_core_package_addon
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/project/project.lua b/xmake/core/sandbox/modules/import/core/project/project.lua
index 9b69349d0..4964d988c 100644
--- a/xmake/core/sandbox/modules/import/core/project/project.lua
+++ b/xmake/core/sandbox/modules/import/core/project/project.lua
@@ -161,6 +161,10 @@ end]]
function sandbox_core_project._load_package_rules_for_target(target)
for _, rulename in ipairs(table.wrap(target:get("rules"))) do
local packagename = rulename:match("@(.-)/")
+ -- @note we need to ignore the addon rules, e.g. add_rules("@addon/foo")
+ if packagename == "addon" then
+ packagename = nil
+ end
if packagename then
local ruleinst
local pkginfo = project.required_package(packagename)
diff --git a/xmake/core/sandbox/modules/import/core/sandbox/module.lua b/xmake/core/sandbox/modules/import/core/sandbox/module.lua
index 2090c1f52..294cf7556 100644
--- a/xmake/core/sandbox/modules/import/core/sandbox/module.lua
+++ b/xmake/core/sandbox/modules/import/core/sandbox/module.lua
@@ -30,6 +30,7 @@ local table = require("base/table")
local string = require("base/string")
local option = require("base/option")
local global = require("base/global")
+local addon = require("package/addon")
local config = require("project/config")
local memcache = require("cache/memcache")
local sandbox = require("sandbox/sandbox")
@@ -386,7 +387,7 @@ function core_sandbox_module._find_and_load(name, opt)
errors = moduleinfo[2]
else
module, errors = core_sandbox_module._load(moduledir, name, {
- instance = idx < #modules_directories and opt.instance or nil, -- last modules need not fork sandbox
+ instance = moduledir ~= core_sandbox_module.coredir() and opt.instance or nil, -- the core modules need not fork sandbox
module = module,
always_build = always_build,
modulekind = modulekind})
@@ -419,9 +420,10 @@ end
function core_sandbox_module.directories()
local moduledirs = memcache.get("core_sandbox_module", "moduledirs")
if not moduledirs then
+ -- @note the core modules directory must be the last one, @see core_sandbox_module._find_and_load
moduledirs = { path.join(global.directory(), "modules"),
path.join(os.programdir(), "modules"),
- path.join(os.programdir(), "core/sandbox/modules/import")}
+ core_sandbox_module.coredir()}
local modulesdir = os.getenv("XMAKE_MODULES_DIR")
if modulesdir and os.isdir(modulesdir) then
table.insert(moduledirs, 1, modulesdir)
@@ -431,6 +433,14 @@ function core_sandbox_module.directories()
return moduledirs
end
+-- get the core modules directory
+--
+-- @note the modules in this directory are loaded without sandbox, because they need `require`
+--
+function core_sandbox_module.coredir()
+ return path.join(os.programdir(), "core/sandbox/modules/import")
+end
+
-- add module directories
function core_sandbox_module.add_directories(...)
local moduledirs = core_sandbox_module.directories()
@@ -442,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
@@ -498,6 +516,24 @@ function core_sandbox_module.import(name, opt)
local scope_parent = getfenv(2)
assert(scope_parent)
+ -- import the modules of the installed addons? e.g. import("@addon.foo")
+ --
+ -- @note we need the `@addon.` prefix to distinguish them from the builtin modules
+ --
+ -- import the modules of an addon?
+ -- e.g. import("@addon.esp32.sdkconfig"), import("@self.sdkconfig")
+ local addon_modulesdir
+ local addon_reference = name
+ if addon.is_reference(name, ".") then
+ local referenceinfo, errors = addon.resolve_reference(name, ".", "modules",
+ {scriptdir = opt.scriptdir or sandbox.instance() and sandbox.instance():rootdir()})
+ if not referenceinfo then
+ raise(errors)
+ end
+ addon_modulesdir = referenceinfo.dir
+ name = referenceinfo.name
+ end
+
-- get module name
local modulename = core_sandbox_module.name(name)
if not modulename then
@@ -515,7 +551,25 @@ function core_sandbox_module.import(name, opt)
local rootdir = opt.rootdir or instance:rootdir()
-- init module directories (disable local packages?)
- local modules_directories = (opt.nolocal or not rootdir) and core_sandbox_module.directories() or table.join(rootdir, core_sandbox_module.directories())
+ local modules_directories
+ if addon_modulesdir then
+ -- the addon modules are always resolved from the addon `modules` directory only,
+ -- e.g. import("@addon.esp32.sdkconfig"), import("@self.sdkconfig")
+ modules_directories = {addon_modulesdir}
+ else
+ modules_directories = (opt.nolocal or not rootdir) and core_sandbox_module.directories() or table.join(rootdir, core_sandbox_module.directories())
+
+ -- an addon can export some modules as the global modules, e.g. add_globalmodules("core.tools.esptool"),
+ -- so that the internal calls can import them with their plain names, e.g. import("core.tools.esptool")
+ --
+ -- @note we only accept the declared names, the other modules of this addon are
+ -- still private and can only be imported with `@addon.`/`@self.`
+ --
+ local globalmodulesdir = addon.globalmodules()[name]
+ if globalmodulesdir then
+ table.insert(modules_directories, rootdir and 2 or 1, globalmodulesdir)
+ end
+ end
-- load module
local loadopt = table.clone(opt) or {}
@@ -554,6 +608,8 @@ function core_sandbox_module.import(name, opt)
if not found then
if opt.try then
return nil
+ elseif addon_modulesdir then
+ raise("cannot import module: %s, not found!", addon_reference)
else
raise("cannot import module: %s, not found!", name)
end
diff --git a/xmake/core/tool/toolchain.lua b/xmake/core/tool/toolchain.lua
index 7d19dfc7d..7b841a018 100644
--- a/xmake/core/tool/toolchain.lua
+++ b/xmake/core/tool/toolchain.lua
@@ -33,6 +33,7 @@ local hashset = require("base/hashset")
local scopeinfo = require("base/scopeinfo")
local interpreter = require("base/interpreter")
local is_cross = require("base/private/is_cross")
+local addon = require("package/addon")
local config = require("project/config")
local memcache = require("cache/memcache")
local localcache = require("cache/localcache")
@@ -670,6 +671,28 @@ end
-- e.g. "mingw[clang]@llvm-mingw", "msvc[vs=2025,..]"
--
function toolchain.parsename(name)
+
+ -- the toolchain of an addon?
+ -- e.g. set_toolchains("@addon/esp32/xtensa"), set_toolchains("@addon/esp32/clang@llvm"), set_toolchains("@self/xtensa")
+ --
+ -- @note we need to parse it first, because `@` is also used for the toolchain packages, e.g. "@zig"
+ --
+ -- @note we only strip the `@addon/<addon>/` prefix here, the rest is parsed as usual,
+ -- so the addon toolchains can also be bound to packages, e.g. "@addon/esp32/clang@llvm"
+ --
+ local addon_prefix
+ if name:startswith("@addon/") then
+ local rest = name:sub(#"@addon/" + 1)
+ local pos = rest:find("/", 1, true)
+ if pos then
+ addon_prefix = "@addon/" .. rest:sub(1, pos - 1) .. "/"
+ name = rest:sub(pos + 1)
+ end
+ elseif name:startswith("@self/") then
+ addon_prefix = "@self/"
+ name = name:sub(#"@self/" + 1)
+ end
+
local splitinfo = name:split('@', {plain = true, strict = true})
local toolchain_name = splitinfo[1]
if toolchain_name == "" then
@@ -707,7 +730,8 @@ function toolchain.parsename(name)
end
end
end
- return {name = toolchain_name or packages, packages = packages, requireconfs = requireconfs, requirestr = requirestr}
+ return {name = toolchain_name or packages, packages = packages, addon_prefix = addon_prefix,
+ requireconfs = requireconfs, requirestr = requirestr}
end
-- get toolchain apis
@@ -781,9 +805,27 @@ function toolchain.load(name, opt)
configs.plat = opt.plat or config.get("plat") or os.host()
configs.arch = opt.arch or config.get("arch") or os.arch()
+ -- find the toolchain script path
+ --
+ -- @note we need to resolve the addon reference before the cache, `@self/` depends on
+ -- the addon which owns the caller script, and the different addons may provide
+ -- the same toolchain name
+ --
+ local scriptpath, addon_prefix
+ if parseinfo.addon_prefix then
+ -- e.g. set_toolchains("@addon/esp32/xtensa"), set_toolchains("@self/xtensa")
+ local referenceinfo, errors = addon.resolve_reference(parseinfo.addon_prefix .. name, "/", "toolchains",
+ {scriptdir = opt.scriptdir})
+ if not referenceinfo then
+ return nil, errors
+ end
+ addon_prefix = "@addon/" .. referenceinfo.addon .. "/"
+ scriptpath = path.join(referenceinfo.dir, referenceinfo.name, "xmake.lua")
+ end
+
-- get cache
local cache = toolchain._memcache()
- local cachekey = toolchain._cachekey(name, configs)
+ local cachekey = toolchain._cachekey((addon_prefix or "") .. name, configs)
-- get it directly from cache dirst
local instance = cache:get(cachekey)
@@ -791,16 +833,16 @@ function toolchain.load(name, opt)
return instance
end
- -- find the toolchain script path
- local scriptpath = nil
- for _, dir in ipairs(toolchain.directories()) do
- scriptpath = path.join(dir, name, "xmake.lua")
- if os.isfile(scriptpath) then
- break
+ if not addon_prefix then
+ for _, dir in ipairs(toolchain.directories()) do
+ scriptpath = path.join(dir, name, "xmake.lua")
+ if os.isfile(scriptpath) then
+ break
+ end
end
end
if not scriptpath or not os.isfile(scriptpath) then
- return nil, string.format("the toolchain %s not found!", name)
+ return nil, string.format("the toolchain %s%s not found!", addon_prefix or "", name)
end
-- get interpreter
diff --git a/xmake/modules/package/manager/xmake/search_package.lua b/xmake/modules/package/manager/xmake/search_package.lua
index 32991ae3e..a5ed3caf8 100644
--- a/xmake/modules/package/manager/xmake/search_package.lua
+++ b/xmake/modules/package/manager/xmake/search_package.lua
@@ -23,7 +23,7 @@ import("core.base.semver")
import("private.xrepo.quick_search.cache")
function _search_package(packages, name, opt)
- for _, packageinfo in ipairs(cache.find(name, {description = opt.description ~= false})) do
+ for _, packageinfo in ipairs(cache.find(name, {description = opt.description ~= false, kind = opt.kind})) do
local packagename = packageinfo.name
local packagedata = packageinfo.data
@@ -59,7 +59,7 @@ end
-- search package using the xmake package manager
--
-- @param name the package name with pattern
--- @param opt the options, e.g. {require_version = "1.x"}
+-- @param opt the options, e.g. {require_version = "1.x", kind = "addon"}
--
function main(name, opt)
opt = opt or {}
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 7fb54a951..7ddb66cca 100644
--- a/xmake/modules/private/action/require/impl/actions/install.lua
+++ b/xmake/modules/private/action/require/impl/actions/install.lua
@@ -22,6 +22,7 @@
import("core.base.option")
import("core.base.tty")
import("core.package.package", {alias = "core_package"})
+import("core.package.addon")
import("core.project.target")
import("core.project.project")
import("core.platform.platform")
@@ -283,6 +284,50 @@ function _merge_staticlibs(package)
end
end
+-- register the installed addon, so that xmake can find its payloads, e.g. plugins
+--
+-- @note the addon manifest(addon.lua) is only read when installing it, everything which
+-- is needed later is recorded in the addons registry, @see core/package/addon.lua
+--
+function _register_addon(package)
+
+ -- get the addon deps from the package recipe
+ local deps
+ for _, dep in ipairs(package:plaindeps() or {}) do
+ if dep:is_addon() then
+ deps = deps or {}
+ table.insert(deps, dep:name())
+ end
+ end
+
+ -- the addon describes itself? we prefer its own description
+ --
+ -- @note the deps are duplicated in its manifest, but the recipe is authoritative,
+ -- xmake needs them before downloading the addon sources, so we only report the
+ -- mismatch, they must be kept in sync
+ --
+ local description = package:description()
+ local manifest = package:data("addon.manifest")
+ if manifest then
+ description = manifest.description or description
+ for _, dep in ipairs(manifest.deps) do
+ if not (deps and table.contains(deps, dep)) then
+ wprint("addon(%s): dep(%s) is declared in its manifest, but not in the package recipe!",
+ package:name(), dep)
+ end
+ 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
+
-- get failed install directory
function _get_installdir_failed(package)
return path.join(package:cachedir(), "installdir.failed")
@@ -499,6 +544,11 @@ function main(package)
-- save the package info to the manifest file
package:manifest_save()
+
+ -- register this addon, so that xmake can find its payloads, e.g. plugins
+ if package:is_addon() then
+ _register_addon(package)
+ end
installed_now = true
end
end
diff --git a/xmake/modules/private/action/require/impl/install_packages.lua b/xmake/modules/private/action/require/impl/install_packages.lua
index 9573b38f6..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
@@ -862,7 +864,7 @@ end
-- - requires_extra: the extra require configs from `add_requires()`, indexed by the require string
-- - nodeps: only install the given packages, do not install their dependent packages
-- - system: load package from system if `true`, and never load it if `false` (only for non-3rd packages)
--- - packagekind: the package kind, e.g. "plugin", it will be loaded from the `plugins` root directory of repositories
+-- - packagekind: the package kind, e.g. "addon", it will be loaded from the `addons` root directory of repositories
-- @note `toolchain` is reserved and it will be set internally, @see load_packages
--
-- @return the installed packages, including the toolchain packages and all dependent packages
diff --git a/xmake/modules/private/action/require/impl/package.lua b/xmake/modules/private/action/require/impl/package.lua
index c3998c728..1105384c8 100644
--- a/xmake/modules/private/action/require/impl/package.lua
+++ b/xmake/modules/private/action/require/impl/package.lua
@@ -198,7 +198,7 @@ end
-- - plat: the given platform of this package
-- - arch: the given architecture of this package
-- - name: the given repository name, we will only find this package in the given repository
--- - rootdir: the root directory of repositories, e.g. "packages" (default), "plugins"
+-- - rootdir: the root directory of repositories, e.g. "packages" (default), "addons"
-- - locked_repo: the locked repository info in `xmake-requires.lock`, e.g. {url = .., commit = .., branch = ..}
--
function _load_package_from_repository(packagename, opt)
@@ -209,6 +209,23 @@ function _load_package_from_repository(packagename, opt)
end
end
+-- get the root directory of repositories for the given package
+--
+-- e.g. "packages" (default), "addons", "plugins" (deprecated)
+--
+-- @note the addon packages are only searched from the `addons` root directory,
+-- and we need to set it explicitly, e.g. add_deps("foo", {kind = "addon"})
+--
+function _get_repository_rootdir(requireinfo, opt)
+ local packagekind = requireinfo.kind or opt.packagekind
+ if packagekind == "addon" then
+ return "addons"
+ elseif packagekind == "plugin" then
+ return "plugins"
+ end
+ return "packages"
+end
+
-- load package package from base
--
-- e.g. package("foo") set_base("bar")
@@ -956,7 +973,7 @@ end
-- @param opt the options
-- - system: load package from system if `true`, and never load it if `false`,
-- it's only used when `add_requires("zlib", {system = nil})` is not set (only for non-3rd packages)
--- - packagekind: the package kind, e.g. "plugin", it will be loaded from the `plugins` root directory of repositories
+-- - packagekind: the package kind, e.g. "addon", it will be loaded from the `addons` root directory of repositories
-- - toolchain: only load toolchain packages, the non-toolchain toplevel packages will be ignored
-- - requirepath: the current require path, e.g. "foo.bar", it's used to detect circular dependencies
-- and match `add_requireconfs()`
@@ -969,6 +986,12 @@ function _load_package(packagename, requireinfo, opt)
-- check circular dependency
opt = opt or {}
+
+ -- the `addon` and `self` names are reserved, we use them to reference the addon resources,
+ -- e.g. add_rules("@addon/esp32/flash"), import("@self.sdkconfig")
+ if packagename == "addon" or packagename == "self" then
+ raise("package(%s): the name `%s` is reserved by xmake for the addon references, please rename it!", packagename, packagename)
+ end
if opt.requirepath then
local splitinfo = opt.requirepath:split(".", {plain = true})
if #splitinfo > 3 and
@@ -1014,7 +1037,7 @@ function _load_package(packagename, requireinfo, opt)
plat = requireinfo.plat,
arch = requireinfo.arch,
name = requireinfo.reponame,
- rootdir = opt.packagekind == "plugin" and "plugins" or "packages",
+ rootdir = _get_repository_rootdir(requireinfo, opt),
locked_repo = locked_requireinfo and locked_requireinfo.repo})
if package then
from_repo = true
@@ -1025,7 +1048,7 @@ function _load_package(packagename, requireinfo, opt)
if package and package:get("base") then
_load_package_from_base(package, package:get("base"), {
name = requireinfo.reponame,
- rootdir = opt.packagekind == "plugin" and "plugins" or "packages",
+ rootdir = _get_repository_rootdir(requireinfo, opt),
locked_repo = locked_requireinfo and locked_requireinfo.repo})
end
@@ -1211,7 +1234,6 @@ function _load_packages(requires, opt)
parentinfo = requireinfo,
nodeps = opt.nodeps,
resolvedinfo = opt.resolvedinfo,
- packagekind = opt.packagekind,
system = false})
for _, dep in ipairs(plaindeps) do
dep:parents_add(package)
@@ -1680,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 {}
@@ -1765,7 +1790,7 @@ end
-- - requires_extra: the extra require configs from `add_requires()`, e.g. {["zlib >=1.2.11"] = {configs = {shared = true}}}
-- - nodeps: only load the given packages, do not load their dependent packages
-- - system: load package from system if `true`, and never load it if `false` (only for non-3rd packages)
--- - packagekind: the package kind, e.g. "plugin", it will be loaded from the `plugins` root directory of repositories
+-- - packagekind: the package kind, e.g. "addon", it will be loaded from the `addons` root directory of repositories
-- - toolchain: only load toolchain packages and their dependent packages
-- - requirepath: the parent require path, e.g. "foo.bar", it's used to detect circular dependencies and match `add_requireconfs()`
-- - parentinfo: the parent requireinfo, the child package will inherit some builtin configs from it, e.g. runtimes, pic
diff --git a/xmake/modules/private/action/require/impl/repository.lua b/xmake/modules/private/action/require/impl/repository.lua
index 6b16a7183..d17bdb570 100644
--- a/xmake/modules/private/action/require/impl/repository.lua
+++ b/xmake/modules/private/action/require/impl/repository.lua
@@ -153,7 +153,7 @@ end
-- get package directory from repositories
--
-- @param packagename the package name
--- @param opt {rootdir = "packages|plugins"}
+-- @param opt {rootdir = "packages|addons|plugins"}
function packagedir(packagename, opt)
-- strip trailing ~tag, e.g. zlib~debug
diff --git a/xmake/modules/private/action/require/install.lua b/xmake/modules/private/action/require/install.lua
index 6adf0dfa2..ebc648124 100644
--- a/xmake/modules/private/action/require/install.lua
+++ b/xmake/modules/private/action/require/install.lua
@@ -82,7 +82,13 @@ function main(requires_raw)
-- install packages
environment.enter()
- local packagekind = option.get("plugin") and "plugin" or "package"
+ -- @note the `--plugin` option is deprecated, please use `--addon` instead
+ local packagekind = "package"
+ if option.get("addon") then
+ packagekind = "addon"
+ elseif option.get("plugin") then
+ packagekind = "plugin"
+ end
local packages = install_packages(requires, {packagekind = packagekind, requires_extra = requires_extra})
if packages then
_check_missing_packages(packages)
diff --git a/xmake/modules/private/action/require/search.lua b/xmake/modules/private/action/require/search.lua
index e714c6c19..a0f98e0d9 100644
--- a/xmake/modules/private/action/require/search.lua
+++ b/xmake/modules/private/action/require/search.lua
@@ -20,6 +20,7 @@
-- imports
import("core.base.task")
+import("core.base.option")
import("private.action.require.impl.utils.filter")
import("private.action.require.impl.repository")
import("private.action.require.impl.environment")
@@ -41,11 +42,14 @@ function main(names)
task.run("repo", {update = true})
end
+ -- we only search the addon packages if `--addon` is enabled
+ local kind = option.get("addon") and "addon" or nil
+
-- show title
- print("The package names:")
+ print(kind == "addon" and "The addon names:" or "The package names:")
-- search packages
- for name, packages in pairs(search_packages(names)) do
+ for name, packages in pairs(search_packages(names, {kind = kind})) do
if #packages > 0 then
-- show name
diff --git a/xmake/modules/private/check/checkers/api/package/kind.lua b/xmake/modules/private/check/checkers/api/package/kind.lua
index d2c7afd10..2e4f2495b 100644
--- a/xmake/modules/private/check/checkers/api/package/kind.lua
+++ b/xmake/modules/private/check/checkers/api/package/kind.lua
@@ -37,6 +37,8 @@ function main(opt)
end
return true
end
- return value == "binary" or value == "toolchain" or value == "template" or value == "plugin"
+ -- @note the `plugin` kind is deprecated, please use the `addon` kind instead
+ return value == "binary" or value == "toolchain" or value == "template" or
+ value == "addon" or value == "plugin"
end}))
end
diff --git a/xmake/modules/private/xrepo/action/install.lua b/xmake/modules/private/xrepo/action/install.lua
index 9e9bb1086..4abb57c7f 100644
--- a/xmake/modules/private/xrepo/action/install.lua
+++ b/xmake/modules/private/xrepo/action/install.lua
@@ -47,7 +47,9 @@ function menu_options()
"e.g.",
" - xrepo install -p cross --toolchain=mytool --includes='toolchain1.lua" .. path.envsep() .. "toolchain2.lua'"},
{nil, "policies", "kv", nil, "Set the policies." },
- {nil, "plugin", "k", nil, "Install plugin packages from <repository>/plugins/"},
+ {nil, "addon", "k", nil, "Install addon packages from <repository>/addons/"},
+ {nil, "plugin", "k", nil, "Install plugin packages from <repository>/plugins/",
+ "(deprecated, please use --addon instead)"},
{category = "Visual Studio SDK Configuration" },
{nil, "vs", "kv", nil, "The Microsoft Visual Studio"
, " e.g. --vs=2017" },
@@ -282,6 +284,9 @@ function _install_packages(packages)
if option.get("build") or is_debug then
table.insert(require_argv, "--build")
end
+ if option.get("addon") then
+ table.insert(require_argv, "--addon")
+ end
if option.get("plugin") then
table.insert(require_argv, "--plugin")
end
diff --git a/xmake/modules/private/xrepo/action/remove.lua b/xmake/modules/private/xrepo/action/remove.lua
index ed92a83b8..eed956c6e 100644
--- a/xmake/modules/private/xrepo/action/remove.lua
+++ b/xmake/modules/private/xrepo/action/remove.lua
@@ -20,6 +20,7 @@
-- imports
import("core.base.option")
+import("core.package.addon")
import("private.action.require.impl.remove_packages", {alias = "remove_all_packages"})
-- get menu options
@@ -44,6 +45,10 @@ function menu_options()
{nil, "toolchain", "kv", nil, "Set the toolchain name." },
{nil, "toolchain_host", "kv", nil, "Set the host toolchain name." },
{ },
+ {nil, "addon", "k", nil, "Remove the given installed addon packages.",
+ "e.g.",
+ " - xrepo remove --addon serial-monitor" },
+ {'f', "force", "k", nil, "Force to remove the addon packages, even if they are depended on by the others." },
{nil, "all", "k", nil, "Remove all packages and ignore extra package configs.",
"If `--all` is enabled, the package name parameter will support lua pattern",
"e.g.",
@@ -193,10 +198,21 @@ function _remove_packages(packages)
os.vexecv(os.programfile(), require_argv)
end
+-- remove the given installed addons
+function _remove_addons(names)
+ for _, name in ipairs(names) do
+ addon.remove(name, {force = option.get("force")})
+ cprint("${color.success}remove ${bright}%s${clear} ok!", name)
+ end
+end
+
-- main entry
function main()
local packages = option.get("packages")
- if option.get("all") then
+ if option.get("addon") then
+ assert(packages, "please specify the addons to be removed.")
+ _remove_addons(packages)
+ elseif option.get("all") then
remove_all_packages(packages)
elseif packages then
_remove_packages(packages)
diff --git a/xmake/modules/private/xrepo/action/search.lua b/xmake/modules/private/xrepo/action/search.lua
index d389e3833..c532af44e 100644
--- a/xmake/modules/private/xrepo/action/search.lua
+++ b/xmake/modules/private/xrepo/action/search.lua
@@ -30,6 +30,9 @@ function menu_options()
-- menu options
local options =
{
+ {nil, "addon", "k", nil, "Search the addon packages from <repository>/addons/",
+ "e.g.",
+ " - xrepo search --addon serial"},
{nil, "packages", "vs", nil, "The packages list (support lua pattern).",
"e.g.",
" - xrepo search zlib boost",
@@ -80,6 +83,9 @@ function _search_packages(packages)
if option.get("diagnosis") then
table.insert(require_argv, "-D")
end
+ if option.get("addon") then
+ table.insert(require_argv, "--addon")
+ end
table.join2(require_argv, packages)
os.vexecv(os.programfile(), require_argv)
end
diff --git a/xmake/modules/private/xrepo/quick_search/cache.lua b/xmake/modules/private/xrepo/quick_search/cache.lua
index cf75a212d..feba8cda9 100644
--- a/xmake/modules/private/xrepo/quick_search/cache.lua
+++ b/xmake/modules/private/xrepo/quick_search/cache.lua
@@ -25,20 +25,36 @@ import("private.action.require.impl.repository")
local cache = globalcache.cache("quick_search")
+-- get the cache key of the given package
+--
+-- @note the addons are stored with the `addon::` prefix,
+-- because an addon and a package may have the same name
+--
+function _cachekey(packagename, kind)
+ return kind == "addon" and ("addon::" .. packagename) or packagename
+end
+
-- search package directories from repositories
+--
+-- the packages are stored in <repodir>/packages/<first-letter>/<name>,
+-- and the addons are stored in <repodir>/addons/<first-letter>/<name>
+--
function _list_package_dirs()
-- find the package directories from all repositories
local unique = {}
local packageinfos = {}
for _, repo in ipairs(repository.repositories()) do
- for _, file in ipairs(os.files(path.join(repo:directory(), "packages", "*", "*", "xmake.lua"))) do
- local dir = path.directory(file)
- local subdirname = path.basename(path.directory(dir))
- if #subdirname == 1 then -- ignore l/luajit/port/xmake.lua
- local packagename = path.filename(dir)
- if not unique[packagename] then
- table.insert(packageinfos, {name = packagename, repo = repo, packagedir = dir})
- unique[packagename] = true
+ for _, rootinfo in ipairs({{rootdir = "packages"}, {rootdir = "addons", kind = "addon"}}) do
+ for _, file in ipairs(os.files(path.join(repo:directory(), rootinfo.rootdir, "*", "*", "xmake.lua"))) do
+ local dir = path.directory(file)
+ local subdirname = path.basename(path.directory(dir))
+ if #subdirname == 1 then -- ignore l/luajit/port/xmake.lua
+ local packagename = path.filename(dir)
+ local cachekey = _cachekey(packagename, rootinfo.kind)
+ if not unique[cachekey] then
+ table.insert(packageinfos, {name = packagename, kind = rootinfo.kind, repo = repo, packagedir = dir})
+ unique[cachekey] = true
+ end
end
end
end
@@ -57,7 +73,9 @@ end
function update()
for _, packageinfo in ipairs(_list_package_dirs()) do
local package = core_package.load_from_repository(packageinfo.name, packageinfo.packagedir, {repo = packageinfo.repo})
- cache:set(packageinfo.name, {
+ cache:set(_cachekey(packageinfo.name, packageinfo.kind), {
+ name = packageinfo.name,
+ kind = packageinfo.kind,
reponame = package:repo() and package:repo():name(),
description = package:description(),
versions = package:versions(),
@@ -79,22 +97,30 @@ function get()
end
-- find package
+--
+-- @param name the package name (support lua pattern)
+-- @param opt the options, e.g. {prefix = true, description = true, kind = "addon"}
+--
function find(name, opt)
_init()
opt = opt or {}
local list_result = {}
- for packagename, packagedata in pairs(cache:data()) do
- local found = false
- if opt.prefix then
- found = packagename:startswith(name)
- else
- found = packagename:find(path.pattern(name))
- end
- if not found and opt.description and packagedata.description and packagedata.description:find(name) then
- found = true
- end
- if found then
- table.insert(list_result, {name = packagename, data = packagedata})
+ for cachekey, packagedata in table.orderpairs(cache:data()) do
+ -- we only search the packages with the given kind, e.g. nil (package), "addon"
+ local packagename = packagedata.name or cachekey
+ if packagedata.kind == opt.kind then
+ local found = false
+ if opt.prefix then
+ found = packagename:startswith(name)
+ else
+ found = packagename:find(path.pattern(name))
+ end
+ if not found and opt.description and packagedata.description and packagedata.description:find(name) then
+ found = true
+ end
+ if found then
+ table.insert(list_result, {name = packagename, data = packagedata})
+ end
end
end
return list_result
diff --git a/xmake/plugins/plugin/main.lua b/xmake/plugins/plugin/main.lua
index facbc0b4c..ce1ac3e4f 100644
--- a/xmake/plugins/plugin/main.lua
+++ b/xmake/plugins/plugin/main.lua
@@ -257,6 +257,7 @@ function _clear()
end
function main()
+ cprint("${bright color.warning}${text.warning}: ${color.warning}`xmake plugin` is deprecated, please use `xmake addon` instead!")
if option.get("install") then
_install()
elseif option.get("remove") then
diff --git a/xmake/plugins/plugin/xmake.lua b/xmake/plugins/plugin/xmake.lua
index ae7869203..1136b0044 100644
--- a/xmake/plugins/plugin/xmake.lua
+++ b/xmake/plugins/plugin/xmake.lua
@@ -23,7 +23,7 @@ task("plugin")
on_run("main")
set_menu {
usage = "xmake plugin [options]",
- description = "Manage plugins of xmake.",
+ description = "Manage plugins of xmake. (deprecated, please use `xmake addon` instead)",
options = {
{'i', "install", "k", nil, "Install plugins."},
{'r', "remove", "k", nil, "Remove the given installed plugin."},