summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2026-08-11 00:46:53 +0800
committerruki <[email protected]>2026-08-11 00:46:53 +0800
commitd699ee8188196a7e77bc1e2912f9b5dc6bd47416 (patch)
tree6a56f694b067bca75f32da15ac853e17e226d563
parent06a00d75a9574bba124877432553e5cc61f0ba99 (diff)
add addon.lua
-rw-r--r--tests/actions/addon/demo-addon-dep/addon.lua3
-rw-r--r--tests/actions/addon/demo-addon/addon.lua3
-rw-r--r--tests/actions/addon/demo-addon/src/rules/app/xmake.lua5
-rw-r--r--tests/actions/addon/test.lua21
-rw-r--r--xmake/actions/addon/main.lua22
-rw-r--r--xmake/core/package/addon.lua472
-rw-r--r--xmake/core/sandbox/modules/import/core/base/option.lua14
-rw-r--r--xmake/core/sandbox/modules/import/core/package/addon.lua17
8 files changed, 397 insertions, 160 deletions
diff --git a/tests/actions/addon/demo-addon-dep/addon.lua b/tests/actions/addon/demo-addon-dep/addon.lua
new file mode 100644
index 000000000..2cc460768
--- /dev/null
+++ b/tests/actions/addon/demo-addon-dep/addon.lua
@@ -0,0 +1,3 @@
+addon("demo-addon-dep")
+ set_description("the demo addon which depends on the other addon")
+ add_deps("demo-addon")
diff --git a/tests/actions/addon/demo-addon/addon.lua b/tests/actions/addon/demo-addon/addon.lua
new file mode 100644
index 000000000..b676f95ba
--- /dev/null
+++ b/tests/actions/addon/demo-addon/addon.lua
@@ -0,0 +1,3 @@
+addon("demo-addon")
+ set_description("the demo addon of the tests")
+ set_srcdir("src")
diff --git a/tests/actions/addon/demo-addon/src/rules/app/xmake.lua b/tests/actions/addon/demo-addon/src/rules/app/xmake.lua
index ff54eb85a..b6236bc28 100644
--- a/tests/actions/addon/demo-addon/src/rules/app/xmake.lua
+++ b/tests/actions/addon/demo-addon/src/rules/app/xmake.lua
@@ -2,5 +2,8 @@ rule("app")
add_deps("@self/base")
on_load(function (target)
import("@self.greeting")
- print("demo-addon: rule app is loaded, " .. greeting(target:name()))
+ import("core.package.addon")
+ -- the addon code should never hardcode its own name, it can always ask for it
+ local addonname = addon.owner()
+ print("demo-addon: rule app is loaded by the addon(%s), %s", addonname, greeting(target:name()))
end)
diff --git a/tests/actions/addon/test.lua b/tests/actions/addon/test.lua
index 6a65d233c..3929ebb73 100644
--- a/tests/actions/addon/test.lua
+++ b/tests/actions/addon/test.lua
@@ -52,12 +52,16 @@ function _with_repo(func)
local reponame = "addon-test-repo-" .. suffix
local repodir = os.tmpfile() .. ".addon-repo"
local recipes = {
- -- the addon itself
- [ADDON] = ("set_sourcedir(%q)"):format(path.join(_addondir(ADDON), "src")),
+ -- the addon itself, its manifest sets the payload root directory, e.g. set_srcdir("src")
+ [ADDON] = ("set_sourcedir(%q)"):format(_addondir(ADDON)),
-- the addon which depends on the addon above
[ADDON_DEP] = ("set_sourcedir(%q)\n add_deps(%q, {kind = \"addon\"})"):format(_addondir(ADDON_DEP), ADDON),
-- the addon which provides the same plugin and template names as demo-addon
- ["demo-addon-clone"] = ("set_sourcedir(%q)"):format(path.join(_addondir(ADDON), "src"))
+ --
+ -- @note it points at the payloads directly, so it has no manifest and no name conflict
+ ["demo-addon-clone"] = ("set_sourcedir(%q)"):format(path.join(_addondir(ADDON), "src")),
+ -- the addon whose package name does not match the name in its manifest
+ ["demo-addon-badname"] = ("set_sourcedir(%q)"):format(_addondir(ADDON))
}
for name, body in pairs(recipes) do
io.writefile(path.join(repodir, "addons", name:sub(1, 1), name, "xmake.lua"),
@@ -130,7 +134,8 @@ function test_install(t)
for _, ourfile in ipairs({"src", "tests"}) do
t:require_not(os.exists(path.join(installdir, ourfile)))
end
- t:require(os.iorunv("xmake", {"addon", "--list"}):find(ADDON, 1, true))
+ -- the addon describes itself, we should get its description from the manifest
+ t:require(os.iorunv("xmake", {"addon", "--list"}):find("the demo addon of the tests", 1, true))
end)
-- it should not be runnable after removing it
@@ -170,7 +175,7 @@ target("test")
]])
t:require(out:find("demo-addon: includes check is loaded", 1, true))
t:require(out:find("demo-addon: rule base is loaded", 1, true))
- t:require(out:find("demo-addon: rule app is loaded, hello from demo-addon: test", 1, true))
+ t:require(out:find("demo-addon: rule app is loaded by the addon(demo-addon), hello from demo-addon: test", 1, true))
end)
end
@@ -229,6 +234,12 @@ end
-- the invalid installs should fail, and the `addon` name is reserved for the addon references
function test_invalid(t)
t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "addon-test-missing"}); return true end })
+
+ -- the addon name in its manifest must match the package name which distributes it
+ _with_repo(function ()
+ t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "demo-addon-badname"}); return true end })
+ end)
+
t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "somerepo@.."}); return true end })
t:require_not(try { function () _config_project([[
add_requires("addon")
diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua
index b99df4fe0..e3ed7e5be 100644
--- a/xmake/actions/addon/main.lua
+++ b/xmake/actions/addon/main.lua
@@ -88,12 +88,34 @@ function _install_from_local(dir, name)
local payloadroot = addon.payloadroot(dir)
assert(payloadroot, "addon path(%s): no payload directory found, e.g. ${bright}plugins${clear}!", dir)
+ -- the addon describes itself? its manifest is always authoritative,
+ -- otherwise we can only guess its name from the directory name
+ local manifest = addon.manifest(dir)
+ if manifest then
+ name = manifest.name
+ end
name = name or path.filename(dir)
+
local dstdir = _get_addondir(name, LOCALVERSION)
assert(not os.isdir(dstdir), "addon(%s) already exists!", name)
+
+ -- the addons which it depends on are not installed by the local installation,
+ -- so we need to install them from the repositories first
+ if manifest then
+ for _, dep in ipairs(manifest.deps) do
+ if not addon.addons()[addon.dirname(dep)] then
+ _install_from_repo(dep)
+ end
+ end
+ end
+
for _, payloaddir in ipairs(addon.payloads_of(payloadroot)) do
os.vcp(path.join(payloadroot, payloaddir), path.join(dstdir, payloaddir))
end
+ local manifestfile = path.join(dir, "addon.lua")
+ if os.isfile(manifestfile) then
+ os.vcp(manifestfile, path.join(dstdir, "addon.lua"))
+ end
-- we need to roll back the installed payloads if it cannot be registered, e.g. the name conflicts
try
{
diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua
index 8523da8d0..bddd5ea45 100644
--- a/xmake/core/package/addon.lua
+++ b/xmake/core/package/addon.lua
@@ -35,10 +35,232 @@ local global = require("base/global")
--
-- @note only `plugins` is activated for now, the others are reserved
--
-function addon.payloaddirs()
+function addon._payloaddirs()
return {"plugins", "rules", "toolchains", "platforms", "modules", "templates", "themes", "includes"}
end
+-- the manifest file of an addon, e.g. <sourcedir>/addon.lua
+--
+-- an addon describes itself in this file, so its name and layout never depend on
+-- the package name of the repository which distributes it
+--
+function addon._manifestfile(sourcedir)
+ return path.join(sourcedir, "addon.lua")
+end
+
+-- the interpreter of the addon manifest
+function addon._interpreter()
+ local interp = addon._INTERPRETER
+ if interp == nil then
+ -- we need to load it lazily, the interpreter also depends on this module
+ local interpreter = require("base/interpreter")
+ interp = interpreter.new()
+ interp:api_define(addon.apis())
+ addon._INTERPRETER = interp
+ end
+ return interp
+end
+
+-- the registry file of the installed addons, e.g. ~/.xmake/addons/addons.conf
+--
+-- we save all installed addons to this file when installing/removing them,
+-- so we do not need to scan the whole addons directory on startup
+--
+function addon._registryfile()
+ return path.join(addon.installdir(), "addons.conf")
+end
+
+-- save the given addons to the registry file
+function addon._save(addons)
+ addon._ADDONS = addons
+ local registryfile = addon._registryfile()
+ -- we need not create an empty registry file if no addons are installed
+ if next(addons) == nil and not os.isfile(registryfile) then
+ return
+ end
+ local ok, errors = io.save(registryfile, addons)
+ if not ok then
+ utils.warning(errors)
+ end
+end
+
+-- get the payload directory of the given addon
+--
+-- @param name the addon name, e.g. "esp32"
+-- @param kind the payload kind, e.g. "rules", "modules"
+-- @return the directory, e.g. ~/.xmake/addons/esp32/v1.0.0/rules
+--
+function addon._payloaddir(name, kind)
+ local dirname = addon.dirname(name)
+ local addoninfo = addon.addons()[dirname]
+ if addoninfo and table.contains(addoninfo.payloads or {}, kind) then
+ return path.join(addon.installdir(), dirname, addoninfo.version, kind)
+ end
+end
+
+-- get the plugin task names of the given addon directory
+--
+-- the plugins are not namespaced, we need them to check the conflicts
+--
+function addon._plugins_of(addondir)
+ local plugins = {}
+ for _, filepath in ipairs(os.files(path.join(addondir, "plugins", "*", "xmake.lua"))) do
+ local content = io.readfile(filepath)
+ if content then
+ for taskname in content:gmatch("task%s*%(%s*\"(.-)\"") do
+ table.insert(plugins, taskname)
+ end
+ end
+ end
+ return plugins
+end
+
+-- get the template ids of the given addon directory, e.g. {"c/console"}
+--
+-- the templates are not namespaced, we need them to check the conflicts
+--
+function addon._templates_of(addondir)
+ local templates = {}
+ local templatesdir = path.join(addondir, "templates")
+ for _, langdir in ipairs(os.dirs(path.join(templatesdir, "*"))) do
+ local lang = path.filename(langdir)
+ local accepted = {}
+ for _, filepath in ipairs(os.files(path.join(langdir, "**", "xmake.lua"))) do
+ local dir = path.directory(filepath)
+ local relpath = path.relative(dir, langdir)
+ if relpath and relpath ~= "." then
+ local nested = false
+ for _, root in ipairs(accepted) do
+ if dir:startswith(root .. path.sep()) then
+ nested = true
+ break
+ end
+ end
+ if not nested then
+ table.insert(accepted, dir)
+ table.insert(templates, lang .. "/" .. (relpath:gsub("[/\\]", ".")))
+ end
+ end
+ end
+ end
+ return templates
+end
+
+-- check the conflicts of the plugins and templates, they are not namespaced
+--
+-- @param dirname the addon directory name
+-- @param addoninfo the addon information, @see addon.register
+--
+-- @return the errors if there are some conflicts
+--
+function addon._check_conflicts(dirname, addoninfo)
+ 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
+
+-- 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
+
+-- unregister the given addon
+function addon._unregister(name)
+ local dirname = addon.dirname(name)
+ local addons = addon.addons()
+ if addons[dirname] then
+ addons[dirname] = nil
+ addon._save(addons)
+ end
+end
+
+-- get the apis of the addon manifest
+function addon.apis()
+ return {
+ values = {
+ -- addon.set_xxx
+ "addon.set_description"
+ , "addon.set_homepage"
+ , "addon.set_license"
+ , "addon.set_srcdir"
+ -- addon.add_xxx
+ , "addon.add_deps"
+ }
+ }
+end
+
+-- get the manifest of the given addon directory
+--
+-- @param sourcedir the addon source or install directory, which contains `addon.lua`
+--
+-- @return the manifest, e.g. {name = "esp32-devel", description = "...", srcdir = "src", deps = {"serial-tools"}}
+-- it will be nil if this addon does not describe itself
+--
+function addon.manifest(sourcedir)
+
+ -- we may resolve a lot of `@self` references, so we need to cache them
+ local manifests = addon._MANIFESTS
+ if manifests == nil then
+ manifests = {}
+ addon._MANIFESTS = manifests
+ end
+ local cachekey = path.absolute(sourcedir)
+ local cacheinfo = manifests[cachekey]
+ if cacheinfo ~= nil then
+ return cacheinfo or nil
+ end
+
+ local manifestfile = addon._manifestfile(sourcedir)
+ if not os.isfile(manifestfile) then
+ manifests[cachekey] = false
+ return
+ end
+ local interp = addon._interpreter()
+ local ok, errors = interp:load(manifestfile)
+ if not ok then
+ return nil, errors
+ end
+ local results, errors = interp:make("addon", true, true)
+ if not results then
+ return nil, errors
+ end
+ local manifest
+ for name, addoninfo in pairs(results) do
+ if manifest then
+ return nil, string.format("%s: only one addon() scope is allowed!", manifestfile)
+ end
+ manifest = {name = name,
+ description = addoninfo:get("description"),
+ homepage = addoninfo:get("homepage"),
+ license = addoninfo:get("license"),
+ srcdir = addoninfo:get("srcdir"),
+ deps = table.wrap(addoninfo:get("deps"))}
+ end
+ if not manifest then
+ return nil, string.format("%s: no addon() scope found!", manifestfile)
+ end
+ manifests[cachekey] = manifest
+ return manifest
+end
+
-- the install directory of addons, e.g. ~/.xmake/addons
function addon.installdir()
return path.join(global.directory(), "addons")
@@ -63,10 +285,17 @@ end
-- 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
+-- it will be the directory of the caller script by default
+-- @return the addon name and its root directory, e.g. esp32, ~/.xmake/addons/esp32/v1.0.0
--
function addon.owner(scriptdir)
if not scriptdir then
+ -- we can get it from the sandbox of the caller script, e.g. the rule script of an addon
+ local sandbox = require("sandbox/sandbox")
+ local instance = sandbox.instance()
+ scriptdir = instance and instance:rootdir()
+ end
+ if not scriptdir then
return
end
scriptdir = path.absolute(scriptdir)
@@ -76,7 +305,11 @@ function addon.owner(scriptdir)
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]
+ local addondir = path.join(installdir, parts[1], parts[2])
+ -- the addon describes itself? the manifest is always authoritative,
+ -- the directory name is only its normalized form, e.g. "myns::foo" -> "myns_foo"
+ local manifest = addon.manifest(addondir)
+ return manifest and manifest.name or parts[1], addondir
end
return
end
@@ -84,9 +317,15 @@ function addon.owner(scriptdir)
-- 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
+ -- the addon describes itself? we get its name from the manifest
+ local manifest = addon.manifest(dir)
+ if manifest then
+ return manifest.name, dir
+ end
+ -- otherwise we can only guess it from the payload directories
+ for _, payloaddir in ipairs(addon._payloaddirs()) do
if os.isdir(path.join(dir, payloaddir)) then
- return dir, path.filename(dir)
+ return path.filename(dir), dir
end
end
local parentdir = path.directory(dir)
@@ -121,7 +360,7 @@ function addon.resolve_reference(reference, sep, kind, opt)
if name == "" then
return nil, nil, nil, string.format("invalid addon reference(%s)!", reference)
end
- local addondir, addonname = addon.owner(opt.scriptdir)
+ local addonname, addondir = 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
@@ -139,22 +378,13 @@ function addon.resolve_reference(reference, sep, kind, opt)
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)
+ 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,
--- so we do not need to scan the whole addons directory on startup
---
-function addon.registryfile()
- return path.join(addon.installdir(), "addons.conf")
-end
-
-- get all installed addons
--
-- @return the addons table, e.g. {["hello-world"] = {version = "latest", payloads = {"plugins"}}}
@@ -163,7 +393,7 @@ function addon.addons()
local addons = addon._ADDONS
if addons == nil then
addons = {}
- local registryfile = addon.registryfile()
+ local registryfile = addon._registryfile()
if os.isfile(registryfile) then
addons = io.load(registryfile) or {}
end
@@ -200,20 +430,6 @@ 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"
@@ -246,6 +462,17 @@ end
-- @return the payload root directory, it will be nil if no payload is found
--
function addon.payloadroot(sourcedir)
+
+ -- the addon can set its payload root directory explicitly, e.g. set_srcdir("src")
+ local manifest = addon.manifest(sourcedir)
+ if manifest and manifest.srcdir then
+ local srcdir = path.join(sourcedir, manifest.srcdir)
+ if #addon.payloads_of(srcdir) > 0 then
+ return srcdir
+ end
+ return
+ end
+
local srcdir = path.join(sourcedir, "src")
if #addon.payloads_of(srcdir) > 0 then
return srcdir
@@ -258,7 +485,7 @@ end
-- get the payload directories of the given addon directory, e.g. {"plugins", "rules"}
function addon.payloads_of(addondir)
local payloads = {}
- for _, payloaddir in ipairs(addon.payloaddirs()) do
+ for _, payloaddir in ipairs(addon._payloaddirs()) do
if os.isdir(path.join(addondir, payloaddir)) then
table.insert(payloads, payloaddir)
end
@@ -272,94 +499,31 @@ end
--
function addon.installscript()
return function (package)
- local payloadroot = addon.payloadroot(os.curdir())
+ local sourcedir = os.curdir()
+
+ -- the addon name is its identity, e.g. the install directory, the registry key
+ -- and the `@addon/<name>/xxx` references, so the package must be distributed with the same name
+ local manifest, errors = addon.manifest(sourcedir)
+ if errors then
+ os.raise(errors)
+ end
+ if manifest and addon.dirname(manifest.name) ~= addon.dirname(package:name()) then
+ os.raise("addon(%s) does not match the package name(%s) in the repository!\nplease fix the package recipe or the addon manifest.",
+ manifest.name, package:name())
+ end
+
+ local payloadroot = addon.payloadroot(sourcedir)
if not payloadroot then
os.raise("addon(%s): no payload directory found, e.g. plugins!", package:name())
end
for _, payloaddir in ipairs(addon.payloads_of(payloadroot)) do
os.cp(path.join(payloadroot, payloaddir), package:installdir())
end
- end
-end
--- save the given addons to the registry file
-function addon._save(addons)
- addon._ADDONS = addons
- local registryfile = addon.registryfile()
- -- we need not create an empty registry file if no addons are installed
- if next(addons) == nil and not os.isfile(registryfile) then
- return
- end
- local ok, errors = io.save(registryfile, addons)
- if not ok then
- utils.warning(errors)
- 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
+ -- we also install the manifest, so we can get the addon information after installing it
+ local manifestfile = addon._manifestfile(sourcedir)
+ if os.isfile(manifestfile) then
+ os.cp(manifestfile, package:installdir())
end
end
end
@@ -376,16 +540,43 @@ function addon.register(name, version, opt)
opt = opt or {}
local dirname = addon.dirname(name)
local addondir = path.join(addon.installdir(), dirname, version)
+
+ -- the installed addon describes itself? we prefer its own information
+ --
+ -- @note the deps are duplicated in the package recipe, the recipe is authoritative
+ -- because xmake needs them before downloading the addon sources, but we should
+ -- report the mismatch, they must be kept in sync
+ --
+ local description = opt.description
+ local deps = opt.deps
+ local manifest = addon.manifest(addondir)
+ if manifest then
+ description = manifest.description or description
+ if deps then
+ for _, dep in ipairs(manifest.deps) do
+ if not table.contains(deps, addon.dirname(dep)) then
+ utils.warning("addon(%s): dep(%s) is declared in its manifest, but not in the package recipe!", name, dep)
+ end
+ end
+ else
+ deps = {}
+ for _, dep in ipairs(manifest.deps) do
+ table.insert(deps, addon.dirname(dep))
+ end
+ deps = #deps > 0 and deps or nil
+ end
+ end
+
local addoninfo = {version = version,
- description = opt.description,
- deps = opt.deps,
+ description = description,
+ deps = deps,
payloads = addon.payloads_of(addondir),
- plugins = addon.plugins_of(addondir),
- templates = addon.templates_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)
+ local errors = addon._check_conflicts(dirname, addoninfo)
if errors then
return false, errors
end
@@ -396,22 +587,6 @@ function addon.register(name, version, opt)
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
--
-- @param name the addon name
@@ -427,7 +602,7 @@ function addon.remove(name, opt)
-- we cannot remove it if the other addons depend on it
if not opt.force then
- local parents = addon.parents(name)
+ 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, ", "))
@@ -450,20 +625,10 @@ function addon.remove(name, opt)
if not ok then
return false, errors
end
- addon.unregister(name)
+ addon._unregister(name)
return true
end
--- unregister the given addon
-function addon.unregister(name)
- local dirname = addon.dirname(name)
- local addons = addon.addons()
- if addons[dirname] then
- addons[dirname] = nil
- addon._save(addons)
- end
-end
-
-- rescan the install directory and rebuild the registry
--
-- it's only used to repair the registry file, e.g. the user removed some addon directories manually
@@ -476,7 +641,6 @@ function addon.rescan()
if #payloads > 0 then
local dirname = path.filename(path.directory(versiondir))
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, deps
if oldaddoninfo and oldaddoninfo.version == version then
@@ -484,8 +648,19 @@ function addon.rescan()
description = oldaddoninfo.description
deps = oldaddoninfo.deps
end
+ -- but we can always get them from the installed manifest
+ local manifest = addon.manifest(versiondir)
+ if manifest then
+ description = manifest.description or description
+ if #manifest.deps > 0 then
+ deps = {}
+ for _, dep in ipairs(manifest.deps) do
+ table.insert(deps, addon.dirname(dep))
+ end
+ end
+ end
addons[dirname] = {version = version, description = description, deps = deps, payloads = payloads,
- plugins = addon.plugins_of(versiondir), templates = addon.templates_of(versiondir)}
+ plugins = addon._plugins_of(versiondir), templates = addon._templates_of(versiondir)}
end
end
addon._save(addons)
@@ -499,6 +674,7 @@ function addon.clear()
os.rmdir(installdir)
end
addon._ADDONS = {}
+ addon._MANIFESTS = nil
end
-- return module
diff --git a/xmake/core/sandbox/modules/import/core/base/option.lua b/xmake/core/sandbox/modules/import/core/base/option.lua
index 983978917..e30c8ad9d 100644
--- a/xmake/core/sandbox/modules/import/core/base/option.lua
+++ b/xmake/core/sandbox/modules/import/core/base/option.lua
@@ -69,6 +69,10 @@ function sandbox_core_base_option.raw_parse(argv, options, opt)
end
-- parse arguments with the given options
+--
+-- @note we can also pass the extra options in the last argument,
+-- e.g. option.parse(argv, options, "the description", {allow_unknown = true})
+--
function sandbox_core_base_option.parse(argv, options, ...)
assert(argv and options)
@@ -77,8 +81,14 @@ function sandbox_core_base_option.parse(argv, options, ...)
table.insert(options, 2, {'h', "help", "k", nil, "Print this help message and exit." })
table.insert(options, 3, {})
- -- show help
+ -- get the descriptions and the extra options
local descriptions = {...}
+ local opt
+ if type(descriptions[#descriptions]) == "table" then
+ opt = table.join({populate_defaults = true}, table.remove(descriptions))
+ end
+
+ -- show help
local function show_help()
for _, description in ipairs(descriptions) do
print(description)
@@ -87,7 +97,7 @@ function sandbox_core_base_option.parse(argv, options, ...)
end
-- parse it
- local results, errors = option.parse(argv, options)
+ local results, errors = option.parse(argv, options, opt)
if not results then
show_help()
raise(errors)
diff --git a/xmake/core/sandbox/modules/import/core/package/addon.lua b/xmake/core/sandbox/modules/import/core/package/addon.lua
index ea4e0a7e8..c1c7f5b1d 100644
--- a/xmake/core/sandbox/modules/import/core/package/addon.lua
+++ b/xmake/core/sandbox/modules/import/core/package/addon.lua
@@ -30,19 +30,28 @@ sandbox_core_package_addon.installdir = addon.installdir
sandbox_core_package_addon.dirname = addon.dirname
sandbox_core_package_addon.owner = addon.owner
sandbox_core_package_addon.is_reference = addon.is_reference
-sandbox_core_package_addon.registryfile = addon.registryfile
-sandbox_core_package_addon.payloaddirs = addon.payloaddirs
sandbox_core_package_addon.payloads = addon.payloads
-sandbox_core_package_addon.payloaddir = addon.payloaddir
sandbox_core_package_addon.payloadinfos = addon.payloadinfos
sandbox_core_package_addon.payloads_of = addon.payloads_of
sandbox_core_package_addon.payloadroot = addon.payloadroot
sandbox_core_package_addon.addons = addon.addons
sandbox_core_package_addon.addondir = addon.addondir
-sandbox_core_package_addon.unregister = addon.unregister
sandbox_core_package_addon.rescan = addon.rescan
sandbox_core_package_addon.clear = addon.clear
+-- get the manifest of the given addon directory, e.g. <sourcedir>/addon.lua
+--
+-- @param sourcedir the addon source or install directory
+-- @return the manifest, it will be nil if this addon does not describe itself
+--
+function sandbox_core_package_addon.manifest(sourcedir)
+ local manifest, errors = addon.manifest(sourcedir)
+ if errors then
+ raise(errors)
+ end
+ return manifest
+end
+
-- resolve the given addon reference, e.g. `@addon/<addon>/<name>`, `@self/<name>`
--
-- @param reference the addon reference