From 7caece0cb3ee8ab207130edc17421d604610e686 Mon Sep 17 00:00:00 2001 From: ruki Date: Sun, 9 Aug 2026 17:57:20 +0800 Subject: improve addon to import includes, rules and toolchains --- tests/actions/addon/test.lua | 334 +++++++++++++++++++-- xmake/actions/addon/main.lua | 9 +- xmake/actions/addon/xmake.lua | 1 + xmake/actions/create/main.lua | 13 + xmake/actions/create/template.lua | 44 ++- xmake/core/base/interpreter.lua | 22 ++ xmake/core/base/task.lua | 16 +- xmake/core/package/addon.lua | 238 ++++++++++++++- xmake/core/project/project.lua | 4 + xmake/core/project/rule.lua | 87 +++++- .../modules/import/core/project/project.lua | 4 + .../sandbox/modules/import/core/sandbox/module.lua | 63 +++- xmake/core/tool/toolchain.lua | 53 +++- .../action/require/impl/actions/install.lua | 12 +- .../private/action/require/impl/package.lua | 19 +- xmake/modules/private/xrepo/action/remove.lua | 3 +- 16 files changed, 854 insertions(+), 68 deletions(-) diff --git a/tests/actions/addon/test.lua b/tests/actions/addon/test.lua index d393c0545..2be6acf94 100644 --- a/tests/actions/addon/test.lua +++ b/tests/actions/addon/test.lua @@ -1,8 +1,17 @@ import("core.base.global") --- write a minimal plugin that prints its name when run +-- the payloads of the mocked addons +-- +-- the plugins and templates are not namespaced, so they are named with the addon name to keep them unique, +-- the other payloads are always referenced with `@addon//` or `@self/`, so they can use fixed names +local RULENAME = "flash" +local RULEBASENAME = "base" +local TOOLCHAINAME = "xtensa" +local MODULENAME = "sdkconfig" +local INCLUDESNAME = "check" + +-- write a minimal plugin, it imports a module of its own addon with `@self` -- --- the plugins of an addon are placed in its `plugins` payload directory, -- e.g. /plugins//xmake.lua function _write_plugin(dir, name) io.writefile(path.join(dir, "xmake.lua"), string.format([[ @@ -11,11 +20,15 @@ task("%s") 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)) + io.writefile(path.join(dir, "main.lua"), string.format([[ +function main() + import("@self.%s") + print("%s: " .. %s()) +end +]], MODULENAME, name, MODULENAME)) end --- write a minimal template into the `templates` payload directory of an addon, --- e.g. /templates///xmake.lua +-- write a minimal template, e.g. /templates///xmake.lua function _write_template(dir, lang, templateid) local templatedir = path.join(dir, "templates", lang, templateid) io.writefile(path.join(templatedir, "xmake.lua"), [[ @@ -28,37 +41,106 @@ int main(int argc, char** argv) { return 0; } ]]) end --- write an addon payload directory, it provides a plugin and a template +-- write two rules, the main one depends on the other one of the same addon with `@self` +-- +-- e.g. /rules//xmake.lua +function _write_rules(dir, name) + io.writefile(path.join(dir, "rules", RULEBASENAME, "xmake.lua"), string.format([[ +rule("%s") + on_load(function (target) + print("hello from rule %s") + end) +]], RULEBASENAME, RULEBASENAME)) + io.writefile(path.join(dir, "rules", RULENAME, "xmake.lua"), string.format([[ +rule("%s") + add_deps("@self/%s") + on_load(function (target) + import("@self.%s") + print("hello from rule %s of %s: " .. %s()) + end) +]], RULENAME, RULEBASENAME, MODULENAME, RULENAME, name, MODULENAME)) +end + +-- write a minimal toolchain, e.g. /toolchains//xmake.lua +function _write_toolchain(dir, name) + io.writefile(path.join(dir, "toolchains", TOOLCHAINAME, "xmake.lua"), string.format([[ +toolchain("%s") + set_kind("standalone") + set_description("hello from toolchain %s of %s") + on_load(function (toolchain) + toolchain:set("toolset", "cc", "gcc") + end) +]], TOOLCHAINAME, TOOLCHAINAME, name)) +end + +-- write a minimal module, e.g. /modules/.lua +function _write_module(dir, name) + io.writefile(path.join(dir, "modules", MODULENAME .. ".lua"), string.format([[ +function main() + return "hello from module %s of %s" +end +]], MODULENAME, name)) +end + +-- write a minimal includes file, e.g. /includes//xmake.lua +function _write_includes(dir, name) + io.writefile(path.join(dir, "includes", INCLUDESNAME, "xmake.lua"), string.format([[ +print("hello from includes %s of %s") +]], INCLUDESNAME, name)) +end + +-- write an addon payload directory, it provides all the supported payloads function _write_addon(dir, name) _write_plugin(path.join(dir, "plugins", name), name) _write_template(dir, "c", name) + _write_rules(dir, name) + _write_toolchain(dir, name) + _write_module(dir, name) + _write_includes(dir, name) end -- write an addon package description, its payloads are placed in the `src` directory -- -- addons in a repository are described as packages, e.g. /addons///xmake.lua -function _write_addon_package(dir, name) +-- +-- @param opt the options, e.g. {deps = {"other-addon"}} +function _write_addon_package(dir, name, opt) + opt = opt or {} + local deps = "" + for _, depname in ipairs(opt.deps) do + deps = deps .. string.format("\n add_deps(\"%s\", {kind = \"addon\"})", depname) + end io.writefile(path.join(dir, "xmake.lua"), string.format([[ package("%s") set_kind("addon") set_description("say hello from %s") - set_sourcedir(path.join(os.scriptdir(), "src")) -]], name, name)) + set_sourcedir(path.join(os.scriptdir(), "src"))%s +]], name, name, deps)) _write_addon(path.join(dir, "src"), name) end -- create a temporary addon repository (packages layout: addons//) and register it -- +-- @param basenames the addon base names, e.g. {"hello", "world"} +-- @param opt the options, e.g. {deps = {hello = {2}}}, the first addon depends on the second one +-- -- @return reponame, names, cleanup -function _mock_repo(basenames) +function _mock_repo(basenames, opt) + opt = opt or {} local suffix = path.filename(os.tmpfile()):gsub("[^%w]", "") local reponame = "addon-test-repo-" .. suffix local repodir = os.tmpfile() .. ".addon-repo" local names = {} for _, base in ipairs(basenames) do - local name = base .. "-" .. suffix - _write_addon_package(path.join(repodir, "addons", name:sub(1, 1), name), name) - table.insert(names, name) + table.insert(names, base .. "-" .. suffix) + end + for idx, base in ipairs(basenames) do + local name = names[idx] + local deps = {} + for _, depidx in ipairs(table.wrap((opt.deps or {})[base])) do + table.insert(deps, names[depidx]) + end + _write_addon_package(path.join(repodir, "addons", name:sub(1, 1), name), name, {deps = deps}) end -- register the repository into the cache @@ -73,8 +155,8 @@ function _mock_repo(basenames) local function cleanup() for _, name in ipairs(names) do + try { function () os.runv("xmake", {"addon", "--remove", "--force", name}) end } os.tryrm(path.join(global.directory(), "addons", name)) - try { function () os.runv("xmake", {"addon", "--remove", name}) end } end local cache = os.isfile(cachefile) and io.load(cachefile) or {} if cache.repositories then @@ -88,8 +170,8 @@ function _mock_repo(basenames) end -- run the given function with a mocked repository, we always clean it up even if the test fails -function _with_repo(basenames, func) - local reponame, names, cleanup = _mock_repo(basenames) +function _with_repo(basenames, func, opt) + local reponame, names, cleanup = _mock_repo(basenames, opt) try { function () @@ -102,6 +184,38 @@ function _with_repo(basenames, func) } end +-- run `xmake config` in a temporary project and return its output +function _config_project(content) + local projectdir = os.tmpfile() .. ".addon-project" + os.tryrm(projectdir) + io.writefile(path.join(projectdir, "xmake.lua"), content) + local oldir = os.cd(projectdir) + local out, errors + try + { + function () + out = os.iorunv("xmake", {"config", "-y"}) + 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 + -- install an addon from a repository, by plain name and by repo@name function test_install_from_repo(t) _with_repo({"hello"}, function (reponame, names) @@ -120,6 +234,29 @@ function test_install_from_repo(t) end) end +-- an addon can reference its own payloads with `@self`, it never needs to know its installed name +function test_self_reference(t) + _with_repo({"hello"}, function (_, names) + local name = names[1] + os.runv("xmake", {"addon", "--install", "-y", name}) + + -- the plugin imports its own module with `import("@self.")` + local out = os.iorunv("xmake", {name}) + t:require(out:find("hello from module " .. MODULENAME .. " of " .. name, 1, true)) + + -- the rule depends on the other rule of the same addon with `add_deps("@self/")` + out = _config_project(string.format([[ +target("test") + set_kind("phony") + add_rules("@addon/%s/%s") +]], name, RULENAME)) + t:require(out:find("hello from rule " .. RULEBASENAME, 1, true)) + t:require(out:find("hello from rule " .. RULENAME .. " of " .. name, 1, true)) + + os.runv("xmake", {"addon", "--remove", name}) + end) +end + -- the templates of an installed addon can be used by `xmake create` function test_install_templates(t) _with_repo({"hello"}, function (_, names) @@ -136,8 +273,135 @@ function test_install_templates(t) os.runv("xmake", {"create", "-l", "c", "-t", name, "-P", projectdir}) t:require(os.isfile(path.join(projectdir, "xmake.lua"))) t:require(os.isfile(path.join(projectdir, "src", "main.c"))) - os.tryrm(projectdir) + + os.runv("xmake", {"addon", "--remove", name}) + end) +end + +-- the rules of an installed addon can be used with the `@addon//` prefix +function test_install_rules(t) + _with_repo({"hello"}, function (_, names) + local name = names[1] + os.runv("xmake", {"addon", "--install", "-y", name}) + + -- it should be found in the global rules + local script = string.format("import(\"core.project.rule\"); print(rule.rule(\"@addon/%s/%s\") ~= nil)", name, RULENAME) + t:require(os.iorunv("xmake", {"lua", "-c", script}):find("true", 1, true)) + + -- the addon name is always required + local script2 = string.format("import(\"core.project.rule\"); print(rule.rule(\"@addon/%s\"))", RULENAME) + t:require_not(try { function () os.iorunv("xmake", {"lua", "-c", script2}); return true end }) + + os.runv("xmake", {"addon", "--remove", name}) + end) +end + +-- the includes of an installed addon can be used with the `@addon//` prefix +function test_install_includes(t) + _with_repo({"hello"}, function (_, names) + local name = names[1] + os.runv("xmake", {"addon", "--install", "-y", name}) + + local out = _config_project(string.format([[ +includes("@addon/%s/%s") +target("test") + set_kind("phony") +]], name, INCLUDESNAME)) + t:require(out:find("hello from includes " .. INCLUDESNAME .. " of " .. name, 1, true)) + + os.runv("xmake", {"addon", "--remove", name}) + end) +end + +-- the toolchains of an installed addon can be loaded with the `@addon//` prefix +function test_install_toolchains(t) + _with_repo({"hello"}, function (_, names) + local name = names[1] + os.runv("xmake", {"addon", "--install", "-y", name}) + + local script = string.format("import(\"core.tool.toolchain\"); print(toolchain.load(\"@addon/%s/%s\"):get(\"description\"))", name, TOOLCHAINAME) + t:require(os.iorunv("xmake", {"lua", "-c", script}):find("hello from toolchain " .. TOOLCHAINAME .. " of " .. name, 1, true)) + + -- it can also be bound to a package, e.g. "@addon//clang@llvm" + local script2 = string.format("import(\"core.tool.toolchain\"); print(toolchain.load(\"@addon/%s/%s@llvm\"):config(\"packages\"))", name, TOOLCHAINAME) + t:require(os.iorunv("xmake", {"lua", "-c", script2}):find("llvm", 1, true)) + + -- it should not be found without the `@addon//` prefix + local script3 = string.format("import(\"core.tool.toolchain\"); print(toolchain.load(\"%s\"))", TOOLCHAINAME) + t:require_not(try { function () os.iorunv("xmake", {"lua", "-c", script3}); return true end }) + + os.runv("xmake", {"addon", "--remove", name}) + end) +end + +-- the modules of an installed addon can be imported with the `@addon..` prefix +function test_install_modules(t) + _with_repo({"hello"}, function (_, names) + local name = names[1] + os.runv("xmake", {"addon", "--install", "-y", name}) + + local script = string.format("import(\"@addon.%s.%s\"); print(%s())", name, MODULENAME, MODULENAME) + t:require(os.iorunv("xmake", {"lua", "-c", script}):find("hello from module " .. MODULENAME .. " of " .. name, 1, true)) + + -- the addon name is always required + local script2 = string.format("import(\"@addon.%s\")", MODULENAME) + t:require_not(try { function () os.iorunv("xmake", {"lua", "-c", script2}); return true end }) + + os.runv("xmake", {"addon", "--remove", name}) + end) +end + +-- an addon can depend on the other addons with `add_deps(name, {kind = "addon"})` +function test_addon_deps(t) + _with_repo({"hello", "world"}, function (_, names) + local name, depname = names[1], names[2] + + -- installing the first addon should install and activate its addon dependency + os.runv("xmake", {"addon", "--install", "-y", name}) + t:require(os.iorunv("xmake", {"addon", "--list"}):find(depname, 1, true)) + + -- the payloads of the dependency should be usable, e.g. its plugin + t:require(os.iorunv("xmake", {depname}):find(depname, 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", depname}); return true end }) + + os.runv("xmake", {"addon", "--remove", name}) + os.runv("xmake", {"addon", "--remove", depname}) + end, {deps = {hello = {2}}}) +end + +-- the plugins and templates are not namespaced, the conflicts should be rejected when installing +function test_install_conflicts(t) + _with_repo({"hello"}, function (_, names) + local name = names[1] + os.runv("xmake", {"addon", "--install", "-y", name}) + + -- install another addon which provides the same plugin name + local othername = name .. "-other" + local dir = path.join(os.tmpfile() .. ".addon-conflict", othername) + _write_plugin(path.join(dir, "plugins", name), name) + _write_module(dir, name) + try + { + function () + -- it should be rejected + t:require_not(try { function () os.runv("xmake", {"addon", "--install", dir}); return true end }) + + -- and the other commands should still work + t:require(os.iorunv("xmake", {"addon", "--list"}):find(name, 1, true)) + t:require(os.iorunv("xmake", {name}):find(name, 1, true)) + end, + finally + { + function () + try { function () os.runv("xmake", {"addon", "--remove", "--force", othername}) end } + os.tryrm(path.directory(dir)) + end + } + } + os.runv("xmake", {"addon", "--remove", name}) end) end @@ -148,15 +412,24 @@ function test_install_from_local(t) local name = "hello-local-" .. suffix local dir = path.join(os.tmpfile() .. ".addon-local", name) _write_addon(dir, name) + try + { + function () + os.runv("xmake", {"addon", "--install", dir}) + t:require(os.iorunv("xmake", {name}):find(name, 1, true)) - os.runv("xmake", {"addon", "--install", dir}) - t:require(os.iorunv("xmake", {name}):find(name, 1, true)) - - -- the removed addon should no longer be runnable - os.runv("xmake", {"addon", "--remove", name}) - t:require_not(try { function () os.runv("xmake", {name}); return true end }) - - os.tryrm(path.directory(dir)) + -- the removed addon should no longer be runnable + os.runv("xmake", {"addon", "--remove", name}) + t:require_not(try { function () os.runv("xmake", {name}); return true end }) + end, + finally + { + function () + try { function () os.runv("xmake", {"addon", "--remove", name}) end } + os.tryrm(path.directory(dir)) + end + } + } end -- --list shows the installed addons and their payloads @@ -169,7 +442,7 @@ function test_list(t) t:require(out:find("the installed addons:", 1, true)) t:require(out:find(names[1], 1, true)) - -- the payloads of the installed addon should be shown, e.g. (plugins, templates) + -- the payloads of the installed addon should be shown, e.g. (plugins, rules, templates) t:require(out:find("plugins", 1, true)) t:require(out:find("templates", 1, true)) @@ -194,6 +467,15 @@ function test_search(t) end) end +-- the `addon` package name is reserved for the addon references +function test_reserved_name(t) + t:require_not(try { function () _config_project([[ +add_requires("addon") +target("test") + set_kind("phony") +]]); return true end }) +end + -- invalid installs should fail function test_install_invalid(t) t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "addon-test-missing"}); return true end }) diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua index f3da68d8e..c88e6300e 100644 --- a/xmake/actions/addon/main.lua +++ b/xmake/actions/addon/main.lua @@ -62,6 +62,9 @@ function _xrepo(action, names) if option.get("diagnosis") then table.insert(argv, "-D") end + if option.get("force") then + table.insert(argv, "--force") + end table.join2(argv, names) os.execv(os.programfile(), argv) end @@ -80,7 +83,11 @@ function _install_from_local(dir, name) local dstdir = _get_addondir(name, LOCALVERSION) assert(not os.isdir(dstdir), "addon(%s) already exists!", name) os.vcp(dir, dstdir) - addon.register(name, LOCALVERSION) + local ok, errors = addon.register(name, LOCALVERSION) + if not ok then + os.tryrm(dstdir) + raise(errors) + end cprint("${color.success}install ${bright}%s${clear} ok!", name) end diff --git a/xmake/actions/addon/xmake.lua b/xmake/actions/addon/xmake.lua index 240fef5bf..3c814cc6d 100644 --- a/xmake/actions/addon/xmake.lua +++ b/xmake/actions/addon/xmake.lua @@ -30,6 +30,7 @@ task("addon") {'s', "search", "k", nil, "Search the addons from the repositories."}, {'l', "list", "k", nil, "List all installed addons."}, {'c', "clear", "k", nil, "Clear all installed addons."}, + {'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", diff --git a/xmake/actions/create/main.lua b/xmake/actions/create/main.lua index 476fd1325..c7be76b48 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//