diff options
| author | ruki <[email protected]> | 2026-08-09 17:57:20 +0800 |
|---|---|---|
| committer | ruki <[email protected]> | 2026-08-09 18:28:59 +0800 |
| commit | 7caece0cb3ee8ab207130edc17421d604610e686 (patch) | |
| tree | 0c29dff7e79f0b455f707b9dec7de6ea1cabe2f8 | |
| parent | cf62f410738a7489fe052d183bcf616ecf5f7067 (diff) | |
improve addon to import includes, rules and toolchains
| -rw-r--r-- | tests/actions/addon/test.lua | 334 | ||||
| -rw-r--r-- | xmake/actions/addon/main.lua | 9 | ||||
| -rw-r--r-- | xmake/actions/addon/xmake.lua | 1 | ||||
| -rw-r--r-- | xmake/actions/create/main.lua | 13 | ||||
| -rw-r--r-- | xmake/actions/create/template.lua | 44 | ||||
| -rw-r--r-- | xmake/core/base/interpreter.lua | 22 | ||||
| -rw-r--r-- | xmake/core/base/task.lua | 16 | ||||
| -rw-r--r-- | xmake/core/package/addon.lua | 238 | ||||
| -rw-r--r-- | xmake/core/project/project.lua | 4 | ||||
| -rw-r--r-- | xmake/core/project/rule.lua | 87 | ||||
| -rw-r--r-- | xmake/core/sandbox/modules/import/core/project/project.lua | 4 | ||||
| -rw-r--r-- | xmake/core/sandbox/modules/import/core/sandbox/module.lua | 63 | ||||
| -rw-r--r-- | xmake/core/tool/toolchain.lua | 53 | ||||
| -rw-r--r-- | xmake/modules/private/action/require/impl/actions/install.lua | 12 | ||||
| -rw-r--r-- | xmake/modules/private/action/require/impl/package.lua | 19 | ||||
| -rw-r--r-- | 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/<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. <addondir>/plugins/<name>/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. <addondir>/templates/<language>/<templateid>/xmake.lua +-- write a minimal template, e.g. <addondir>/templates/<language>/<templateid>/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. <addondir>/rules/<name>/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. <addondir>/toolchains/<name>/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. <addondir>/modules/<name>.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. <addondir>/includes/<name>/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. <repodir>/addons/<first-letter>/<name>/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/<first-letter>/<name>) 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.<module>")` + 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/<rule>")` + 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/<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/<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/<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/<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/<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.<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/<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 diff --git a/xmake/actions/create/template.lua b/xmake/actions/create/template.lua index 778028ce8..ff8f9aefa 100644 --- a/xmake/actions/create/template.lua +++ b/xmake/actions/create/template.lua @@ -120,6 +120,26 @@ 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 templatesdir, id, _, errors = addon.resolve_reference(templateid, "/", "templates") + if not templatesdir then + os.raise(errors) + end + local subdir = _templateid_subdir(id) + if subdir then + local dir = path.join(templatesdir, 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 @@ -191,11 +211,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 @@ -219,7 +249,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 + utils.warning("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/core/base/interpreter.lua b/xmake/core/base/interpreter.lua index 83bbdf593..7a08446d8 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} @@ -1811,6 +1812,27 @@ function interpreter:api_builtin_includes(...) 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 + local includesdir, addon_path, _, errors = addon.resolve_reference(subpath, "/", "includes", + {scriptdir = self:scriptdir()}) + if not includesdir then + os.raise(errors) + end + local files + if addon_path:endswith(".lua") then + files = os.files(path.join(includesdir, addon_path)) + else + files = os.files(path.join(includesdir, addon_path, "xmake.lua")) + end + if files and #files > 0 then + table.join2(subpaths_matched, files) + found = true + else + os.raise("includes(%s) not found!", subpath) + end + end -- find the given files from the project directory if not found then local files diff --git a/xmake/core/base/task.lua b/xmake/core/base/task.lua index e8d2441b9..eeb2dd8f2 100644 --- a/xmake/core/base/task.lua +++ b/xmake/core/base/task.lua @@ -415,6 +415,8 @@ function task.tasks() -- load tasks local tasks = {} + local taskfiles = {} + local addondir = path.absolute(addon.installdir()) local dirs = task._directories() for _, dir in ipairs(dirs) do local files = os.files(path.join(dir, "*", "xmake.lua")) @@ -422,7 +424,19 @@ 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 + -- 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 + local taskfile = taskfiles[taskname] + if taskfile and (path.absolute(taskfile):startswith(addondir) or path.absolute(filepath):startswith(addondir)) then + -- @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) + else + 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 index 78830d266..7ced8a780 100644 --- a/xmake/core/package/addon.lua +++ b/xmake/core/package/addon.lua @@ -49,6 +49,103 @@ 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 +-- @return the addon root directory and its name, e.g. ~/.xmake/addons/esp32/v1.0.0, esp32 +-- +function addon.owner(scriptdir) + 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 + return path.join(installdir, parts[1], parts[2]), parts[1] + 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 + for _, payloaddir in ipairs(addon.payloaddirs()) do + if os.isdir(path.join(dir, payloaddir)) then + return dir, path.filename(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 payload directory, the resource name, the addon name and errors +-- +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, nil, nil, string.format("invalid addon reference(%s)!", reference) + end + local addondir, addonname = addon.owner(opt.scriptdir) + if not addondir then + return nil, nil, nil, string.format("%s: cannot resolve `@self`, it can only be used inside an addon!", reference) + end + return path.join(addondir, kind), name, 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, nil, 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, nil, addonname, string.format("%s not found!\nplease install the addon which provides it first: xmake addon --install %s", reference, addonname) + end + return payloaddir, name, addonname +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, @@ -103,6 +200,20 @@ function addon.payloads(kind) return payloads 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 payload information of the given kind from all installed addons -- -- @param kind the payload kind, e.g. "plugins", "rules" @@ -166,20 +277,120 @@ function addon._save(addons) 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) + for _, kind in ipairs({"plugins", "templates"}) 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) + end + end + 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 = "..."} +-- @param opt the options, e.g. {description = "...", deps = {"foo"}} +-- +-- @return true or false and errors -- function addon.register(name, version, opt) opt = opt or {} local dirname = addon.dirname(name) - local addons = addon.addons() - addons[dirname] = {version = version, + local addondir = path.join(addon.installdir(), dirname, version) + local addoninfo = {version = version, description = opt.description, - payloads = addon.payloads_of(path.join(addon.installdir(), dirname, version))} + deps = opt.deps, + 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 + + local addons = addon.addons() + addons[dirname] = addoninfo addon._save(addons) + return true +end + +-- get the addons which depend on the given addon +function addon.parents(name) + local dirname = addon.dirname(name) + local parents + for otherdirname, addoninfo in pairs(addon.addons()) do + if otherdirname ~= dirname and table.contains(addoninfo.deps or {}, dirname) then + parents = parents or {} + table.insert(parents, otherdirname) + end + end + if parents then + table.sort(parents) + end + return parents end -- remove the given installed addon @@ -187,12 +398,22 @@ end -- @param name the addon name -- @return true or false and errors -- -function addon.remove(name) +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 local ok, errors = os.rm(installdir) if not ok then return false, errors @@ -225,11 +446,14 @@ function addon.rescan() local version = path.filename(versiondir) -- we need to keep the description, we cannot get it from the installed payloads local oldaddoninfo = oldaddons[dirname] - local description + local description, deps if oldaddoninfo and oldaddoninfo.version == version then + -- we need to keep them, we cannot get them from the installed payloads description = oldaddoninfo.description + deps = oldaddoninfo.deps end - addons[dirname] = {version = version, description = description, payloads = payloads} + addons[dirname] = {version = version, description = description, deps = deps, payloads = payloads, + plugins = addon.plugins_of(versiondir), templates = addon.templates_of(versiondir)} end end addon._save(addons) diff --git a/xmake/core/project/project.lua b/xmake/core/project/project.lua index d1030f130..803bb5d9b 100644 --- a/xmake/core/project/project.lua +++ b/xmake/core/project/project.lua @@ -1259,6 +1259,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..d98696503 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() @@ -454,8 +460,69 @@ function rule.new(name, info, opt) 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 = rule.rules()[name] + if instance == nil and name:startswith("@addon/") then + local _, _, addonname, 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, addonname or "<addon>") + end + return instance +end + +-- load the rules from the given directory +function rule._load_rules(ruleinfos, dir, opt) + opt = opt or {} + 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 + 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,19 +530,11 @@ 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 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..6daba311e 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,36 @@ 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 + +-- 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() @@ -498,6 +530,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 modulesdir, modulename, addonname, errors = addon.resolve_reference(name, ".", "modules", + {scriptdir = opt.scriptdir or sandbox.instance() and sandbox.instance():rootdir()}) + if not modulesdir then + raise(errors) + end + addon_modulesdir = modulesdir + name = modulename + end + -- get module name local modulename = core_sandbox_module.name(name) if not modulename then @@ -515,7 +565,12 @@ 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 + modules_directories = core_sandbox_module.addon_directories(addon_modulesdir) + else + modules_directories = (opt.nolocal or not rootdir) and core_sandbox_module.directories() or table.join(rootdir, core_sandbox_module.directories()) + end -- load module local loadopt = table.clone(opt) or {} @@ -554,6 +609,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..9c3080026 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,29 @@ 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("@self.xtensa") + -- + -- @note we need to parse it first, because `@` is also used for the toolchain packages, e.g. "@zig" + -- + -- e.g. set_toolchains("@addon/esp32/xtensa"), set_toolchains("@addon/esp32/clang@llvm"), set_toolchains("@self/xtensa") + -- + -- @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 +731,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 @@ -752,6 +777,8 @@ function toolchain.directories() return dirs end + + -- add toolchain directories function toolchain.add_directories(...) local dirs = toolchain.directories() @@ -783,7 +810,8 @@ function toolchain.load(name, opt) -- get cache local cache = toolchain._memcache() - local cachekey = toolchain._cachekey(name, configs) + -- @note we need the addon prefix here, the different addons may provide the same toolchain name + local cachekey = toolchain._cachekey((parseinfo.addon_prefix or "") .. name, configs) -- get it directly from cache dirst local instance = cache:get(cachekey) @@ -793,13 +821,26 @@ function toolchain.load(name, opt) -- 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 parseinfo.addon_prefix then + -- e.g. set_toolchains("@addon/esp32/xtensa"), set_toolchains("@self/xtensa") + local toolchainsdir, toolchainname, _, errors = addon.resolve_reference(parseinfo.addon_prefix .. name, "/", "toolchains", + {scriptdir = opt.scriptdir}) + if not toolchainsdir then + return nil, errors + end + scriptpath = path.join(toolchainsdir, toolchainname, "xmake.lua") + else + 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 + if parseinfo.addon_prefix then + return nil, string.format("the toolchain %s%s not found!", parseinfo.addon_prefix, name) + end return nil, string.format("the toolchain %s not found!", name) end diff --git a/xmake/modules/private/action/require/impl/actions/install.lua b/xmake/modules/private/action/require/impl/actions/install.lua index 40ca225f0..629b37aff 100644 --- a/xmake/modules/private/action/require/impl/actions/install.lua +++ b/xmake/modules/private/action/require/impl/actions/install.lua @@ -503,8 +503,16 @@ function main(package) -- register this addon, so that xmake can find its payloads, e.g. plugins if package:is_addon() then - addon.register(package:name(), package:version_str() or "latest", - {description = package:description()}) + 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 + local ok, errors = addon.register(package:name(), package:version_str() or "latest", + {description = package:description(), deps = deps}) + assert(ok, errors) end installed_now = true end diff --git a/xmake/modules/private/action/require/impl/package.lua b/xmake/modules/private/action/require/impl/package.lua index e57850673..65d5f4153 100644 --- a/xmake/modules/private/action/require/impl/package.lua +++ b/xmake/modules/private/action/require/impl/package.lua @@ -209,11 +209,15 @@ function _load_package_from_repository(packagename, opt) end end --- get the root directory of repositories for the given package kind +-- get the root directory of repositories for the given package -- -- e.g. "packages" (default), "addons", "plugins" (deprecated) -- -function _get_repository_rootdir(packagekind) +-- @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 @@ -982,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 @@ -1027,7 +1037,7 @@ function _load_package(packagename, requireinfo, opt) plat = requireinfo.plat, arch = requireinfo.arch, name = requireinfo.reponame, - rootdir = _get_repository_rootdir(opt.packagekind), + rootdir = _get_repository_rootdir(requireinfo, opt), locked_repo = locked_requireinfo and locked_requireinfo.repo}) if package then from_repo = true @@ -1038,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 = _get_repository_rootdir(opt.packagekind), + rootdir = _get_repository_rootdir(requireinfo, opt), locked_repo = locked_requireinfo and locked_requireinfo.repo}) end @@ -1224,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) diff --git a/xmake/modules/private/xrepo/action/remove.lua b/xmake/modules/private/xrepo/action/remove.lua index adfec92c0..ee078f225 100644 --- a/xmake/modules/private/xrepo/action/remove.lua +++ b/xmake/modules/private/xrepo/action/remove.lua @@ -48,6 +48,7 @@ function menu_options() {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.", @@ -200,7 +201,7 @@ end -- remove the given installed addons function _remove_addons(names) for _, name in ipairs(names) do - local ok, errors = addon.remove(name) + local ok, errors = addon.remove(name, {force = option.get("force")}) assert(ok, errors) cprint("${color.success}remove ${bright}%s${clear} ok!", name) end |
