diff options
| author | ruki <[email protected]> | 2026-08-11 23:45:05 +0800 |
|---|---|---|
| committer | ruki <[email protected]> | 2026-08-11 23:45:05 +0800 |
| commit | 7ffcc9f8e2a98d63f7f6880b9a3e4553439583c6 (patch) | |
| tree | f2bf7c4e0e9806fd157cc3243a45f691fd282988 | |
| parent | 9e6abef8b0a26d7098d79749dd68b5a2590512c4 (diff) | |
add global modules and rewrite addon tests
34 files changed, 291 insertions, 185 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/demo-addon/src/templates/c/demoaddon/hello/xmake.lua b/tests/actions/addon/custom-template/templates/c/customaddon/hello/xmake.lua index f9ea6e8d2..33c36ec7e 100644 --- a/tests/actions/addon/demo-addon/src/templates/c/demoaddon/hello/xmake.lua +++ b/tests/actions/addon/custom-template/templates/c/customaddon/hello/xmake.lua @@ -1,3 +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..b58743d4b --- /dev/null +++ b/tests/actions/addon/custom-toolchain/src/modules/core/tools/mycl6x.lua @@ -0,0 +1,7 @@ +-- our compiler behaves like gcc, it's just a wrapper of the host one +inherit("core.tools.gcc") + +-- the marker of this module, the tests use it to check that it comes from the addon +function greeting() + return "hello from the custom toolchain addon" +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..443690792 --- /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 = 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/demo-addon-dep/addon.lua b/tests/actions/addon/demo-addon-dep/addon.lua deleted file mode 100644 index 2cc460768..000000000 --- a/tests/actions/addon/demo-addon-dep/addon.lua +++ /dev/null @@ -1,3 +0,0 @@ -addon("demo-addon-dep") - set_description("the demo addon which depends on the other addon") - add_deps("demo-addon") diff --git a/tests/actions/addon/demo-addon-dep/rules/dep/xmake.lua b/tests/actions/addon/demo-addon-dep/rules/dep/xmake.lua deleted file mode 100644 index be4805e92..000000000 --- a/tests/actions/addon/demo-addon-dep/rules/dep/xmake.lua +++ /dev/null @@ -1,10 +0,0 @@ -rule("dep") - - -- depend on a rule of the other addon - add_deps("@addon/demo-addon/base") - - on_load(function (target) - -- import a module of the other addon - import("@addon.demo-addon.greeting") - print("demo-addon-dep: " .. greeting("dep")) - end) diff --git a/tests/actions/addon/demo-addon/addon.lua b/tests/actions/addon/demo-addon/addon.lua deleted file mode 100644 index 546c6a7aa..000000000 --- a/tests/actions/addon/demo-addon/addon.lua +++ /dev/null @@ -1,3 +0,0 @@ -addon("demo-addon") - set_description("the demo addon of the tests") - set_sourcedir("src") diff --git a/tests/actions/addon/demo-addon/src/includes/check/xmake.lua b/tests/actions/addon/demo-addon/src/includes/check/xmake.lua deleted file mode 100644 index 3ba656e64..000000000 --- a/tests/actions/addon/demo-addon/src/includes/check/xmake.lua +++ /dev/null @@ -1,7 +0,0 @@ -print("demo-addon: includes check is loaded") - -option("demo_addon_option") - set_default(true) - set_showmenu(true) - set_description("An option provided by the demo addon.") -option_end() diff --git a/tests/actions/addon/demo-addon/src/modules/greeting.lua b/tests/actions/addon/demo-addon/src/modules/greeting.lua deleted file mode 100644 index 9adbc01ce..000000000 --- a/tests/actions/addon/demo-addon/src/modules/greeting.lua +++ /dev/null @@ -1,3 +0,0 @@ -function main(who) - return "hello from demo-addon: " .. (who or "world") -end diff --git a/tests/actions/addon/demo-addon/src/plugins/demo_hello/main.lua b/tests/actions/addon/demo-addon/src/plugins/demo_hello/main.lua deleted file mode 100644 index c4bf7951c..000000000 --- a/tests/actions/addon/demo-addon/src/plugins/demo_hello/main.lua +++ /dev/null @@ -1,6 +0,0 @@ -import("core.base.option") -import("@self.greeting") - -function main() - print(greeting(option.get("name"))) -end diff --git a/tests/actions/addon/demo-addon/src/plugins/demo_hello/xmake.lua b/tests/actions/addon/demo-addon/src/plugins/demo_hello/xmake.lua deleted file mode 100644 index 8908b23a1..000000000 --- a/tests/actions/addon/demo-addon/src/plugins/demo_hello/xmake.lua +++ /dev/null @@ -1,10 +0,0 @@ -task("demo_hello") - set_category("plugin") - on_run("main") - set_menu { - usage = "xmake demo_hello [options]", - description = "Say hello from the demo addon.", - options = { - {'n', "name", "kv", nil, "Set the name to say hello to."} - } - } diff --git a/tests/actions/addon/demo-addon/src/rules/app/xmake.lua b/tests/actions/addon/demo-addon/src/rules/app/xmake.lua deleted file mode 100644 index b6236bc28..000000000 --- a/tests/actions/addon/demo-addon/src/rules/app/xmake.lua +++ /dev/null @@ -1,9 +0,0 @@ -rule("app") - add_deps("@self/base") - on_load(function (target) - import("@self.greeting") - import("core.package.addon") - -- the addon code should never hardcode its own name, it can always ask for it - local addonname = addon.owner() - print("demo-addon: rule app is loaded by the addon(%s), %s", addonname, greeting(target:name())) - end) diff --git a/tests/actions/addon/demo-addon/src/rules/base/xmake.lua b/tests/actions/addon/demo-addon/src/rules/base/xmake.lua deleted file mode 100644 index 0417c2361..000000000 --- a/tests/actions/addon/demo-addon/src/rules/base/xmake.lua +++ /dev/null @@ -1,4 +0,0 @@ -rule("base") - on_load(function (target) - print("demo-addon: rule base is loaded") - end) diff --git a/tests/actions/addon/demo-addon/src/templates/c/demoaddon/hello/src/main.c b/tests/actions/addon/demo-addon/src/templates/c/demoaddon/hello/src/main.c deleted file mode 100644 index 9b130982d..000000000 --- a/tests/actions/addon/demo-addon/src/templates/c/demoaddon/hello/src/main.c +++ /dev/null @@ -1,3 +0,0 @@ -int main(int argc, char** argv) { - return 0; -} diff --git a/tests/actions/addon/demo-addon/src/toolchains/demo/xmake.lua b/tests/actions/addon/demo-addon/src/toolchains/demo/xmake.lua deleted file mode 100644 index 701a72e29..000000000 --- a/tests/actions/addon/demo-addon/src/toolchains/demo/xmake.lua +++ /dev/null @@ -1,6 +0,0 @@ -toolchain("demo") - set_kind("standalone") - set_description("the demo toolchain of the demo addon") - on_load(function (toolchain) - toolchain:set("toolset", "cc", "gcc") - end) diff --git a/tests/actions/addon/demo-addon/tests/hello_test.lua b/tests/actions/addon/demo-addon/tests/hello_test.lua deleted file mode 100644 index 1fcb312de..000000000 --- a/tests/actions/addon/demo-addon/tests/hello_test.lua +++ /dev/null @@ -1 +0,0 @@ --- our own test file, it should not be installed diff --git a/tests/actions/addon/test.lua b/tests/actions/addon/test.lua index a72cc4236..3542c907b 100644 --- a/tests/actions/addon/test.lua +++ b/tests/actions/addon/test.lua @@ -1,20 +1,14 @@ import("core.base.global") +import("lib.detect.find_program") --- the addon fixtures, @see tests/actions/addon/demo-addon +-- the addon fixtures, each of them provides one kind of payload only -- --- demo-addon: the `src` layout, it provides all the payload kinds --- demo-addon-dep: the plain layout (payloads at the root), it depends on demo-addon -local ADDON = "demo-addon" -local ADDON_DEP = "demo-addon-dep" +-- @note they are independent from each other, @see tests/actions/addon/custom-* function _addondir(name) return path.join(os.scriptdir(), name) end -function _installdir(name) - return path.join(global.directory(), "addons", name, "latest") -end - function _remove(name) try { function () os.runv("xmake", {"addon", "--remove", "--force", name}) end } end @@ -44,25 +38,13 @@ function _with_addons(names, func) } end --- create a temporary repository which indexes the addon fixtures, and register it +-- 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(func) +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" - local recipes = { - -- the addon itself, its manifest sets the payload root directory, e.g. set_sourcedir("src") - [ADDON] = ("set_sourcedir(%q)"):format(_addondir(ADDON)), - -- the addon which depends on the addon above - [ADDON_DEP] = ("set_sourcedir(%q)\n add_deps(%q, {kind = \"addon\"})"):format(_addondir(ADDON_DEP), ADDON), - -- the addon which provides the same plugin and template names as demo-addon - -- - -- @note it points at the payloads directly, so it has no manifest and no name conflict - ["demo-addon-clone"] = ("set_sourcedir(%q)"):format(path.join(_addondir(ADDON), "src")), - -- the addon whose package name does not match the name in its manifest - ["demo-addon-badname"] = ("set_sourcedir(%q)"):format(_addondir(ADDON)) - } 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)) @@ -99,16 +81,20 @@ function _with_repo(func) } end --- run `xmake config` in a temporary project and return its output -function _config_project(content) +-- 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", {"config", "-y"}) end, + function () out = os.iorunv("xmake", argv) end, catch { function (e) errors = e end }, finally { @@ -124,123 +110,183 @@ function _config_project(content) return out end +function _config_project(content) + return _run_project(content, {"config", "-y"}) +end + -- only the payloads should be installed, our own files should not function test_install(t) - _with_addons({ADDON}, function () - local installdir = _installdir(ADDON) - for _, payloaddir in ipairs({"plugins", "rules", "toolchains", "modules", "includes", "templates"}) do - t:require(os.isdir(path.join(installdir, payloaddir))) - end - for _, ourfile in ipairs({"src", "tests"}) do + _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 demo addon of the tests", 1, true)) + 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", {"demo_hello"}); return true end }) + t:require_not(try { function () os.runv("xmake", {"hello_addon"}); return true end }) end --- all the payload kinds should be activated -function test_payloads(t) - _with_addons({ADDON}, function () +-- 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) - -- the plugin imports a module of its own addon with `@self` - t:require(os.iorunv("xmake", {"demo_hello", "-n", "xmake"}):find("hello from demo-addon: xmake", 1, true)) + -- it should fail if the addon which provides it is not installed + t:require_not(try { function () _config_project(projectfile); return true end }) +end - -- the module can be imported with the addon name - local script = "import(\"@addon.demo-addon.greeting\"); print(greeting(\"module\"))" - t:require(os.iorunv("xmake", {"lua", "-c", script}):find("hello from demo-addon: module", 1, true)) +-- 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 toolchain can be loaded with the addon name - local script2 = "import(\"core.tool.toolchain\"); print(toolchain.load(\"@addon/demo-addon/demo\"):get(\"description\"))" - t:require(os.iorunv("xmake", {"lua", "-c", script2}):find("the demo toolchain", 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)) - -- the template should be listed and can create a project - t:require(os.iorunv("xmake", {"create", "--list", "-l", "c"}):find("demoaddon.hello", 1, true)) local projectdir = os.tmpfile() .. ".addon-project" os.tryrm(projectdir) - os.runv("xmake", {"create", "-l", "c", "-t", "demoaddon.hello", "-P", 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 - -- the includes and the rules should work in a project, the app rule depends on - -- the other rule of the same addon with `add_deps("@self/base")` - local out = _config_project([[ -includes("@addon/demo-addon/check") +-- 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") + local script2 = "import(\"core.tools.mycl6x\"); print(mycl6x.greeting()); " .. + "import(\"lib.detect.find_tool\"); assert(find_tool(\"mycl6x\"), \"mycl6x not found!\")" + t:require(os.iorunv("xmake", {"lua", "-c", script2}):find("hello from the custom toolchain addon", 1, true)) + + -- and a project can build with it + -- + -- @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("phony") - add_rules("@addon/demo-addon/app") -]]) - t:require(out:find("demo-addon: includes check is loaded", 1, true)) - t:require(out:find("demo-addon: rule base is loaded", 1, true)) - t:require(out:find("demo-addon: rule app is loaded by the addon(demo-addon), hello from demo-addon: test", 1, true)) + set_kind("binary") + set_toolchains("@addon/custom-toolchain/my-c6000") + add_files("src/main.c") +]], {"build", "-y"}, {files = {["src/main.c"] = "int main(int argc, char** argv) { return 0; }\n"}}) + end end) end --- install the addons from a repository, by plain name and by repo@name +-- the addons can be installed from a repository, by plain name and by repo@name function test_install_from_repo(t) - _with_repo(function (reponame) - os.runv("xmake", {"addon", "--install", "-y", ADDON}) - t:require(os.iorunv("xmake", {"demo_hello"}):find("hello from demo-addon", 1, true)) + local recipes = {["custom-plugin"] = ("set_sourcedir(%q)"):format(_addondir("custom-plugin"))} + _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", ADDON}) - os.runv("xmake", {"addon", "--install", "-y", reponame .. "@" .. ADDON}) - t:require(os.iorunv("xmake", {"demo_hello"}):find("hello from demo-addon", 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 - t:require(os.iorunv("xmake", {"addon", "--search", ADDON}):find(ADDON, 1, true)) - t:require_not(os.iorunv("xrepo", {"search", ADDON}):find(ADDON, 1, true)) + t:require(os.iorunv("xmake", {"addon", "--search", "custom-plugin"}):find("custom-plugin", 1, true)) + t:require_not(os.iorunv("xrepo", {"search", "custom-plugin"}):find("custom-plugin", 1, true)) end) end --- an addon can depend on the other addons, and use their rules and modules +-- an addon can depend on the other addons, they are installed and activated together function test_addon_deps(t) - _with_repo(function () - - -- installing it should install and activate its addon dependency - os.runv("xmake", {"addon", "--install", "-y", ADDON_DEP}) - t:require(os.iorunv("xmake", {"addon", "--list"}):find(ADDON, 1, true)) + 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", ADDON}); return true end }) - - -- its rule depends on the rule of the other addon and imports its module - local out = _config_project([[ -target("test") - set_kind("phony") - add_rules("@addon/demo-addon-dep/dep") -]]) - t:require(out:find("demo-addon: rule base is loaded", 1, true)) - t:require(out:find("demo-addon-dep: hello from demo-addon: dep", 1, true)) + 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_repo(function () - os.runv("xmake", {"addon", "--install", "-y", ADDON}) + _with_addons({"custom-plugin"}, function () - -- this addon provides the same plugin and template names - t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "demo-addon-clone"}); return true end }) + -- 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", {"addon", "--list"}):find(ADDON, 1, true)) - t:require(os.iorunv("xmake", {"demo_hello"}):find("hello from demo-addon", 1, true)) + 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 - _with_repo(function () - t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "demo-addon-badname"}); return true end }) + 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) - t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "somerepo@.."}); return true end }) + -- the `addon` name is reserved t:require_not(try { function () _config_project([[ add_requires("addon") target("test") diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua index 878b1712d..481b9334b 100644 --- a/xmake/actions/addon/main.lua +++ b/xmake/actions/addon/main.lua @@ -118,12 +118,19 @@ function _install_from_local(dir, name) { function () addon.register(name, LOCALVERSION, manifest and { - description = manifest.description, deps = #manifest.deps > 0 and manifest.deps or nil}) + 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 } diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua index e5b03b959..1f116bcd4 100644 --- a/xmake/core/package/addon.lua +++ b/xmake/core/package/addon.lua @@ -154,16 +154,25 @@ end -- @return the errors if there are some conflicts -- function addon._check_conflicts(dirname, addoninfo) - for _, kind in ipairs({"plugins", "templates"}) do + 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", - kind == "plugins" and "plugin" or "template", name, otherdirname, otherdirname) + kindnames[kind], name, otherdirname, otherdirname) end end end end + + -- the global modules can also conflict with the builtin ones + for _, name in ipairs(addoninfo.globalmodules or {}) do + local modulefile = path.join(os.programdir(), "modules", (name:gsub("%.", "/")) .. ".lua") + if os.isfile(modulefile) then + return string.format("global module(%s) conflicts, it has been provided by xmake!\nplease rename it in the addon manifest.", name) + end + end end -- get the addons which depend on the given addon @@ -203,6 +212,7 @@ function addon.apis() , "addon.set_sourcedir" -- addon.add_xxx , "addon.add_deps" + , "addon.add_globalmodules" } } end @@ -252,7 +262,8 @@ function addon.manifest(sourcedir) homepage = addoninfo:get("homepage"), license = addoninfo:get("license"), sourcedir = addoninfo:get("sourcedir"), - deps = table.wrap(addoninfo:get("deps"))} + 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) @@ -422,6 +433,28 @@ function addon.addondir(name, version) 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" @@ -560,6 +593,7 @@ function addon.register(name, version, opt) -- 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)} @@ -665,6 +699,7 @@ function addon.clear() end addon._ADDONS = {} addon._MANIFESTS = nil + addon._GLOBALMODULES = nil end -- return module diff --git a/xmake/core/sandbox/modules/import/core/sandbox/module.lua b/xmake/core/sandbox/modules/import/core/sandbox/module.lua index 3fd2d0fe6..4cf419b44 100644 --- a/xmake/core/sandbox/modules/import/core/sandbox/module.lua +++ b/xmake/core/sandbox/modules/import/core/sandbox/module.lua @@ -441,28 +441,6 @@ function core_sandbox_module.coredir() return path.join(os.programdir(), "core/sandbox/modules/import") end --- get the module directories for the given addon reference --- --- they are only used for the addon modules, --- e.g. import("@addon.esp32.sdkconfig"), import("@self.sdkconfig") --- -function core_sandbox_module.addon_directories(modulesdir) - local moduledirs = {modulesdir} - - -- add the modules of the addon toolchains, e.g. <addondir>/toolchains/<name>/modules - -- so that a custom toolchain can bundle its tool modules together - local toolchainsdir = path.join(path.directory(modulesdir), "toolchains") - if os.isdir(toolchainsdir) then - for _, toolchaindir in ipairs(os.dirs(path.join(toolchainsdir, "*"))) do - local dir = path.join(toolchaindir, "modules") - if os.isdir(dir) then - table.insert(moduledirs, dir) - end - end - end - return moduledirs -end - -- add module directories function core_sandbox_module.add_directories(...) local moduledirs = core_sandbox_module.directories() @@ -567,9 +545,22 @@ function core_sandbox_module.import(name, opt) -- init module directories (disable local packages?) local modules_directories if addon_modulesdir then - modules_directories = core_sandbox_module.addon_directories(addon_modulesdir) + -- 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 diff --git a/xmake/modules/private/action/require/impl/actions/install.lua b/xmake/modules/private/action/require/impl/actions/install.lua index 8bc51f4b3..c9e09763e 100644 --- a/xmake/modules/private/action/require/impl/actions/install.lua +++ b/xmake/modules/private/action/require/impl/actions/install.lua @@ -529,7 +529,8 @@ function main(package) end addon.register(package:name(), package:version_str() or "latest", {description = description, deps = deps, - manifest_deps = manifest and manifest.deps or nil}) + manifest_deps = manifest and manifest.deps or nil, + globalmodules = manifest and #manifest.globalmodules > 0 and manifest.globalmodules or nil}) end installed_now = true end |
