summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2026-08-09 17:57:20 +0800
committerruki <[email protected]>2026-08-09 18:28:59 +0800
commit7caece0cb3ee8ab207130edc17421d604610e686 (patch)
tree0c29dff7e79f0b455f707b9dec7de6ea1cabe2f8
parentcf62f410738a7489fe052d183bcf616ecf5f7067 (diff)
improve addon to import includes, rules and toolchains
-rw-r--r--tests/actions/addon/test.lua334
-rw-r--r--xmake/actions/addon/main.lua9
-rw-r--r--xmake/actions/addon/xmake.lua1
-rw-r--r--xmake/actions/create/main.lua13
-rw-r--r--xmake/actions/create/template.lua44
-rw-r--r--xmake/core/base/interpreter.lua22
-rw-r--r--xmake/core/base/task.lua16
-rw-r--r--xmake/core/package/addon.lua238
-rw-r--r--xmake/core/project/project.lua4
-rw-r--r--xmake/core/project/rule.lua87
-rw-r--r--xmake/core/sandbox/modules/import/core/project/project.lua4
-rw-r--r--xmake/core/sandbox/modules/import/core/sandbox/module.lua63
-rw-r--r--xmake/core/tool/toolchain.lua53
-rw-r--r--xmake/modules/private/action/require/impl/actions/install.lua12
-rw-r--r--xmake/modules/private/action/require/impl/package.lua19
-rw-r--r--xmake/modules/private/xrepo/action/remove.lua3
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