summaryrefslogtreecommitdiff
path: root/xmake/core/package/addon.lua
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 /xmake/core/package/addon.lua
parentcf62f410738a7489fe052d183bcf616ecf5f7067 (diff)
improve addon to import includes, rules and toolchains
Diffstat (limited to 'xmake/core/package/addon.lua')
-rw-r--r--xmake/core/package/addon.lua238
1 files changed, 231 insertions, 7 deletions
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)