diff options
| author | ruki <[email protected]> | 2026-08-11 00:46:53 +0800 |
|---|---|---|
| committer | ruki <[email protected]> | 2026-08-11 00:46:53 +0800 |
| commit | d699ee8188196a7e77bc1e2912f9b5dc6bd47416 (patch) | |
| tree | 6a56f694b067bca75f32da15ac853e17e226d563 | |
| parent | 06a00d75a9574bba124877432553e5cc61f0ba99 (diff) | |
add addon.lua
| -rw-r--r-- | tests/actions/addon/demo-addon-dep/addon.lua | 3 | ||||
| -rw-r--r-- | tests/actions/addon/demo-addon/addon.lua | 3 | ||||
| -rw-r--r-- | tests/actions/addon/demo-addon/src/rules/app/xmake.lua | 5 | ||||
| -rw-r--r-- | tests/actions/addon/test.lua | 21 | ||||
| -rw-r--r-- | xmake/actions/addon/main.lua | 22 | ||||
| -rw-r--r-- | xmake/core/package/addon.lua | 472 | ||||
| -rw-r--r-- | xmake/core/sandbox/modules/import/core/base/option.lua | 14 | ||||
| -rw-r--r-- | xmake/core/sandbox/modules/import/core/package/addon.lua | 17 |
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 |
