From 73a790f3c4810755921350c61cf84d6504a30297 Mon Sep 17 00:00:00 2001 From: ruki Date: Sat, 8 Aug 2026 16:32:53 +0800 Subject: add addon support for plugins --- xmake/actions/require/xmake.lua | 4 +- xmake/core/base/task.lua | 27 +- xmake/core/package/package.lua | 61 ++++- .../modules/import/core/package/package.lua | 16 +- .../action/require/impl/install_packages.lua | 2 +- .../private/action/require/impl/package.lua | 23 +- .../private/action/require/impl/repository.lua | 2 +- xmake/modules/private/action/require/install.lua | 8 +- .../private/check/checkers/api/package/kind.lua | 4 +- xmake/modules/private/xrepo/action/install.lua | 7 +- xmake/plugins/addon/main.lua | 287 +++++++++++++++++++++ xmake/plugins/addon/xmake.lua | 43 +++ xmake/plugins/plugin/main.lua | 1 + xmake/plugins/plugin/xmake.lua | 2 +- 14 files changed, 459 insertions(+), 28 deletions(-) create mode 100644 xmake/plugins/addon/main.lua create mode 100644 xmake/plugins/addon/xmake.lua diff --git a/xmake/actions/require/xmake.lua b/xmake/actions/require/xmake.lua index 6b04f2621..b800c4528 100644 --- a/xmake/actions/require/xmake.lua +++ b/xmake/actions/require/xmake.lua @@ -39,7 +39,9 @@ task("require") {nil, "linkjobs", "kv", nil, "Set the number of parallel link jobs."}, {nil, "shallow", "k", nil, "Does not install or download dependent packages."}, {nil, "build", "k", nil, "Always build and install packages from source."}, - {nil, "plugin", "k", nil, "Install plugin packages from /plugins/."}, + {nil, "addon", "k", nil, "Install addon packages from /addons/."}, + {nil, "plugin", "k", nil, "Install plugin packages from /plugins/.", + "(deprecated, please use --addon instead)"}, {'l', "list", "k", nil, "List all package dependencies in project.", "e.g.", " $ xmake require --list"}, diff --git a/xmake/core/base/task.lua b/xmake/core/base/task.lua index d6f581cf9..2488665f5 100644 --- a/xmake/core/base/task.lua +++ b/xmake/core/base/task.lua @@ -81,13 +81,26 @@ end -- the directories of tasks function task._directories() - local dirs = { - path.join(global.directory(), "plugins"), - path.join(os.programdir(), "plugins"), - path.join(os.programdir(), "actions")} - local plugindirs = os.getenv("XMAKE_PLUGIN_DIRS") - if plugindirs then - table.insert(dirs, 1, plugindirs) + local dirs = task._DIRECTORIES + if dirs == nil then + dirs = { + path.join(global.directory(), "plugins"), + path.join(os.programdir(), "plugins"), + path.join(os.programdir(), "actions")} + + -- add the plugins of the installed addons, e.g. ~/.xmake/addons///plugins + local addonsdir = path.join(global.directory(), "addons") + if os.isdir(addonsdir) then + for _, plugindir in ipairs(os.dirs(path.join(addonsdir, "*", "*", "plugins"))) do + table.insert(dirs, plugindir) + end + end + + local plugindirs = os.getenv("XMAKE_PLUGIN_DIRS") + if plugindirs then + table.insert(dirs, 1, plugindirs) + end + task._DIRECTORIES = dirs end return dirs end diff --git a/xmake/core/package/package.lua b/xmake/core/package/package.lua index e014d9578..8f035b6b5 100644 --- a/xmake/core/package/package.lua +++ b/xmake/core/package/package.lua @@ -620,7 +620,19 @@ function _instance:is_toolchain() return self:kind() == "toolchain" end +-- is addon package? +-- +-- it will be installed to `~/.xmake/addons//` and +-- it can provide plugins, rules, toolchains, templates and modules for xmake +-- +function _instance:is_addon() + return self:kind() == "addon" +end + -- is plugin package? +-- +-- @note this kind is deprecated, please use the `addon` kind instead +-- function _instance:is_plugin() return self:kind() == "plugin" end @@ -922,7 +934,15 @@ function _instance:installdir(...) installdir = self:get("installdir") if not installdir then local name = self:name():lower():gsub("::", "_") - if self:is_plugin() then + if self:is_addon() then + -- e.g. ~/.xmake/addons// + local version_str = self:version_str() or "latest" + if os.is_host("windows") then + version_str = version_str:gsub("[>=<|%*]", "") + end + installdir = path.join(package.addon_installdir(), name, version_str) + elseif self:is_plugin() then + -- deprecated, @see the `addon` kind installdir = path.join(global.directory(), "plugins", name) else if self:is_local() then @@ -1195,7 +1215,10 @@ function _instance:_rawenvs() end -- add plugin env for on_test - if self:is_plugin() then + if self:is_addon() then + -- e.g. ~/.xmake/addons///plugins + envs.XMAKE_PLUGIN_DIRS = path.join(self:installdir(), "plugins") + elseif self:is_plugin() then envs.XMAKE_PLUGIN_DIRS = path.directory(self:installdir()) end self._RAWENVS = envs @@ -3030,6 +3053,21 @@ function package.installdir(opt) return installdir end +-- the install directory for addon packages, e.g. ~/.xmake/addons +function package.addon_installdir() + return path.join(global.directory(), "addons") +end + +-- the payload directories of addon packages +-- +-- an addon can provide any subset of them, e.g. only `plugins` +-- +-- @note only `plugins` is activated for now, the others are reserved +-- +function package.addon_payloaddirs() + return {"plugins", "rules", "toolchains", "platforms", "modules", "templates", "themes", "includes"} +end + -- the search directories function package.searchdirs() local searchdirs = global.get("pkg_searchdirs") @@ -3226,7 +3264,26 @@ function package.load_from_repository(packagename, packagedir, opt) return nil, string.format("%s: package(%s) not found!", scriptpath, packagename) end + -- we need set the default on_install script if it's addon package, + -- we only install the payload directories of this addon, e.g. plugins, rules, toolchains, ... + if packageinfo:get("kind") == "addon" and not packageinfo:get("install") then + local on_install = function (pkg) + local installed = false + for _, payloaddir in ipairs(package.addon_payloaddirs()) do + if os.isdir(payloaddir) then + os.cp(payloaddir, pkg:installdir()) + installed = true + end + end + if not installed then + os.raise("addon(%s): no payload directory found, e.g. plugins!", pkg:name()) + end + end + packageinfo:set("install", on_install) + end + -- we need set the default on_install script if it's plugin package + -- @note the plugin kind is deprecated, please use the addon kind instead if packageinfo:get("kind") == "plugin" and not packageinfo:get("install") then -- only one code line, we can directly omit the sandbox wrapper. local on_install = function (pkg) diff --git a/xmake/core/sandbox/modules/import/core/package/package.lua b/xmake/core/sandbox/modules/import/core/package/package.lua index e011bdff0..14729af65 100644 --- a/xmake/core/sandbox/modules/import/core/package/package.lua +++ b/xmake/core/sandbox/modules/import/core/package/package.lua @@ -27,13 +27,15 @@ local package = require("package/package") local raise = require("sandbox/modules/raise") -- inherit some builtin interfaces -sandbox_core_package_package.cachedir = package.cachedir -sandbox_core_package_package.installdir = package.installdir -sandbox_core_package_package.searchdirs = package.searchdirs -sandbox_core_package_package.targetplat = package.targetplat -sandbox_core_package_package.targetarch = package.targetarch -sandbox_core_package_package.apis = package.apis -sandbox_core_package_package.new = package.new +sandbox_core_package_package.cachedir = package.cachedir +sandbox_core_package_package.installdir = package.installdir +sandbox_core_package_package.addon_installdir = package.addon_installdir +sandbox_core_package_package.addon_payloaddirs = package.addon_payloaddirs +sandbox_core_package_package.searchdirs = package.searchdirs +sandbox_core_package_package.targetplat = package.targetplat +sandbox_core_package_package.targetarch = package.targetarch +sandbox_core_package_package.apis = package.apis +sandbox_core_package_package.new = package.new -- load the package from the project file function sandbox_core_package_package.load_from_project(packagename) diff --git a/xmake/modules/private/action/require/impl/install_packages.lua b/xmake/modules/private/action/require/impl/install_packages.lua index 9573b38f6..7ab247351 100644 --- a/xmake/modules/private/action/require/impl/install_packages.lua +++ b/xmake/modules/private/action/require/impl/install_packages.lua @@ -862,7 +862,7 @@ end -- - requires_extra: the extra require configs from `add_requires()`, indexed by the require string -- - nodeps: only install the given packages, do not install their dependent packages -- - system: load package from system if `true`, and never load it if `false` (only for non-3rd packages) --- - packagekind: the package kind, e.g. "plugin", it will be loaded from the `plugins` root directory of repositories +-- - packagekind: the package kind, e.g. "addon", it will be loaded from the `addons` root directory of repositories -- @note `toolchain` is reserved and it will be set internally, @see load_packages -- -- @return the installed packages, including the toolchain packages and all dependent packages diff --git a/xmake/modules/private/action/require/impl/package.lua b/xmake/modules/private/action/require/impl/package.lua index c3998c728..e57850673 100644 --- a/xmake/modules/private/action/require/impl/package.lua +++ b/xmake/modules/private/action/require/impl/package.lua @@ -198,7 +198,7 @@ end -- - plat: the given platform of this package -- - arch: the given architecture of this package -- - name: the given repository name, we will only find this package in the given repository --- - rootdir: the root directory of repositories, e.g. "packages" (default), "plugins" +-- - rootdir: the root directory of repositories, e.g. "packages" (default), "addons" -- - locked_repo: the locked repository info in `xmake-requires.lock`, e.g. {url = .., commit = .., branch = ..} -- function _load_package_from_repository(packagename, opt) @@ -209,6 +209,19 @@ function _load_package_from_repository(packagename, opt) end end +-- get the root directory of repositories for the given package kind +-- +-- e.g. "packages" (default), "addons", "plugins" (deprecated) +-- +function _get_repository_rootdir(packagekind) + if packagekind == "addon" then + return "addons" + elseif packagekind == "plugin" then + return "plugins" + end + return "packages" +end + -- load package package from base -- -- e.g. package("foo") set_base("bar") @@ -956,7 +969,7 @@ end -- @param opt the options -- - system: load package from system if `true`, and never load it if `false`, -- it's only used when `add_requires("zlib", {system = nil})` is not set (only for non-3rd packages) --- - packagekind: the package kind, e.g. "plugin", it will be loaded from the `plugins` root directory of repositories +-- - packagekind: the package kind, e.g. "addon", it will be loaded from the `addons` root directory of repositories -- - toolchain: only load toolchain packages, the non-toolchain toplevel packages will be ignored -- - requirepath: the current require path, e.g. "foo.bar", it's used to detect circular dependencies -- and match `add_requireconfs()` @@ -1014,7 +1027,7 @@ function _load_package(packagename, requireinfo, opt) plat = requireinfo.plat, arch = requireinfo.arch, name = requireinfo.reponame, - rootdir = opt.packagekind == "plugin" and "plugins" or "packages", + rootdir = _get_repository_rootdir(opt.packagekind), locked_repo = locked_requireinfo and locked_requireinfo.repo}) if package then from_repo = true @@ -1025,7 +1038,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 = opt.packagekind == "plugin" and "plugins" or "packages", + rootdir = _get_repository_rootdir(opt.packagekind), locked_repo = locked_requireinfo and locked_requireinfo.repo}) end @@ -1765,7 +1778,7 @@ end -- - requires_extra: the extra require configs from `add_requires()`, e.g. {["zlib >=1.2.11"] = {configs = {shared = true}}} -- - nodeps: only load the given packages, do not load their dependent packages -- - system: load package from system if `true`, and never load it if `false` (only for non-3rd packages) --- - packagekind: the package kind, e.g. "plugin", it will be loaded from the `plugins` root directory of repositories +-- - packagekind: the package kind, e.g. "addon", it will be loaded from the `addons` root directory of repositories -- - toolchain: only load toolchain packages and their dependent packages -- - requirepath: the parent require path, e.g. "foo.bar", it's used to detect circular dependencies and match `add_requireconfs()` -- - parentinfo: the parent requireinfo, the child package will inherit some builtin configs from it, e.g. runtimes, pic diff --git a/xmake/modules/private/action/require/impl/repository.lua b/xmake/modules/private/action/require/impl/repository.lua index 6b16a7183..d17bdb570 100644 --- a/xmake/modules/private/action/require/impl/repository.lua +++ b/xmake/modules/private/action/require/impl/repository.lua @@ -153,7 +153,7 @@ end -- get package directory from repositories -- -- @param packagename the package name --- @param opt {rootdir = "packages|plugins"} +-- @param opt {rootdir = "packages|addons|plugins"} function packagedir(packagename, opt) -- strip trailing ~tag, e.g. zlib~debug diff --git a/xmake/modules/private/action/require/install.lua b/xmake/modules/private/action/require/install.lua index 6adf0dfa2..ebc648124 100644 --- a/xmake/modules/private/action/require/install.lua +++ b/xmake/modules/private/action/require/install.lua @@ -82,7 +82,13 @@ function main(requires_raw) -- install packages environment.enter() - local packagekind = option.get("plugin") and "plugin" or "package" + -- @note the `--plugin` option is deprecated, please use `--addon` instead + local packagekind = "package" + if option.get("addon") then + packagekind = "addon" + elseif option.get("plugin") then + packagekind = "plugin" + end local packages = install_packages(requires, {packagekind = packagekind, requires_extra = requires_extra}) if packages then _check_missing_packages(packages) diff --git a/xmake/modules/private/check/checkers/api/package/kind.lua b/xmake/modules/private/check/checkers/api/package/kind.lua index d2c7afd10..2e4f2495b 100644 --- a/xmake/modules/private/check/checkers/api/package/kind.lua +++ b/xmake/modules/private/check/checkers/api/package/kind.lua @@ -37,6 +37,8 @@ function main(opt) end return true end - return value == "binary" or value == "toolchain" or value == "template" or value == "plugin" + -- @note the `plugin` kind is deprecated, please use the `addon` kind instead + return value == "binary" or value == "toolchain" or value == "template" or + value == "addon" or value == "plugin" end})) end diff --git a/xmake/modules/private/xrepo/action/install.lua b/xmake/modules/private/xrepo/action/install.lua index 9e9bb1086..4abb57c7f 100644 --- a/xmake/modules/private/xrepo/action/install.lua +++ b/xmake/modules/private/xrepo/action/install.lua @@ -47,7 +47,9 @@ function menu_options() "e.g.", " - xrepo install -p cross --toolchain=mytool --includes='toolchain1.lua" .. path.envsep() .. "toolchain2.lua'"}, {nil, "policies", "kv", nil, "Set the policies." }, - {nil, "plugin", "k", nil, "Install plugin packages from /plugins/"}, + {nil, "addon", "k", nil, "Install addon packages from /addons/"}, + {nil, "plugin", "k", nil, "Install plugin packages from /plugins/", + "(deprecated, please use --addon instead)"}, {category = "Visual Studio SDK Configuration" }, {nil, "vs", "kv", nil, "The Microsoft Visual Studio" , " e.g. --vs=2017" }, @@ -282,6 +284,9 @@ function _install_packages(packages) if option.get("build") or is_debug then table.insert(require_argv, "--build") end + if option.get("addon") then + table.insert(require_argv, "--addon") + end if option.get("plugin") then table.insert(require_argv, "--plugin") end diff --git a/xmake/plugins/addon/main.lua b/xmake/plugins/addon/main.lua new file mode 100644 index 000000000..829aa0438 --- /dev/null +++ b/xmake/plugins/addon/main.lua @@ -0,0 +1,287 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015-present, Xmake Open Source Community. +-- +-- @author ruki +-- @file main.lua +-- + +-- imports +import("core.base.option") +import("core.package.repository") +import("core.package.package", {alias = "core_package"}) +import("devel.git") +import("private.action.require.impl.environment") + +-- the version directory name for the addons installed from git urls or local directories +local LOCALVERSION = "latest" + +-- validate an addon directory name +function _check_addon_name(name) + assert(type(name) == "string" and name ~= "" and name ~= "." and not name:find("..", 1, true) and not name:find("[/\\:]"), "invalid addon name(%s)!", name) + return name +end + +-- get addon directory in ~/.xmake/addons +function _get_addondir(name, version) + local addondir = core_package.addon_installdir() + if name then + addondir = path.join(addondir, _check_addon_name(name)) + if version then + addondir = path.join(addondir, version) + end + end + return addondir +end + +-- get local and global repositories, with local taking precedence +function _repositories() + return table.join(repository.repositories({global = false}), repository.repositories({global = true})) +end + +-- get the payload directories of the given addon directory, e.g. {"plugins", "rules"} +function _get_payloads(dir) + local payloads = {} + for _, payloaddir in ipairs(core_package.addon_payloaddirs()) do + if os.isdir(path.join(dir, payloaddir)) then + table.insert(payloads, payloaddir) + end + end + return payloads +end + +-- install an addon from the given repository or the first repository containing it +function _install_from_repo(name, reponame) + + -- check addon name + _check_addon_name(name) + + -- do install + local installname = name + if reponame then + installname = reponame .. "@" .. name + end + local argv = {"lua", "private.xrepo", "install", "--addon"} + -- we need to pass the common options to the sub-process, e.g. -y, -v, -D + if option.get("yes") then + table.insert(argv, "-y") + end + if option.get("verbose") then + table.insert(argv, "-v") + end + if option.get("diagnosis") then + table.insert(argv, "-D") + end + table.insert(argv, installname) + os.execv(os.programfile(), argv) +end + +-- install a single addon from a source directory (as the given name, default to the directory name) +function _install_from_local(dir, name) + assert(os.isdir(dir), "addon path(%s) not found!", dir) + assert(#_get_payloads(dir) > 0, "addon path(%s): no payload directory found, e.g. ${bright}plugins${clear}!", dir) + name = name or path.filename(path.absolute(dir)) + local dstdir = _get_addondir(name, LOCALVERSION) + assert(not os.isdir(dstdir), "addon(%s) already exists!", name) + os.vcp(dir, dstdir) + cprint("${color.success}install ${bright}%s${clear} ok!", name) +end + +-- install a single addon from a git url or github shortcut, e.g. https://github.com/xmake-addons/serial-monitor +function _install_from_git(url) + local branch + if url:startswith("github:") then + local i = url:find("#", 1, true) + if i then + branch = url:sub(i + 1) + url = url:sub(1, i - 1) + end + url = git.asgiturl(url) + end + local name = (path.filename(url):gsub("%.git$", "")) + local tmpdir = os.tmpfile() .. ".dir" + git.clone(url, {verbose = option.get("verbose"), branch = branch, outputdir = tmpdir}) + os.tryrm(path.join(tmpdir, ".git")) + _install_from_local(tmpdir, name) + os.tryrm(tmpdir) +end + +-- install a single addon +function _install_one(name) + -- parse repo@addon format + local i = name:find("@", 1, true) + if i and not name:find("[/\\:]") then + local reponame = name:sub(1, i - 1) + local addonname = name:sub(i + 1) + _install_from_repo(addonname, reponame) + return + end + + -- github shortcut: github:user/repo or github:user/repo#branch + if name:startswith("github:") then + _install_from_git(name) + return + end + + -- git url or local path + if git.asgiturl(name) then + _install_from_git(name) + return + elseif os.isdir(name) then + _install_from_local(name) + return + end + + -- plain name: try to find it in repositories + _install_from_repo(name) +end + +-- install addons +function _install() + local names = assert(option.get("addons"), "please specify the addons to be installed!") + environment.enter() + for _, name in ipairs(names) do + _install_one(name) + end + environment.leave() +end + +-- remove the given installed addon +function _remove() + local names = assert(option.get("addons"), "please specify the addon name to be removed!") + assert(#names == 1, "please specify only one addon name to be removed!") + local name = names[1] + local dir = _get_addondir(name) + assert(os.isdir(dir), "addon(%s) not found!", name) + os.rmdir(dir) + cprint("${color.success}remove ${bright}%s${clear} ok!", name) +end + +-- get the description of an addon from its package description file +function _addon_description(dir) + local filepath = path.join(dir, "xmake.lua") + if os.isfile(filepath) then + local content = io.readfile(filepath) + if content then + return content:match("set_description%s*%(\"(.-)\"%)") + end + end +end + +-- collect the installed addons, e.g. ~/.xmake/addons// +function _collect_installed_addons() + local entries = {} + for _, versiondir in ipairs(os.dirs(path.join(_get_addondir(), "*", "*"))) do + local payloads = _get_payloads(versiondir) + if #payloads > 0 then + table.insert(entries, { + name = path.filename(path.directory(versiondir)), + version = path.filename(versiondir), + payloads = payloads}) + end + end + return entries +end + +-- collect the addons in the given repository, they follow the packages layout (addons//) +function _collect_repo_addons(root, seen) + local entries = {} + for _, dir in ipairs(os.dirs(path.join(root, "*", "*"))) do + local name = path.filename(dir) + if os.isfile(path.join(dir, "xmake.lua")) and not seen[name] then + seen[name] = true + table.insert(entries, {name = name, description = _addon_description(dir)}) + end + end + return entries +end + +-- print an addon entry with its description aligned on the right +function _print_addon(name, description, width, note) + local suffix = description or "" + if note then + suffix = suffix ~= "" and (suffix .. " " .. note) or note + end + if suffix ~= "" then + local padding = math.max(width - #name, 1) + cprint(" ${color.dump.string}%s${clear}%s%s", name, (" "):rep(padding), suffix) + else + cprint(" ${color.dump.string}%s${clear}", name) + end +end + +-- list all addons +function _list() + local seen = {} + local installed = _collect_installed_addons() + for _, entry in ipairs(installed) do + seen[entry.name] = true + end + local avail = {} + for _, repo in ipairs(_repositories()) do + table.join2(avail, _collect_repo_addons(path.join(repo:directory(), "addons"), seen)) + end + + -- compute the alignment width from all addon names + local width = 0 + for _, entries in ipairs({installed, avail}) do + for _, entry in ipairs(entries) do + width = math.max(width, #entry.name + 4) + end + end + + -- installed addons + cprint("${bright}the installed addons:${clear}") + if #installed > 0 then + for _, entry in ipairs(installed) do + local note = string.format("(%s, %s)", entry.version, table.concat(entry.payloads, ", ")) + _print_addon(entry.name, nil, width, note) + end + else + print(" (none)") + end + + -- addons available in repositories (not yet installed) + cprint("${bright}available in configured repositories:${clear}") + if #avail > 0 then + for _, entry in ipairs(avail) do + local note = string.format("(run xmake addon --install %s to install)", entry.name) + _print_addon(entry.name, entry.description, width, note) + end + else + print(" (none)") + end +end + +-- clear all installed addons +function _clear() + local addonsdir = _get_addondir() + if os.isdir(addonsdir) then + os.rmdir(addonsdir) + end + cprint("${color.success}clear all installed addons ok!") +end + +function main() + if option.get("install") then + _install() + elseif option.get("remove") then + _remove() + elseif option.get("list") then + _list() + elseif option.get("clear") then + _clear() + end +end diff --git a/xmake/plugins/addon/xmake.lua b/xmake/plugins/addon/xmake.lua new file mode 100644 index 000000000..885e35644 --- /dev/null +++ b/xmake/plugins/addon/xmake.lua @@ -0,0 +1,43 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015-present, Xmake Open Source Community. +-- +-- @author ruki +-- @file xmake.lua +-- + +task("addon") + set_category("plugin") + on_run("main") + set_menu { + usage = "xmake addon [options]", + description = "Manage addons of xmake.", + options = { + {'i', "install", "k", nil, "Install addons."}, + {'r', "remove", "k", nil, "Remove the given installed addon."}, + {'l', "list", "k", nil, "List all installed addons."}, + {'c', "clear", "k", nil, "Clear all installed addons."}, + {nil, "addons", "vs", nil, "The addon paths, urls or names.", + "e.g.", + " $ xmake addon --install https://github.com/myrepo/serial-monitor", + " $ xmake addon --install github:myrepo/serial-monitor", + " $ xmake addon --install github:myrepo/serial-monitor#dev", + " $ xmake addon --install /tmp/my-addon", + " $ xmake addon --install xmake-repo@serial-monitor", + " $ xmake addon --install serial-monitor", + " $ xmake addon --remove serial-monitor", + " $ xmake addon --list"} + } + } diff --git a/xmake/plugins/plugin/main.lua b/xmake/plugins/plugin/main.lua index facbc0b4c..dbb23a139 100644 --- a/xmake/plugins/plugin/main.lua +++ b/xmake/plugins/plugin/main.lua @@ -257,6 +257,7 @@ function _clear() end function main() + wprint("`xmake plugin` is deprecated, please use `xmake addon` instead!") if option.get("install") then _install() elseif option.get("remove") then diff --git a/xmake/plugins/plugin/xmake.lua b/xmake/plugins/plugin/xmake.lua index ae7869203..1136b0044 100644 --- a/xmake/plugins/plugin/xmake.lua +++ b/xmake/plugins/plugin/xmake.lua @@ -23,7 +23,7 @@ task("plugin") on_run("main") set_menu { usage = "xmake plugin [options]", - description = "Manage plugins of xmake.", + description = "Manage plugins of xmake. (deprecated, please use `xmake addon` instead)", options = { {'i', "install", "k", nil, "Install plugins."}, {'r', "remove", "k", nil, "Remove the given installed plugin."}, -- cgit v1.3.1 From 53075cd271268c1931a41648a92891c362f2483a Mon Sep 17 00:00:00 2001 From: ruki Date: Sat, 8 Aug 2026 18:31:55 +0800 Subject: add addon module --- xmake/core/base/task.lua | 12 +- xmake/core/package/addon.lua | 200 +++++++++++++++++++++ xmake/core/package/package.lua | 35 +--- .../sandbox/modules/import/core/package/addon.lua | 41 +++++ .../modules/import/core/package/package.lua | 16 +- .../action/require/impl/actions/install.lua | 6 + xmake/plugins/addon/main.lua | 43 ++--- 7 files changed, 279 insertions(+), 74 deletions(-) create mode 100644 xmake/core/package/addon.lua create mode 100644 xmake/core/sandbox/modules/import/core/package/addon.lua diff --git a/xmake/core/base/task.lua b/xmake/core/base/task.lua index 2488665f5..e8d2441b9 100644 --- a/xmake/core/base/task.lua +++ b/xmake/core/base/task.lua @@ -28,6 +28,7 @@ local string = require("base/string") local global = require("base/global") local hashset = require("base/hashset") local interpreter = require("base/interpreter") +local addon = require("package/addon") local sandbox = require("sandbox/sandbox") local config = require("project/config") local sandbox_os = require("sandbox/modules/os") @@ -89,12 +90,11 @@ function task._directories() path.join(os.programdir(), "actions")} -- add the plugins of the installed addons, e.g. ~/.xmake/addons///plugins - local addonsdir = path.join(global.directory(), "addons") - if os.isdir(addonsdir) then - for _, plugindir in ipairs(os.dirs(path.join(addonsdir, "*", "*", "plugins"))) do - table.insert(dirs, plugindir) - end - end + -- + -- we get them from the addons registry file directly, + -- so we do not need to scan the whole addons directory on startup + -- + table.join2(dirs, addon.payloads("plugins")) local plugindirs = os.getenv("XMAKE_PLUGIN_DIRS") if plugindirs then diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua new file mode 100644 index 000000000..e40a83a47 --- /dev/null +++ b/xmake/core/package/addon.lua @@ -0,0 +1,200 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015-present, Xmake Open Source Community. +-- +-- @author ruki +-- @file addon.lua +-- + +-- define module +local addon = addon or {} + +-- load modules +local os = require("base/os") +local io = require("base/io") +local path = require("base/path") +local table = require("base/table") +local utils = require("base/utils") +local global = require("base/global") + +-- the payload directories of an addon +-- +-- an addon can provide any subset of them, e.g. only `plugins` +-- +-- @note only `plugins` is activated for now, the others are reserved +-- +function addon.payloaddirs() + return {"plugins", "rules", "toolchains", "platforms", "modules", "templates", "themes", "includes"} +end + +-- the install directory of addons, e.g. ~/.xmake/addons +function addon.installdir() + return path.join(global.directory(), "addons") +end + +-- get the directory name of the given addon name, e.g. "myns::foo" -> "myns_foo" +function addon.dirname(name) + return (name:lower():gsub("::", "_")) +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"}}} +-- +function addon.addons() + local addons = addon._ADDONS + if addons == nil then + addons = {} + local registryfile = addon.registryfile() + if os.isfile(registryfile) then + addons = io.load(registryfile) or {} + end + addon._ADDONS = addons + end + return addons +end + +-- get the install directory of the given addon, e.g. ~/.xmake/addons// +function addon.addondir(name, version) + local dirname = addon.dirname(name) + if version == nil then + local addoninfo = addon.addons()[dirname] + if addoninfo == nil then + return nil + end + version = addoninfo.version + end + return path.join(addon.installdir(), dirname, version) +end + +-- get the payload directories of the given kind from all installed addons +-- +-- @param kind the payload kind, e.g. "plugins", "rules" +-- @return the directories, e.g. {"~/.xmake/addons/hello-world/latest/plugins"} +-- +-- @note we do not check if these directories exist, the callers will just ignore the invalid ones +-- +function addon.payloads(kind) + local payloads = {} + for name, addoninfo in pairs(addon.addons()) do + if table.contains(addoninfo.payloads or {}, kind) then + table.insert(payloads, path.join(addon.installdir(), name, addoninfo.version, kind)) + end + end + return payloads +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 + if os.isdir(path.join(addondir, payloaddir)) then + table.insert(payloads, payloaddir) + end + end + return payloads +end + +-- get the default on_install script of addon packages +-- +-- we only install the payload directories of this addon, e.g. plugins, rules, toolchains, ... +-- +function addon.installscript() + return function (package) + local installed = false + for _, payloaddir in ipairs(addon.payloaddirs()) do + if os.isdir(payloaddir) then + os.cp(payloaddir, package:installdir()) + installed = true + end + end + if not installed then + os.raise("addon(%s): no payload directory found, e.g. plugins!", package:name()) + 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 + +-- register the given installed addon +-- +-- @param name the addon name +-- @param version the addon version, e.g. "1.0.1", "latest" +-- +function addon.register(name, version) + local dirname = addon.dirname(name) + local addons = addon.addons() + addons[dirname] = {version = version, payloads = addon.payloads_of(path.join(addon.installdir(), dirname, version))} + addon._save(addons) +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 +-- +function addon.rescan() + local addons = {} + for _, versiondir in ipairs(os.dirs(path.join(addon.installdir(), "*", "*"))) do + local payloads = addon.payloads_of(versiondir) + if #payloads > 0 then + addons[path.filename(path.directory(versiondir))] = {version = path.filename(versiondir), payloads = payloads} + end + end + addon._save(addons) + return addons +end + +-- clear all installed addons +function addon.clear() + local installdir = addon.installdir() + if os.isdir(installdir) then + os.rmdir(installdir) + end + addon._ADDONS = {} +end + +-- return module +return addon diff --git a/xmake/core/package/package.lua b/xmake/core/package/package.lua index 8f035b6b5..2b56ee332 100644 --- a/xmake/core/package/package.lua +++ b/xmake/core/package/package.lua @@ -37,6 +37,7 @@ local interpreter = require("base/interpreter") local select_script = require("base/private/select_script") local is_cross = require("base/private/is_cross") local memcache = require("cache/memcache") +local addon = require("package/addon") local toolchain = require("tool/toolchain") local compiler = require("tool/compiler") local linker = require("tool/linker") @@ -940,7 +941,7 @@ function _instance:installdir(...) if os.is_host("windows") then version_str = version_str:gsub("[>=<|%*]", "") end - installdir = path.join(package.addon_installdir(), name, version_str) + installdir = addon.addondir(self:name(), version_str) elseif self:is_plugin() then -- deprecated, @see the `addon` kind installdir = path.join(global.directory(), "plugins", name) @@ -3053,21 +3054,6 @@ function package.installdir(opt) return installdir end --- the install directory for addon packages, e.g. ~/.xmake/addons -function package.addon_installdir() - return path.join(global.directory(), "addons") -end - --- the payload directories of addon packages --- --- an addon can provide any subset of them, e.g. only `plugins` --- --- @note only `plugins` is activated for now, the others are reserved --- -function package.addon_payloaddirs() - return {"plugins", "rules", "toolchains", "platforms", "modules", "templates", "themes", "includes"} -end - -- the search directories function package.searchdirs() local searchdirs = global.get("pkg_searchdirs") @@ -3264,22 +3250,9 @@ function package.load_from_repository(packagename, packagedir, opt) return nil, string.format("%s: package(%s) not found!", scriptpath, packagename) end - -- we need set the default on_install script if it's addon package, - -- we only install the payload directories of this addon, e.g. plugins, rules, toolchains, ... + -- we need set the default on_install script if it's addon package if packageinfo:get("kind") == "addon" and not packageinfo:get("install") then - local on_install = function (pkg) - local installed = false - for _, payloaddir in ipairs(package.addon_payloaddirs()) do - if os.isdir(payloaddir) then - os.cp(payloaddir, pkg:installdir()) - installed = true - end - end - if not installed then - os.raise("addon(%s): no payload directory found, e.g. plugins!", pkg:name()) - end - end - packageinfo:set("install", on_install) + packageinfo:set("install", addon.installscript()) end -- we need set the default on_install script if it's plugin package diff --git a/xmake/core/sandbox/modules/import/core/package/addon.lua b/xmake/core/sandbox/modules/import/core/package/addon.lua new file mode 100644 index 000000000..c1267a5d4 --- /dev/null +++ b/xmake/core/sandbox/modules/import/core/package/addon.lua @@ -0,0 +1,41 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015-present, Xmake Open Source Community. +-- +-- @author ruki +-- @file addon.lua +-- + +-- define module +local sandbox_core_package_addon = sandbox_core_package_addon or {} + +-- load modules +local addon = require("package/addon") + +-- inherit some builtin interfaces +sandbox_core_package_addon.installdir = addon.installdir +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.payloads_of = addon.payloads_of +sandbox_core_package_addon.addons = addon.addons +sandbox_core_package_addon.addondir = addon.addondir +sandbox_core_package_addon.register = addon.register +sandbox_core_package_addon.unregister = addon.unregister +sandbox_core_package_addon.rescan = addon.rescan +sandbox_core_package_addon.clear = addon.clear + +-- return module +return sandbox_core_package_addon diff --git a/xmake/core/sandbox/modules/import/core/package/package.lua b/xmake/core/sandbox/modules/import/core/package/package.lua index 14729af65..e011bdff0 100644 --- a/xmake/core/sandbox/modules/import/core/package/package.lua +++ b/xmake/core/sandbox/modules/import/core/package/package.lua @@ -27,15 +27,13 @@ local package = require("package/package") local raise = require("sandbox/modules/raise") -- inherit some builtin interfaces -sandbox_core_package_package.cachedir = package.cachedir -sandbox_core_package_package.installdir = package.installdir -sandbox_core_package_package.addon_installdir = package.addon_installdir -sandbox_core_package_package.addon_payloaddirs = package.addon_payloaddirs -sandbox_core_package_package.searchdirs = package.searchdirs -sandbox_core_package_package.targetplat = package.targetplat -sandbox_core_package_package.targetarch = package.targetarch -sandbox_core_package_package.apis = package.apis -sandbox_core_package_package.new = package.new +sandbox_core_package_package.cachedir = package.cachedir +sandbox_core_package_package.installdir = package.installdir +sandbox_core_package_package.searchdirs = package.searchdirs +sandbox_core_package_package.targetplat = package.targetplat +sandbox_core_package_package.targetarch = package.targetarch +sandbox_core_package_package.apis = package.apis +sandbox_core_package_package.new = package.new -- load the package from the project file function sandbox_core_package_package.load_from_project(packagename) diff --git a/xmake/modules/private/action/require/impl/actions/install.lua b/xmake/modules/private/action/require/impl/actions/install.lua index 7fb54a951..7e60b16b6 100644 --- a/xmake/modules/private/action/require/impl/actions/install.lua +++ b/xmake/modules/private/action/require/impl/actions/install.lua @@ -22,6 +22,7 @@ import("core.base.option") import("core.base.tty") import("core.package.package", {alias = "core_package"}) +import("core.package.addon") import("core.project.target") import("core.project.project") import("core.platform.platform") @@ -499,6 +500,11 @@ function main(package) -- save the package info to the manifest file package:manifest_save() + + -- 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") + end installed_now = true end end diff --git a/xmake/plugins/addon/main.lua b/xmake/plugins/addon/main.lua index 829aa0438..d072cec7f 100644 --- a/xmake/plugins/addon/main.lua +++ b/xmake/plugins/addon/main.lua @@ -20,8 +20,8 @@ -- imports import("core.base.option") +import("core.package.addon") import("core.package.repository") -import("core.package.package", {alias = "core_package"}) import("devel.git") import("private.action.require.impl.environment") @@ -36,9 +36,9 @@ end -- get addon directory in ~/.xmake/addons function _get_addondir(name, version) - local addondir = core_package.addon_installdir() + local addondir = addon.installdir() if name then - addondir = path.join(addondir, _check_addon_name(name)) + addondir = path.join(addondir, addon.dirname(_check_addon_name(name))) if version then addondir = path.join(addondir, version) end @@ -51,17 +51,6 @@ function _repositories() return table.join(repository.repositories({global = false}), repository.repositories({global = true})) end --- get the payload directories of the given addon directory, e.g. {"plugins", "rules"} -function _get_payloads(dir) - local payloads = {} - for _, payloaddir in ipairs(core_package.addon_payloaddirs()) do - if os.isdir(path.join(dir, payloaddir)) then - table.insert(payloads, payloaddir) - end - end - return payloads -end - -- install an addon from the given repository or the first repository containing it function _install_from_repo(name, reponame) @@ -91,11 +80,12 @@ end -- install a single addon from a source directory (as the given name, default to the directory name) function _install_from_local(dir, name) assert(os.isdir(dir), "addon path(%s) not found!", dir) - assert(#_get_payloads(dir) > 0, "addon path(%s): no payload directory found, e.g. ${bright}plugins${clear}!", dir) + assert(#addon.payloads_of(dir) > 0, "addon path(%s): no payload directory found, e.g. ${bright}plugins${clear}!", dir) name = name or path.filename(path.absolute(dir)) local dstdir = _get_addondir(name, LOCALVERSION) assert(not os.isdir(dstdir), "addon(%s) already exists!", name) os.vcp(dir, dstdir) + addon.register(name, LOCALVERSION) cprint("${color.success}install ${bright}%s${clear} ok!", name) end @@ -166,6 +156,7 @@ function _remove() local dir = _get_addondir(name) assert(os.isdir(dir), "addon(%s) not found!", name) os.rmdir(dir) + addon.unregister(name) cprint("${color.success}remove ${bright}%s${clear} ok!", name) end @@ -180,18 +171,17 @@ function _addon_description(dir) end end --- collect the installed addons, e.g. ~/.xmake/addons// +-- collect the installed addons from the addons registry +-- +-- we always rescan the install directory here to repair the registry, +-- e.g. the user may remove some addon directories manually +-- function _collect_installed_addons() local entries = {} - for _, versiondir in ipairs(os.dirs(path.join(_get_addondir(), "*", "*"))) do - local payloads = _get_payloads(versiondir) - if #payloads > 0 then - table.insert(entries, { - name = path.filename(path.directory(versiondir)), - version = path.filename(versiondir), - payloads = payloads}) - end + for name, addoninfo in pairs(addon.rescan()) do + table.insert(entries, {name = name, version = addoninfo.version, payloads = addoninfo.payloads}) end + table.sort(entries, function (a, b) return a.name < b.name end) return entries end @@ -267,10 +257,7 @@ end -- clear all installed addons function _clear() - local addonsdir = _get_addondir() - if os.isdir(addonsdir) then - os.rmdir(addonsdir) - end + addon.clear() cprint("${color.success}clear all installed addons ok!") end -- cgit v1.3.1 From 6a024e442cf6e3a1fe55ab63b3385be592bb1e1e Mon Sep 17 00:00:00 2001 From: ruki Date: Sat, 8 Aug 2026 18:34:44 +0800 Subject: move addon as action --- xmake/actions/addon/main.lua | 274 ++++++++++++++++++++++++++++++++++++++++++ xmake/actions/addon/xmake.lua | 43 +++++++ xmake/plugins/addon/main.lua | 274 ------------------------------------------ xmake/plugins/addon/xmake.lua | 43 ------- 4 files changed, 317 insertions(+), 317 deletions(-) create mode 100644 xmake/actions/addon/main.lua create mode 100644 xmake/actions/addon/xmake.lua delete mode 100644 xmake/plugins/addon/main.lua delete mode 100644 xmake/plugins/addon/xmake.lua diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua new file mode 100644 index 000000000..d072cec7f --- /dev/null +++ b/xmake/actions/addon/main.lua @@ -0,0 +1,274 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015-present, Xmake Open Source Community. +-- +-- @author ruki +-- @file main.lua +-- + +-- imports +import("core.base.option") +import("core.package.addon") +import("core.package.repository") +import("devel.git") +import("private.action.require.impl.environment") + +-- the version directory name for the addons installed from git urls or local directories +local LOCALVERSION = "latest" + +-- validate an addon directory name +function _check_addon_name(name) + assert(type(name) == "string" and name ~= "" and name ~= "." and not name:find("..", 1, true) and not name:find("[/\\:]"), "invalid addon name(%s)!", name) + return name +end + +-- get addon directory in ~/.xmake/addons +function _get_addondir(name, version) + local addondir = addon.installdir() + if name then + addondir = path.join(addondir, addon.dirname(_check_addon_name(name))) + if version then + addondir = path.join(addondir, version) + end + end + return addondir +end + +-- get local and global repositories, with local taking precedence +function _repositories() + return table.join(repository.repositories({global = false}), repository.repositories({global = true})) +end + +-- install an addon from the given repository or the first repository containing it +function _install_from_repo(name, reponame) + + -- check addon name + _check_addon_name(name) + + -- do install + local installname = name + if reponame then + installname = reponame .. "@" .. name + end + local argv = {"lua", "private.xrepo", "install", "--addon"} + -- we need to pass the common options to the sub-process, e.g. -y, -v, -D + if option.get("yes") then + table.insert(argv, "-y") + end + if option.get("verbose") then + table.insert(argv, "-v") + end + if option.get("diagnosis") then + table.insert(argv, "-D") + end + table.insert(argv, installname) + os.execv(os.programfile(), argv) +end + +-- install a single addon from a source directory (as the given name, default to the directory name) +function _install_from_local(dir, name) + assert(os.isdir(dir), "addon path(%s) not found!", dir) + assert(#addon.payloads_of(dir) > 0, "addon path(%s): no payload directory found, e.g. ${bright}plugins${clear}!", dir) + name = name or path.filename(path.absolute(dir)) + local dstdir = _get_addondir(name, LOCALVERSION) + assert(not os.isdir(dstdir), "addon(%s) already exists!", name) + os.vcp(dir, dstdir) + addon.register(name, LOCALVERSION) + cprint("${color.success}install ${bright}%s${clear} ok!", name) +end + +-- install a single addon from a git url or github shortcut, e.g. https://github.com/xmake-addons/serial-monitor +function _install_from_git(url) + local branch + if url:startswith("github:") then + local i = url:find("#", 1, true) + if i then + branch = url:sub(i + 1) + url = url:sub(1, i - 1) + end + url = git.asgiturl(url) + end + local name = (path.filename(url):gsub("%.git$", "")) + local tmpdir = os.tmpfile() .. ".dir" + git.clone(url, {verbose = option.get("verbose"), branch = branch, outputdir = tmpdir}) + os.tryrm(path.join(tmpdir, ".git")) + _install_from_local(tmpdir, name) + os.tryrm(tmpdir) +end + +-- install a single addon +function _install_one(name) + -- parse repo@addon format + local i = name:find("@", 1, true) + if i and not name:find("[/\\:]") then + local reponame = name:sub(1, i - 1) + local addonname = name:sub(i + 1) + _install_from_repo(addonname, reponame) + return + end + + -- github shortcut: github:user/repo or github:user/repo#branch + if name:startswith("github:") then + _install_from_git(name) + return + end + + -- git url or local path + if git.asgiturl(name) then + _install_from_git(name) + return + elseif os.isdir(name) then + _install_from_local(name) + return + end + + -- plain name: try to find it in repositories + _install_from_repo(name) +end + +-- install addons +function _install() + local names = assert(option.get("addons"), "please specify the addons to be installed!") + environment.enter() + for _, name in ipairs(names) do + _install_one(name) + end + environment.leave() +end + +-- remove the given installed addon +function _remove() + local names = assert(option.get("addons"), "please specify the addon name to be removed!") + assert(#names == 1, "please specify only one addon name to be removed!") + local name = names[1] + local dir = _get_addondir(name) + assert(os.isdir(dir), "addon(%s) not found!", name) + os.rmdir(dir) + addon.unregister(name) + cprint("${color.success}remove ${bright}%s${clear} ok!", name) +end + +-- get the description of an addon from its package description file +function _addon_description(dir) + local filepath = path.join(dir, "xmake.lua") + if os.isfile(filepath) then + local content = io.readfile(filepath) + if content then + return content:match("set_description%s*%(\"(.-)\"%)") + end + end +end + +-- collect the installed addons from the addons registry +-- +-- we always rescan the install directory here to repair the registry, +-- e.g. the user may remove some addon directories manually +-- +function _collect_installed_addons() + local entries = {} + for name, addoninfo in pairs(addon.rescan()) do + table.insert(entries, {name = name, version = addoninfo.version, payloads = addoninfo.payloads}) + end + table.sort(entries, function (a, b) return a.name < b.name end) + return entries +end + +-- collect the addons in the given repository, they follow the packages layout (addons//) +function _collect_repo_addons(root, seen) + local entries = {} + for _, dir in ipairs(os.dirs(path.join(root, "*", "*"))) do + local name = path.filename(dir) + if os.isfile(path.join(dir, "xmake.lua")) and not seen[name] then + seen[name] = true + table.insert(entries, {name = name, description = _addon_description(dir)}) + end + end + return entries +end + +-- print an addon entry with its description aligned on the right +function _print_addon(name, description, width, note) + local suffix = description or "" + if note then + suffix = suffix ~= "" and (suffix .. " " .. note) or note + end + if suffix ~= "" then + local padding = math.max(width - #name, 1) + cprint(" ${color.dump.string}%s${clear}%s%s", name, (" "):rep(padding), suffix) + else + cprint(" ${color.dump.string}%s${clear}", name) + end +end + +-- list all addons +function _list() + local seen = {} + local installed = _collect_installed_addons() + for _, entry in ipairs(installed) do + seen[entry.name] = true + end + local avail = {} + for _, repo in ipairs(_repositories()) do + table.join2(avail, _collect_repo_addons(path.join(repo:directory(), "addons"), seen)) + end + + -- compute the alignment width from all addon names + local width = 0 + for _, entries in ipairs({installed, avail}) do + for _, entry in ipairs(entries) do + width = math.max(width, #entry.name + 4) + end + end + + -- installed addons + cprint("${bright}the installed addons:${clear}") + if #installed > 0 then + for _, entry in ipairs(installed) do + local note = string.format("(%s, %s)", entry.version, table.concat(entry.payloads, ", ")) + _print_addon(entry.name, nil, width, note) + end + else + print(" (none)") + end + + -- addons available in repositories (not yet installed) + cprint("${bright}available in configured repositories:${clear}") + if #avail > 0 then + for _, entry in ipairs(avail) do + local note = string.format("(run xmake addon --install %s to install)", entry.name) + _print_addon(entry.name, entry.description, width, note) + end + else + print(" (none)") + end +end + +-- clear all installed addons +function _clear() + addon.clear() + cprint("${color.success}clear all installed addons ok!") +end + +function main() + if option.get("install") then + _install() + elseif option.get("remove") then + _remove() + elseif option.get("list") then + _list() + elseif option.get("clear") then + _clear() + end +end diff --git a/xmake/actions/addon/xmake.lua b/xmake/actions/addon/xmake.lua new file mode 100644 index 000000000..8394c110b --- /dev/null +++ b/xmake/actions/addon/xmake.lua @@ -0,0 +1,43 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015-present, Xmake Open Source Community. +-- +-- @author ruki +-- @file xmake.lua +-- + +task("addon") + set_category("action") + on_run("main") + set_menu { + usage = "xmake addon [options]", + description = "Manage addons of xmake.", + options = { + {'i', "install", "k", nil, "Install addons."}, + {'r', "remove", "k", nil, "Remove the given installed addon."}, + {'l', "list", "k", nil, "List all installed addons."}, + {'c', "clear", "k", nil, "Clear all installed addons."}, + {nil, "addons", "vs", nil, "The addon paths, urls or names.", + "e.g.", + " $ xmake addon --install https://github.com/myrepo/serial-monitor", + " $ xmake addon --install github:myrepo/serial-monitor", + " $ xmake addon --install github:myrepo/serial-monitor#dev", + " $ xmake addon --install /tmp/my-addon", + " $ xmake addon --install xmake-repo@serial-monitor", + " $ xmake addon --install serial-monitor", + " $ xmake addon --remove serial-monitor", + " $ xmake addon --list"} + } + } diff --git a/xmake/plugins/addon/main.lua b/xmake/plugins/addon/main.lua deleted file mode 100644 index d072cec7f..000000000 --- a/xmake/plugins/addon/main.lua +++ /dev/null @@ -1,274 +0,0 @@ ---!A cross-platform build utility based on Lua --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- --- Copyright (C) 2015-present, Xmake Open Source Community. --- --- @author ruki --- @file main.lua --- - --- imports -import("core.base.option") -import("core.package.addon") -import("core.package.repository") -import("devel.git") -import("private.action.require.impl.environment") - --- the version directory name for the addons installed from git urls or local directories -local LOCALVERSION = "latest" - --- validate an addon directory name -function _check_addon_name(name) - assert(type(name) == "string" and name ~= "" and name ~= "." and not name:find("..", 1, true) and not name:find("[/\\:]"), "invalid addon name(%s)!", name) - return name -end - --- get addon directory in ~/.xmake/addons -function _get_addondir(name, version) - local addondir = addon.installdir() - if name then - addondir = path.join(addondir, addon.dirname(_check_addon_name(name))) - if version then - addondir = path.join(addondir, version) - end - end - return addondir -end - --- get local and global repositories, with local taking precedence -function _repositories() - return table.join(repository.repositories({global = false}), repository.repositories({global = true})) -end - --- install an addon from the given repository or the first repository containing it -function _install_from_repo(name, reponame) - - -- check addon name - _check_addon_name(name) - - -- do install - local installname = name - if reponame then - installname = reponame .. "@" .. name - end - local argv = {"lua", "private.xrepo", "install", "--addon"} - -- we need to pass the common options to the sub-process, e.g. -y, -v, -D - if option.get("yes") then - table.insert(argv, "-y") - end - if option.get("verbose") then - table.insert(argv, "-v") - end - if option.get("diagnosis") then - table.insert(argv, "-D") - end - table.insert(argv, installname) - os.execv(os.programfile(), argv) -end - --- install a single addon from a source directory (as the given name, default to the directory name) -function _install_from_local(dir, name) - assert(os.isdir(dir), "addon path(%s) not found!", dir) - assert(#addon.payloads_of(dir) > 0, "addon path(%s): no payload directory found, e.g. ${bright}plugins${clear}!", dir) - name = name or path.filename(path.absolute(dir)) - local dstdir = _get_addondir(name, LOCALVERSION) - assert(not os.isdir(dstdir), "addon(%s) already exists!", name) - os.vcp(dir, dstdir) - addon.register(name, LOCALVERSION) - cprint("${color.success}install ${bright}%s${clear} ok!", name) -end - --- install a single addon from a git url or github shortcut, e.g. https://github.com/xmake-addons/serial-monitor -function _install_from_git(url) - local branch - if url:startswith("github:") then - local i = url:find("#", 1, true) - if i then - branch = url:sub(i + 1) - url = url:sub(1, i - 1) - end - url = git.asgiturl(url) - end - local name = (path.filename(url):gsub("%.git$", "")) - local tmpdir = os.tmpfile() .. ".dir" - git.clone(url, {verbose = option.get("verbose"), branch = branch, outputdir = tmpdir}) - os.tryrm(path.join(tmpdir, ".git")) - _install_from_local(tmpdir, name) - os.tryrm(tmpdir) -end - --- install a single addon -function _install_one(name) - -- parse repo@addon format - local i = name:find("@", 1, true) - if i and not name:find("[/\\:]") then - local reponame = name:sub(1, i - 1) - local addonname = name:sub(i + 1) - _install_from_repo(addonname, reponame) - return - end - - -- github shortcut: github:user/repo or github:user/repo#branch - if name:startswith("github:") then - _install_from_git(name) - return - end - - -- git url or local path - if git.asgiturl(name) then - _install_from_git(name) - return - elseif os.isdir(name) then - _install_from_local(name) - return - end - - -- plain name: try to find it in repositories - _install_from_repo(name) -end - --- install addons -function _install() - local names = assert(option.get("addons"), "please specify the addons to be installed!") - environment.enter() - for _, name in ipairs(names) do - _install_one(name) - end - environment.leave() -end - --- remove the given installed addon -function _remove() - local names = assert(option.get("addons"), "please specify the addon name to be removed!") - assert(#names == 1, "please specify only one addon name to be removed!") - local name = names[1] - local dir = _get_addondir(name) - assert(os.isdir(dir), "addon(%s) not found!", name) - os.rmdir(dir) - addon.unregister(name) - cprint("${color.success}remove ${bright}%s${clear} ok!", name) -end - --- get the description of an addon from its package description file -function _addon_description(dir) - local filepath = path.join(dir, "xmake.lua") - if os.isfile(filepath) then - local content = io.readfile(filepath) - if content then - return content:match("set_description%s*%(\"(.-)\"%)") - end - end -end - --- collect the installed addons from the addons registry --- --- we always rescan the install directory here to repair the registry, --- e.g. the user may remove some addon directories manually --- -function _collect_installed_addons() - local entries = {} - for name, addoninfo in pairs(addon.rescan()) do - table.insert(entries, {name = name, version = addoninfo.version, payloads = addoninfo.payloads}) - end - table.sort(entries, function (a, b) return a.name < b.name end) - return entries -end - --- collect the addons in the given repository, they follow the packages layout (addons//) -function _collect_repo_addons(root, seen) - local entries = {} - for _, dir in ipairs(os.dirs(path.join(root, "*", "*"))) do - local name = path.filename(dir) - if os.isfile(path.join(dir, "xmake.lua")) and not seen[name] then - seen[name] = true - table.insert(entries, {name = name, description = _addon_description(dir)}) - end - end - return entries -end - --- print an addon entry with its description aligned on the right -function _print_addon(name, description, width, note) - local suffix = description or "" - if note then - suffix = suffix ~= "" and (suffix .. " " .. note) or note - end - if suffix ~= "" then - local padding = math.max(width - #name, 1) - cprint(" ${color.dump.string}%s${clear}%s%s", name, (" "):rep(padding), suffix) - else - cprint(" ${color.dump.string}%s${clear}", name) - end -end - --- list all addons -function _list() - local seen = {} - local installed = _collect_installed_addons() - for _, entry in ipairs(installed) do - seen[entry.name] = true - end - local avail = {} - for _, repo in ipairs(_repositories()) do - table.join2(avail, _collect_repo_addons(path.join(repo:directory(), "addons"), seen)) - end - - -- compute the alignment width from all addon names - local width = 0 - for _, entries in ipairs({installed, avail}) do - for _, entry in ipairs(entries) do - width = math.max(width, #entry.name + 4) - end - end - - -- installed addons - cprint("${bright}the installed addons:${clear}") - if #installed > 0 then - for _, entry in ipairs(installed) do - local note = string.format("(%s, %s)", entry.version, table.concat(entry.payloads, ", ")) - _print_addon(entry.name, nil, width, note) - end - else - print(" (none)") - end - - -- addons available in repositories (not yet installed) - cprint("${bright}available in configured repositories:${clear}") - if #avail > 0 then - for _, entry in ipairs(avail) do - local note = string.format("(run xmake addon --install %s to install)", entry.name) - _print_addon(entry.name, entry.description, width, note) - end - else - print(" (none)") - end -end - --- clear all installed addons -function _clear() - addon.clear() - cprint("${color.success}clear all installed addons ok!") -end - -function main() - if option.get("install") then - _install() - elseif option.get("remove") then - _remove() - elseif option.get("list") then - _list() - elseif option.get("clear") then - _clear() - end -end diff --git a/xmake/plugins/addon/xmake.lua b/xmake/plugins/addon/xmake.lua deleted file mode 100644 index 885e35644..000000000 --- a/xmake/plugins/addon/xmake.lua +++ /dev/null @@ -1,43 +0,0 @@ ---!A cross-platform build utility based on Lua --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- --- Copyright (C) 2015-present, Xmake Open Source Community. --- --- @author ruki --- @file xmake.lua --- - -task("addon") - set_category("plugin") - on_run("main") - set_menu { - usage = "xmake addon [options]", - description = "Manage addons of xmake.", - options = { - {'i', "install", "k", nil, "Install addons."}, - {'r', "remove", "k", nil, "Remove the given installed addon."}, - {'l', "list", "k", nil, "List all installed addons."}, - {'c', "clear", "k", nil, "Clear all installed addons."}, - {nil, "addons", "vs", nil, "The addon paths, urls or names.", - "e.g.", - " $ xmake addon --install https://github.com/myrepo/serial-monitor", - " $ xmake addon --install github:myrepo/serial-monitor", - " $ xmake addon --install github:myrepo/serial-monitor#dev", - " $ xmake addon --install /tmp/my-addon", - " $ xmake addon --install xmake-repo@serial-monitor", - " $ xmake addon --install serial-monitor", - " $ xmake addon --remove serial-monitor", - " $ xmake addon --list"} - } - } -- cgit v1.3.1 From 4fc05750f6da0568df7a0fa836e114128d3ba849 Mon Sep 17 00:00:00 2001 From: ruki Date: Sat, 8 Aug 2026 22:16:02 +0800 Subject: rewrite template dist --- xmake/actions/create/main.lua | 30 ++++++++-------- xmake/actions/create/template.lua | 40 +++++++++++++++++++--- xmake/core/package/addon.lua | 21 ++++++++++-- .../sandbox/modules/import/core/package/addon.lua | 2 ++ 4 files changed, 71 insertions(+), 22 deletions(-) diff --git a/xmake/actions/create/main.lua b/xmake/actions/create/main.lua index 89a4791a6..476fd1325 100644 --- a/xmake/actions/create/main.lua +++ b/xmake/actions/create/main.lua @@ -79,26 +79,15 @@ function _list_templates(lang_filter) languages = {lang_filter} end - -- map a template directory to its root meta (repo/global/builtin) - local rootinfo_of = function (dir) - if not dir then - return - end - dir = path.absolute(dir) - for _, info in ipairs(rootinfos) do - local rootdir = path.absolute(info.dir) - if dir == rootdir or dir:startswith(rootdir .. path.sep()) then - return info - end - end - end + -- map a template directory to its root meta (repo/addon/global/builtin) + local rootinfo_of = template.rootinfo_of local sourcekey_of = function (info) if not info then return "unknown" end - if info.kind == "repo" then - return "repo:" .. info.name + if info.kind == "repo" or info.kind == "addon" then + return info.kind .. ":" .. info.name end return info.kind or "unknown" end @@ -142,7 +131,10 @@ function _list_templates(lang_filter) local info = group.info if info and info.kind == "repo" then local branch = info.branch and (" " .. info.branch) or "" - cprint("${bright}%s${reset}: %s%s", info.name, info.url or "", branch) + local deprecated = info.deprecated and " ${yellow}(deprecated)${clear}" or "" + cprint("${bright}%s${reset}: %s%s%s", info.name, info.url or "", branch, deprecated) + elseif info and info.kind == "addon" then + cprint("${bright}%s${reset}: addon %s", info.name, info.version or "") else cprint("${bright}%s${reset}", (info and info.kind) or "unknown") end @@ -198,6 +190,12 @@ function _create_project(lang, templateid, targetname) raise("template(%s/%s): not found!\nyou can try:\n - xrepo update-repo (update repositories)\n - xmake create --list (show available templates)", lang, templateid) end + -- the templates in repositories are deprecated, they should be distributed as addons + local rootinfo = template.rootinfo_of(sourcedir) + if rootinfo and rootinfo.deprecated then + wprint("the templates in <%s>/templates are deprecated, please install them as an addon instead, e.g. xmake addon --install basic-templates", rootinfo.name or "repo") + end + -- get the builtin variables local builtinvars = template.builtinvars(targetname) diff --git a/xmake/actions/create/template.lua b/xmake/actions/create/template.lua index e18015514..778028ce8 100644 --- a/xmake/actions/create/template.lua +++ b/xmake/actions/create/template.lua @@ -22,6 +22,7 @@ import("core.base.global") import("core.base.hashset") import("core.language.language") +import("core.package.addon") import("core.package.repository") -- some builtin template variables in xmake.lua @@ -33,19 +34,32 @@ end -- get all template roots with extra meta information -- -- priority: --- 1. repo: /templates --- 2. global: /templates --- 3. builtin: /templates +-- 1. addon: /addons///templates +-- 2. repo: /templates (deprecated) +-- 3. global: /templates +-- 4. builtin: /templates function rootinfos() local results = {} + -- get template directories from the installed addons + for _, addoninfo in ipairs(addon.payloadinfos("templates")) do + if os.isdir(addoninfo.dir) then + table.insert(results, {kind = "addon", name = addoninfo.name, version = addoninfo.version, dir = addoninfo.dir}) + end + end + -- get template directories from global repositories + -- + -- @note it's deprecated, please distribute the templates as an addon instead, + -- e.g. xmake addon --install basic-templates + -- local repos = repository.repositories({global = true, network = false}) if repos then for _, repo in ipairs(repos) do local templatesdir = path.join(repo:directory(), "templates") if os.isdir(templatesdir) then - table.insert(results, {kind = "repo", name = repo:name(), url = repo:url(), branch = repo:branch(), dir = templatesdir}) + table.insert(results, {kind = "repo", name = repo:name(), url = repo:url(), branch = repo:branch(), + dir = templatesdir, deprecated = true}) end end end @@ -62,6 +76,24 @@ function rootinfos() return results end +-- get the root information of the given template directory +-- +-- @param templatedir the template directory, e.g. /templates/c/console +-- @return the root information, @see rootinfos +-- +function rootinfo_of(templatedir) + if not templatedir then + return + end + templatedir = path.absolute(templatedir) + for _, info in ipairs(rootinfos()) do + local rootdir = path.absolute(info.dir) + if templatedir == rootdir or templatedir:startswith(rootdir .. path.sep()) then + return info + end + end +end + -- get template root directories function rootdirs() local results = {} diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua index e40a83a47..35966853d 100644 --- a/xmake/core/package/addon.lua +++ b/xmake/core/package/addon.lua @@ -97,12 +97,29 @@ end -- function addon.payloads(kind) local payloads = {} + for _, payloadinfo in ipairs(addon.payloadinfos(kind)) do + table.insert(payloads, payloadinfo.dir) + end + return payloads +end + +-- get the payload information of the given kind from all installed addons +-- +-- @param kind the payload kind, e.g. "plugins", "rules" +-- @return the payload infos, e.g. {{name = "hello-world", version = "latest", dir = "~/.xmake/addons/hello-world/latest/plugins"}} +-- +function addon.payloadinfos(kind) + local payloadinfos = {} for name, addoninfo in pairs(addon.addons()) do if table.contains(addoninfo.payloads or {}, kind) then - table.insert(payloads, path.join(addon.installdir(), name, addoninfo.version, kind)) + table.insert(payloadinfos, { + name = name, + version = addoninfo.version, + dir = path.join(addon.installdir(), name, addoninfo.version, kind)}) end end - return payloads + table.sort(payloadinfos, function (a, b) return a.name < b.name end) + return payloadinfos end -- get the payload directories of the given addon directory, e.g. {"plugins", "rules"} diff --git a/xmake/core/sandbox/modules/import/core/package/addon.lua b/xmake/core/sandbox/modules/import/core/package/addon.lua index c1267a5d4..ecbd95f2b 100644 --- a/xmake/core/sandbox/modules/import/core/package/addon.lua +++ b/xmake/core/sandbox/modules/import/core/package/addon.lua @@ -26,9 +26,11 @@ local addon = require("package/addon") -- inherit some builtin interfaces sandbox_core_package_addon.installdir = addon.installdir +sandbox_core_package_addon.dirname = addon.dirname 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.payloadinfos = addon.payloadinfos sandbox_core_package_addon.payloads_of = addon.payloads_of sandbox_core_package_addon.addons = addon.addons sandbox_core_package_addon.addondir = addon.addondir -- cgit v1.3.1 From 8564ea66201d1398cb8ee5b91efc784a379c4a64 Mon Sep 17 00:00:00 2001 From: ruki Date: Sat, 8 Aug 2026 22:41:30 +0800 Subject: fix plugin bug and add description --- tests/actions/addon/test.lua | 167 +++++++++++++++++++++ tests/plugins/repository/test.lua | 118 --------------- xmake/actions/addon/main.lua | 5 +- xmake/core/package/addon.lua | 19 ++- xmake/core/package/package.lua | 9 +- .../action/require/impl/actions/install.lua | 3 +- 6 files changed, 193 insertions(+), 128 deletions(-) create mode 100644 tests/actions/addon/test.lua delete mode 100644 tests/plugins/repository/test.lua diff --git a/tests/actions/addon/test.lua b/tests/actions/addon/test.lua new file mode 100644 index 000000000..17a5dc33c --- /dev/null +++ b/tests/actions/addon/test.lua @@ -0,0 +1,167 @@ +import("core.base.global") + +-- write a minimal plugin that prints its name when run +-- +-- the plugins of an addon are placed in its `plugins` payload directory, +-- e.g. /plugins//xmake.lua +function _write_plugin(dir, name) + io.writefile(path.join(dir, "xmake.lua"), string.format([[ +task("%s") + set_category("plugin") + 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)) +end + +-- write a minimal template into the `templates` payload directory of an addon, +-- e.g. /templates///xmake.lua +function _write_template(dir, lang, templateid) + local templatedir = path.join(dir, "templates", lang, templateid) + io.writefile(path.join(templatedir, "xmake.lua"), [[ +target("${TARGET_NAME}") + set_kind("binary") + add_files("src/*.c") +]]) + io.writefile(path.join(templatedir, "src", "main.c"), [[ +int main(int argc, char** argv) { return 0; } +]]) +end + +-- write an addon payload directory, it provides a plugin and a template +function _write_addon(dir, name) + _write_plugin(path.join(dir, "plugins", name), name) + _write_template(dir, "c", 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. /addons///xmake.lua +function _write_addon_package(dir, name) + 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)) + _write_addon(path.join(dir, "src"), name) +end + +-- create a temporary addon repository (packages layout: addons//) and register it +-- +-- @return reponame, names, cleanup +function _mock_repo(basenames) + 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) + end + + -- register the repository into the cache + local cachefile = path.join(global.cachedir(), "repository") + local cache = os.isfile(cachefile) and io.load(cachefile) or {} + cache.repositories = cache.repositories or {} + cache.repositories[reponame] = {repodir} + io.save(cachefile, cache) + + local function cleanup() + for _, name in ipairs(names) do + 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 + cache.repositories[reponame] = nil + end + io.save(cachefile, cache) + os.tryrm(repodir) + end + return reponame, names, cleanup +end + +-- install an addon from a repository, by plain name and by repo@name +function test_install_from_repo(t) + local reponame, names, cleanup = _mock_repo({"hello"}) + local name = names[1] + + -- install by plain name (searched across all repositories) + os.runv("xmake", {"addon", "--install", "-y", name}) + t:require(os.iorunv("xmake", {name}):find(name, 1, true)) + + -- reinstall by repo@name + os.runv("xmake", {"addon", "--remove", name}) + os.runv("xmake", {"addon", "--install", "-y", reponame .. "@" .. name}) + t:require(os.iorunv("xmake", {name}):find(name, 1, true)) + + os.runv("xmake", {"addon", "--remove", name}) + cleanup() +end + +-- the templates of an installed addon can be used by `xmake create` +function test_install_templates(t) + local _, names, cleanup = _mock_repo({"hello"}) + local name = names[1] + os.runv("xmake", {"addon", "--install", "-y", name}) + + -- this template should be listed and grouped by its addon name + local out = os.iorunv("xmake", {"create", "--list"}) + t:require(out:find(name, 1, true)) + + -- we can create a new project from it + local projectdir = os.tmpfile() .. ".addon-project" + os.tryrm(projectdir) + 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}) + cleanup() +end + +-- install an addon from a local directory, then remove it +function test_install_from_local(t) + local suffix = path.filename(os.tmpfile()):gsub("[^%w]", "") + local name = "hello-local-" .. suffix + local dir = path.join(os.tmpfile() .. ".addon-local", name) + _write_addon(dir, name) + + 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)) +end + +-- --list shows the installed and available addons +function test_list(t) + local _, names, cleanup = _mock_repo({"hello", "world"}) + + -- install the first addon, leave the second only available + os.runv("xmake", {"addon", "--install", "-y", names[1]}) + local out = os.iorunv("xmake", {"addon", "--list"}) + t:require(out:find("the installed addons:", 1, true)) + t:require(out:find(names[1], 1, true)) + t:require(out:find(names[2], 1, true)) + t:require(out:find("xmake addon --install " .. names[2], 1, true)) + + -- the payloads of the installed addon should be shown, e.g. (latest, plugins, templates) + t:require(out:find("plugins", 1, true)) + t:require(out:find("templates", 1, true)) + + os.runv("xmake", {"addon", "--remove", names[1]}) + cleanup() +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 }) + t:require_not(try { function () os.runv("xmake", {"addon", "--install", "-y", "somerepo@.."}); return true end }) +end diff --git a/tests/plugins/repository/test.lua b/tests/plugins/repository/test.lua deleted file mode 100644 index 7b1619d5e..000000000 --- a/tests/plugins/repository/test.lua +++ /dev/null @@ -1,118 +0,0 @@ -import("core.base.global") - --- write a minimal plugin that prints its name when run -function _write_plugin(dir, name) - io.writefile(path.join(dir, "xmake.lua"), string.format([[ -task("%s") - set_category("plugin") - 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)) -end - --- write a plugin package description, the plugin sources are placed in its `src` directory --- --- plugins in a repository are described as packages, e.g. /plugins///xmake.lua -function _write_plugin_package(dir, name) - io.writefile(path.join(dir, "xmake.lua"), string.format([[ -package("%s") - set_kind("plugin") - set_description("say hello from %s") - set_sourcedir(path.join(os.scriptdir(), "src")) -]], name, name)) - _write_plugin(path.join(dir, "src"), name) -end - --- create a temporary plugin repository (packages layout: plugins//) and register it --- --- @return reponame, names, cleanup -function _mock_repo(basenames) - local suffix = path.filename(os.tmpfile()):gsub("[^%w]", "") - local reponame = "plugin-test-repo-" .. suffix - local repodir = os.tmpfile() .. ".plugin-repo" - local names = {} - for _, base in ipairs(basenames) do - local name = base .. "-" .. suffix - _write_plugin_package(path.join(repodir, "plugins", name:sub(1, 1), name), name) - table.insert(names, name) - end - - -- register the repository into the cache - local cachefile = path.join(global.cachedir(), "repository") - local cache = os.isfile(cachefile) and io.load(cachefile) or {} - cache.repositories = cache.repositories or {} - cache.repositories[reponame] = {repodir} - io.save(cachefile, cache) - - local function cleanup() - for _, name in ipairs(names) do - os.tryrm(path.join(global.directory(), "plugins", name)) - end - local cache = os.isfile(cachefile) and io.load(cachefile) or {} - if cache.repositories then - cache.repositories[reponame] = nil - end - io.save(cachefile, cache) - os.tryrm(repodir) - end - return reponame, names, cleanup -end - --- install a plugin from a repository, by plain name and by repo@name -function test_install_from_repo(t) - local reponame, names, cleanup = _mock_repo({"hello"}) - local name = names[1] - - -- install by plain name (searched across all repositories) - os.runv("xmake", {"plugin", "--install", "-y", name}) - t:require(os.iorunv("xmake", {name}):find(name, 1, true)) - - -- reinstall by repo@name - os.runv("xmake", {"plugin", "--remove", name}) - os.runv("xmake", {"plugin", "--install", "-y", reponame .. "@" .. name}) - t:require(os.iorunv("xmake", {name}):find(name, 1, true)) - - os.runv("xmake", {"plugin", "--remove", name}) - cleanup() -end - --- install a plugin from a local directory, then remove it -function test_install_from_local(t) - local suffix = path.filename(os.tmpfile()):gsub("[^%w]", "") - local name = "hello-local-" .. suffix - local dir = path.join(os.tmpfile() .. ".plugin-local", name) - _write_plugin(dir, name) - - os.runv("xmake", {"plugin", "--install", dir}) - t:require(os.iorunv("xmake", {name}):find(name, 1, true)) - - -- the removed plugin should no longer be runnable - os.runv("xmake", {"plugin", "--remove", name}) - t:require_not(try { function () os.runv("xmake", {name}); return true end }) - - os.tryrm(path.directory(dir)) -end - --- --list shows the built-in, installed and available plugins -function test_list(t) - local reponame, names, cleanup = _mock_repo({"hello", "world"}) - - -- install the first plugin, leave the second only available - os.runv("xmake", {"plugin", "--install", "-y", names[1]}) - local out = os.iorunv("xmake", {"plugin", "--list"}) - t:require(out:find("the built-in plugins:", 1, true)) - t:require(out:find("project", 1, true)) - t:require(out:find(names[1], 1, true)) - t:require(out:find(names[2], 1, true)) - t:require(out:find("xmake plugin --install " .. names[2], 1, true)) - - os.runv("xmake", {"plugin", "--remove", names[1]}) - cleanup() -end - --- invalid installs should fail -function test_install_invalid(t) - t:require_not(try { function () os.runv("xmake", {"plugin", "--install", "-y", "plugin-test-missing"}); return true end }) - t:require_not(try { function () os.runv("xmake", {"plugin", "--install", "-y", "somerepo@.."}); return true end }) -end diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua index d072cec7f..52cb2376b 100644 --- a/xmake/actions/addon/main.lua +++ b/xmake/actions/addon/main.lua @@ -179,7 +179,8 @@ end function _collect_installed_addons() local entries = {} for name, addoninfo in pairs(addon.rescan()) do - table.insert(entries, {name = name, version = addoninfo.version, payloads = addoninfo.payloads}) + table.insert(entries, {name = name, version = addoninfo.version, + description = addoninfo.description, payloads = addoninfo.payloads}) end table.sort(entries, function (a, b) return a.name < b.name end) return entries @@ -237,7 +238,7 @@ function _list() if #installed > 0 then for _, entry in ipairs(installed) do local note = string.format("(%s, %s)", entry.version, table.concat(entry.payloads, ", ")) - _print_addon(entry.name, nil, width, note) + _print_addon(entry.name, entry.description, width, note) end else print(" (none)") diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua index 35966853d..d433bdf8a 100644 --- a/xmake/core/package/addon.lua +++ b/xmake/core/package/addon.lua @@ -170,11 +170,15 @@ end -- -- @param name the addon name -- @param version the addon version, e.g. "1.0.1", "latest" +-- @param opt the options, e.g. {description = "..."} -- -function addon.register(name, version) +function addon.register(name, version, opt) + opt = opt or {} local dirname = addon.dirname(name) local addons = addon.addons() - addons[dirname] = {version = version, payloads = addon.payloads_of(path.join(addon.installdir(), dirname, version))} + addons[dirname] = {version = version, + description = opt.description, + payloads = addon.payloads_of(path.join(addon.installdir(), dirname, version))} addon._save(addons) end @@ -193,11 +197,20 @@ end -- it's only used to repair the registry file, e.g. the user removed some addon directories manually -- function addon.rescan() + local oldaddons = addon.addons() local addons = {} for _, versiondir in ipairs(os.dirs(path.join(addon.installdir(), "*", "*"))) do local payloads = addon.payloads_of(versiondir) if #payloads > 0 then - addons[path.filename(path.directory(versiondir))] = {version = path.filename(versiondir), payloads = payloads} + 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 + if oldaddoninfo and oldaddoninfo.version == version then + description = oldaddoninfo.description + end + addons[dirname] = {version = version, description = description, payloads = payloads} end end addon._save(addons) diff --git a/xmake/core/package/package.lua b/xmake/core/package/package.lua index 2b56ee332..9d0cdc49d 100644 --- a/xmake/core/package/package.lua +++ b/xmake/core/package/package.lua @@ -1216,10 +1216,11 @@ function _instance:_rawenvs() end -- add plugin env for on_test - if self:is_addon() then - -- e.g. ~/.xmake/addons///plugins - envs.XMAKE_PLUGIN_DIRS = path.join(self:installdir(), "plugins") - elseif self:is_plugin() then + -- + -- @note we need not do it for the addon packages, they are registered + -- to the addons registry after installing, and xmake can find their payloads directly + -- + if self:is_plugin() then envs.XMAKE_PLUGIN_DIRS = path.directory(self:installdir()) end self._RAWENVS = envs diff --git a/xmake/modules/private/action/require/impl/actions/install.lua b/xmake/modules/private/action/require/impl/actions/install.lua index 7e60b16b6..40ca225f0 100644 --- a/xmake/modules/private/action/require/impl/actions/install.lua +++ b/xmake/modules/private/action/require/impl/actions/install.lua @@ -503,7 +503,8 @@ 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") + addon.register(package:name(), package:version_str() or "latest", + {description = package:description()}) end installed_now = true end -- cgit v1.3.1 From cf62f410738a7489fe052d183bcf616ecf5f7067 Mon Sep 17 00:00:00 2001 From: ruki Date: Sat, 8 Aug 2026 23:17:52 +0800 Subject: add addon search --- tests/actions/addon/test.lua | 126 ++++++++++++------- xmake/actions/addon/main.lua | 137 +++++++++------------ xmake/actions/addon/xmake.lua | 4 +- xmake/core/package/addon.lua | 19 +++ .../sandbox/modules/import/core/package/addon.lua | 1 + .../package/manager/xmake/search_package.lua | 4 +- xmake/modules/private/action/require/search.lua | 8 +- xmake/modules/private/xrepo/action/remove.lua | 18 ++- xmake/modules/private/xrepo/action/search.lua | 6 + xmake/modules/private/xrepo/quick_search/cache.lua | 68 ++++++---- 10 files changed, 236 insertions(+), 155 deletions(-) diff --git a/tests/actions/addon/test.lua b/tests/actions/addon/test.lua index 17a5dc33c..d393c0545 100644 --- a/tests/actions/addon/test.lua +++ b/tests/actions/addon/test.lua @@ -68,6 +68,9 @@ function _mock_repo(basenames) cache.repositories[reponame] = {repodir} io.save(cachefile, cache) + -- we need to clear the quick search cache, it will be rebuilt on the next search + os.tryrm(path.join(global.cachedir(), "quick_search")) + local function cleanup() for _, name in ipairs(names) do os.tryrm(path.join(global.directory(), "addons", name)) @@ -78,49 +81,65 @@ function _mock_repo(basenames) cache.repositories[reponame] = nil end io.save(cachefile, cache) + os.tryrm(path.join(global.cachedir(), "quick_search")) os.tryrm(repodir) end return reponame, names, cleanup 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) + try + { + function () + func(reponame, names) + end, + finally + { + cleanup + } + } +end + -- install an addon from a repository, by plain name and by repo@name function test_install_from_repo(t) - local reponame, names, cleanup = _mock_repo({"hello"}) - local name = names[1] + _with_repo({"hello"}, function (reponame, names) + local name = names[1] - -- install by plain name (searched across all repositories) - os.runv("xmake", {"addon", "--install", "-y", name}) - t:require(os.iorunv("xmake", {name}):find(name, 1, true)) + -- install by plain name (searched across all repositories) + os.runv("xmake", {"addon", "--install", "-y", name}) + t:require(os.iorunv("xmake", {name}):find(name, 1, true)) - -- reinstall by repo@name - os.runv("xmake", {"addon", "--remove", name}) - os.runv("xmake", {"addon", "--install", "-y", reponame .. "@" .. name}) - t:require(os.iorunv("xmake", {name}):find(name, 1, true)) + -- reinstall by repo@name + os.runv("xmake", {"addon", "--remove", name}) + os.runv("xmake", {"addon", "--install", "-y", reponame .. "@" .. name}) + t:require(os.iorunv("xmake", {name}):find(name, 1, true)) - os.runv("xmake", {"addon", "--remove", name}) - cleanup() + 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) - local _, names, cleanup = _mock_repo({"hello"}) - local name = names[1] - os.runv("xmake", {"addon", "--install", "-y", name}) - - -- this template should be listed and grouped by its addon name - local out = os.iorunv("xmake", {"create", "--list"}) - t:require(out:find(name, 1, true)) - - -- we can create a new project from it - local projectdir = os.tmpfile() .. ".addon-project" - os.tryrm(projectdir) - 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}) - cleanup() + _with_repo({"hello"}, function (_, names) + local name = names[1] + os.runv("xmake", {"addon", "--install", "-y", name}) + + -- this template should be listed and grouped by its addon name + local out = os.iorunv("xmake", {"create", "--list"}) + t:require(out:find(name, 1, true)) + + -- we can create a new project from it + local projectdir = os.tmpfile() .. ".addon-project" + os.tryrm(projectdir) + 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 -- install an addon from a local directory, then remove it @@ -140,24 +159,39 @@ function test_install_from_local(t) os.tryrm(path.directory(dir)) end --- --list shows the installed and available addons +-- --list shows the installed addons and their payloads function test_list(t) - local _, names, cleanup = _mock_repo({"hello", "world"}) - - -- install the first addon, leave the second only available - os.runv("xmake", {"addon", "--install", "-y", names[1]}) - local out = os.iorunv("xmake", {"addon", "--list"}) - t:require(out:find("the installed addons:", 1, true)) - t:require(out:find(names[1], 1, true)) - t:require(out:find(names[2], 1, true)) - t:require(out:find("xmake addon --install " .. names[2], 1, true)) - - -- the payloads of the installed addon should be shown, e.g. (latest, plugins, templates) - t:require(out:find("plugins", 1, true)) - t:require(out:find("templates", 1, true)) - - os.runv("xmake", {"addon", "--remove", names[1]}) - cleanup() + _with_repo({"hello", "world"}, function (_, names) + + -- install the first addon, leave the second only available + os.runv("xmake", {"addon", "--install", "-y", names[1]}) + local out = os.iorunv("xmake", {"addon", "--list"}) + 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) + t:require(out:find("plugins", 1, true)) + t:require(out:find("templates", 1, true)) + + -- the addon which is not installed should be shown in the available addons + t:require(out:find("the available addons:", 1, true)) + t:require(out:find(names[2], 1, true)) + + os.runv("xmake", {"addon", "--remove", names[1]}) + end) +end + +-- --search finds the addons in the repositories, it reuses `xrepo search --addon` +function test_search(t) + _with_repo({"hello"}, function (_, names) + local name = names[1] + local out = os.iorunv("xmake", {"addon", "--search", name}) + t:require(out:find(name, 1, true)) + + -- the addons should not be found by the package search + local packages_out = os.iorunv("xrepo", {"search", name}) + t:require_not(packages_out:find(name, 1, true)) + end) end -- invalid installs should fail diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua index 52cb2376b..f3da68d8e 100644 --- a/xmake/actions/addon/main.lua +++ b/xmake/actions/addon/main.lua @@ -21,13 +21,16 @@ -- imports import("core.base.option") import("core.package.addon") -import("core.package.repository") import("devel.git") import("private.action.require.impl.environment") +import("private.action.require.impl.search_packages") -- the version directory name for the addons installed from git urls or local directories local LOCALVERSION = "latest" +-- the maximum number of the available addons shown by `--list` +local LISTLIMIT = 10 + -- validate an addon directory name function _check_addon_name(name) assert(type(name) == "string" and name ~= "" and name ~= "." and not name:find("..", 1, true) and not name:find("[/\\:]"), "invalid addon name(%s)!", name) @@ -46,23 +49,9 @@ function _get_addondir(name, version) return addondir end --- get local and global repositories, with local taking precedence -function _repositories() - return table.join(repository.repositories({global = false}), repository.repositories({global = true})) -end - --- install an addon from the given repository or the first repository containing it -function _install_from_repo(name, reponame) - - -- check addon name - _check_addon_name(name) - - -- do install - local installname = name - if reponame then - installname = reponame .. "@" .. name - end - local argv = {"lua", "private.xrepo", "install", "--addon"} +-- run the given xrepo action for the addons, e.g. install, remove, search +function _xrepo(action, names) + local argv = {"lua", "private.xrepo", action, "--addon"} -- we need to pass the common options to the sub-process, e.g. -y, -v, -D if option.get("yes") then table.insert(argv, "-y") @@ -73,10 +62,16 @@ function _install_from_repo(name, reponame) if option.get("diagnosis") then table.insert(argv, "-D") end - table.insert(argv, installname) + table.join2(argv, names) os.execv(os.programfile(), argv) end +-- install an addon from the given repository or the first repository containing it +function _install_from_repo(name, reponame) + _check_addon_name(name) + _xrepo("install", {reponame and (reponame .. "@" .. name) or name}) +end + -- install a single addon from a source directory (as the given name, default to the directory name) function _install_from_local(dir, name) assert(os.isdir(dir), "addon path(%s) not found!", dir) @@ -148,27 +143,16 @@ function _install() environment.leave() end --- remove the given installed addon +-- remove the given installed addons function _remove() local names = assert(option.get("addons"), "please specify the addon name to be removed!") - assert(#names == 1, "please specify only one addon name to be removed!") - local name = names[1] - local dir = _get_addondir(name) - assert(os.isdir(dir), "addon(%s) not found!", name) - os.rmdir(dir) - addon.unregister(name) - cprint("${color.success}remove ${bright}%s${clear} ok!", name) + _xrepo("remove", names) end --- get the description of an addon from its package description file -function _addon_description(dir) - local filepath = path.join(dir, "xmake.lua") - if os.isfile(filepath) then - local content = io.readfile(filepath) - if content then - return content:match("set_description%s*%(\"(.-)\"%)") - end - end +-- search the addons from the repositories +function _search() + local patterns = assert(option.get("addons"), "please specify the addon name pattern to be searched!") + _xrepo("search", patterns) end -- collect the installed addons from the addons registry @@ -186,70 +170,57 @@ function _collect_installed_addons() return entries end --- collect the addons in the given repository, they follow the packages layout (addons//) -function _collect_repo_addons(root, seen) - local entries = {} - for _, dir in ipairs(os.dirs(path.join(root, "*", "*"))) do - local name = path.filename(dir) - if os.isfile(path.join(dir, "xmake.lua")) and not seen[name] then - seen[name] = true - table.insert(entries, {name = name, description = _addon_description(dir)}) - end +-- print an addon entry, e.g. -> serial-monitor v1.0.1: monitor the serial port output (in xmake-repo) +function _print_addon(entry, suffix) + local title = entry.name + if entry.version then + title = title .. " " .. entry.version end - return entries + local description = entry.description and (": " .. entry.description) or "" + cprint(" ${color.dump.reference}->${clear} ${color.dump.string}%s${clear}%s%s", title, description, suffix or "") end --- print an addon entry with its description aligned on the right -function _print_addon(name, description, width, note) - local suffix = description or "" - if note then - suffix = suffix ~= "" and (suffix .. " " .. note) or note - end - if suffix ~= "" then - local padding = math.max(width - #name, 1) - cprint(" ${color.dump.string}%s${clear}%s%s", name, (" "):rep(padding), suffix) - else - cprint(" ${color.dump.string}%s${clear}", name) +-- get the addons in the repositories, we reuse the packages search here +function _collect_repo_addons(exclude) + local entries = {} + for _, results in pairs(search_packages({"*"}, {kind = "addon", description = false})) do + for _, result in ipairs(results) do + if not exclude[result.name] then + table.insert(entries, result) + end + end end + table.sort(entries, function (a, b) return a.name < b.name end) + return entries end -- list all addons function _list() - local seen = {} - local installed = _collect_installed_addons() - for _, entry in ipairs(installed) do - seen[entry.name] = true - end - local avail = {} - for _, repo in ipairs(_repositories()) do - table.join2(avail, _collect_repo_addons(path.join(repo:directory(), "addons"), seen)) - end - -- compute the alignment width from all addon names - local width = 0 - for _, entries in ipairs({installed, avail}) do - for _, entry in ipairs(entries) do - width = math.max(width, #entry.name + 4) - end - end - - -- installed addons + -- show the installed addons + local installed = _collect_installed_addons() + local exclude = {} cprint("${bright}the installed addons:${clear}") if #installed > 0 then for _, entry in ipairs(installed) do - local note = string.format("(%s, %s)", entry.version, table.concat(entry.payloads, ", ")) - _print_addon(entry.name, entry.description, width, note) + exclude[entry.name] = true + _print_addon(entry, string.format(" ${dim}(%s)${clear}", table.concat(entry.payloads, ", "))) end else print(" (none)") end - -- addons available in repositories (not yet installed) - cprint("${bright}available in configured repositories:${clear}") + -- show the addons in the repositories, we only show the first ones if there are too many + local avail = _collect_repo_addons(exclude) + cprint("${bright}the available addons:${clear} ${dim}(run `xmake addon --install ` to install," .. + " `--search ` to search)${clear}") if #avail > 0 then - for _, entry in ipairs(avail) do - local note = string.format("(run xmake addon --install %s to install)", entry.name) - _print_addon(entry.name, entry.description, width, note) + for idx, entry in ipairs(avail) do + if idx > LISTLIMIT then + cprint(" ${dim}... and %d more${clear}", #avail - LISTLIMIT) + break + end + _print_addon(entry, entry.reponame and string.format(" ${dim}(in %s)${clear}", entry.reponame) or nil) end else print(" (none)") @@ -269,6 +240,8 @@ function main() _remove() elseif option.get("list") then _list() + elseif option.get("search") then + _search() elseif option.get("clear") then _clear() end diff --git a/xmake/actions/addon/xmake.lua b/xmake/actions/addon/xmake.lua index 8394c110b..240fef5bf 100644 --- a/xmake/actions/addon/xmake.lua +++ b/xmake/actions/addon/xmake.lua @@ -26,7 +26,8 @@ task("addon") description = "Manage addons of xmake.", options = { {'i', "install", "k", nil, "Install addons."}, - {'r', "remove", "k", nil, "Remove the given installed addon."}, + {'r', "remove", "k", nil, "Remove the given installed addons."}, + {'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."}, {nil, "addons", "vs", nil, "The addon paths, urls or names.", @@ -38,6 +39,7 @@ task("addon") " $ xmake addon --install xmake-repo@serial-monitor", " $ xmake addon --install serial-monitor", " $ xmake addon --remove serial-monitor", + " $ xmake addon --search serial", " $ xmake addon --list"} } } diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua index d433bdf8a..78830d266 100644 --- a/xmake/core/package/addon.lua +++ b/xmake/core/package/addon.lua @@ -182,6 +182,25 @@ function addon.register(name, version, opt) addon._save(addons) end +-- remove the given installed addon +-- +-- @param name the addon name +-- @return true or false and errors +-- +function addon.remove(name) + 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 + local ok, errors = os.rm(installdir) + if not ok then + return false, errors + end + addon.unregister(name) + return true +end + -- unregister the given addon function addon.unregister(name) local dirname = addon.dirname(name) diff --git a/xmake/core/sandbox/modules/import/core/package/addon.lua b/xmake/core/sandbox/modules/import/core/package/addon.lua index ecbd95f2b..2885bd45f 100644 --- a/xmake/core/sandbox/modules/import/core/package/addon.lua +++ b/xmake/core/sandbox/modules/import/core/package/addon.lua @@ -36,6 +36,7 @@ sandbox_core_package_addon.addons = addon.addons sandbox_core_package_addon.addondir = addon.addondir sandbox_core_package_addon.register = addon.register sandbox_core_package_addon.unregister = addon.unregister +sandbox_core_package_addon.remove = addon.remove sandbox_core_package_addon.rescan = addon.rescan sandbox_core_package_addon.clear = addon.clear diff --git a/xmake/modules/package/manager/xmake/search_package.lua b/xmake/modules/package/manager/xmake/search_package.lua index 32991ae3e..a5ed3caf8 100644 --- a/xmake/modules/package/manager/xmake/search_package.lua +++ b/xmake/modules/package/manager/xmake/search_package.lua @@ -23,7 +23,7 @@ import("core.base.semver") import("private.xrepo.quick_search.cache") function _search_package(packages, name, opt) - for _, packageinfo in ipairs(cache.find(name, {description = opt.description ~= false})) do + for _, packageinfo in ipairs(cache.find(name, {description = opt.description ~= false, kind = opt.kind})) do local packagename = packageinfo.name local packagedata = packageinfo.data @@ -59,7 +59,7 @@ end -- search package using the xmake package manager -- -- @param name the package name with pattern --- @param opt the options, e.g. {require_version = "1.x"} +-- @param opt the options, e.g. {require_version = "1.x", kind = "addon"} -- function main(name, opt) opt = opt or {} diff --git a/xmake/modules/private/action/require/search.lua b/xmake/modules/private/action/require/search.lua index e714c6c19..a0f98e0d9 100644 --- a/xmake/modules/private/action/require/search.lua +++ b/xmake/modules/private/action/require/search.lua @@ -20,6 +20,7 @@ -- imports import("core.base.task") +import("core.base.option") import("private.action.require.impl.utils.filter") import("private.action.require.impl.repository") import("private.action.require.impl.environment") @@ -41,11 +42,14 @@ function main(names) task.run("repo", {update = true}) end + -- we only search the addon packages if `--addon` is enabled + local kind = option.get("addon") and "addon" or nil + -- show title - print("The package names:") + print(kind == "addon" and "The addon names:" or "The package names:") -- search packages - for name, packages in pairs(search_packages(names)) do + for name, packages in pairs(search_packages(names, {kind = kind})) do if #packages > 0 then -- show name diff --git a/xmake/modules/private/xrepo/action/remove.lua b/xmake/modules/private/xrepo/action/remove.lua index ed92a83b8..adfec92c0 100644 --- a/xmake/modules/private/xrepo/action/remove.lua +++ b/xmake/modules/private/xrepo/action/remove.lua @@ -20,6 +20,7 @@ -- imports import("core.base.option") +import("core.package.addon") import("private.action.require.impl.remove_packages", {alias = "remove_all_packages"}) -- get menu options @@ -44,6 +45,9 @@ function menu_options() {nil, "toolchain", "kv", nil, "Set the toolchain name." }, {nil, "toolchain_host", "kv", nil, "Set the host toolchain name." }, { }, + {nil, "addon", "k", nil, "Remove the given installed addon packages.", + "e.g.", + " - xrepo remove --addon serial-monitor" }, {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.", @@ -193,10 +197,22 @@ function _remove_packages(packages) os.vexecv(os.programfile(), require_argv) end +-- remove the given installed addons +function _remove_addons(names) + for _, name in ipairs(names) do + local ok, errors = addon.remove(name) + assert(ok, errors) + cprint("${color.success}remove ${bright}%s${clear} ok!", name) + end +end + -- main entry function main() local packages = option.get("packages") - if option.get("all") then + if option.get("addon") then + assert(packages, "please specify the addons to be removed.") + _remove_addons(packages) + elseif option.get("all") then remove_all_packages(packages) elseif packages then _remove_packages(packages) diff --git a/xmake/modules/private/xrepo/action/search.lua b/xmake/modules/private/xrepo/action/search.lua index d389e3833..c532af44e 100644 --- a/xmake/modules/private/xrepo/action/search.lua +++ b/xmake/modules/private/xrepo/action/search.lua @@ -30,6 +30,9 @@ function menu_options() -- menu options local options = { + {nil, "addon", "k", nil, "Search the addon packages from /addons/", + "e.g.", + " - xrepo search --addon serial"}, {nil, "packages", "vs", nil, "The packages list (support lua pattern).", "e.g.", " - xrepo search zlib boost", @@ -80,6 +83,9 @@ function _search_packages(packages) if option.get("diagnosis") then table.insert(require_argv, "-D") end + if option.get("addon") then + table.insert(require_argv, "--addon") + end table.join2(require_argv, packages) os.vexecv(os.programfile(), require_argv) end diff --git a/xmake/modules/private/xrepo/quick_search/cache.lua b/xmake/modules/private/xrepo/quick_search/cache.lua index cf75a212d..43af417a8 100644 --- a/xmake/modules/private/xrepo/quick_search/cache.lua +++ b/xmake/modules/private/xrepo/quick_search/cache.lua @@ -25,20 +25,36 @@ import("private.action.require.impl.repository") local cache = globalcache.cache("quick_search") +-- get the cache key of the given package +-- +-- @note the addons are stored with the `addon::` prefix, +-- because an addon and a package may have the same name +-- +function _cachekey(packagename, kind) + return kind == "addon" and ("addon::" .. packagename) or packagename +end + -- search package directories from repositories +-- +-- the packages are stored in /packages//, +-- and the addons are stored in /addons// +-- function _list_package_dirs() -- find the package directories from all repositories local unique = {} local packageinfos = {} for _, repo in ipairs(repository.repositories()) do - for _, file in ipairs(os.files(path.join(repo:directory(), "packages", "*", "*", "xmake.lua"))) do - local dir = path.directory(file) - local subdirname = path.basename(path.directory(dir)) - if #subdirname == 1 then -- ignore l/luajit/port/xmake.lua - local packagename = path.filename(dir) - if not unique[packagename] then - table.insert(packageinfos, {name = packagename, repo = repo, packagedir = dir}) - unique[packagename] = true + for _, rootinfo in ipairs({{rootdir = "packages"}, {rootdir = "addons", kind = "addon"}}) do + for _, file in ipairs(os.files(path.join(repo:directory(), rootinfo.rootdir, "*", "*", "xmake.lua"))) do + local dir = path.directory(file) + local subdirname = path.basename(path.directory(dir)) + if #subdirname == 1 then -- ignore l/luajit/port/xmake.lua + local packagename = path.filename(dir) + local cachekey = _cachekey(packagename, rootinfo.kind) + if not unique[cachekey] then + table.insert(packageinfos, {name = packagename, kind = rootinfo.kind, repo = repo, packagedir = dir}) + unique[cachekey] = true + end end end end @@ -57,7 +73,9 @@ end function update() for _, packageinfo in ipairs(_list_package_dirs()) do local package = core_package.load_from_repository(packageinfo.name, packageinfo.packagedir, {repo = packageinfo.repo}) - cache:set(packageinfo.name, { + cache:set(_cachekey(packageinfo.name, packageinfo.kind), { + name = packageinfo.name, + kind = packageinfo.kind, reponame = package:repo() and package:repo():name(), description = package:description(), versions = package:versions(), @@ -79,22 +97,30 @@ function get() end -- find package +-- +-- @param name the package name (support lua pattern) +-- @param opt the options, e.g. {prefix = true, description = true, kind = "addon"} +-- function find(name, opt) _init() opt = opt or {} local list_result = {} - for packagename, packagedata in pairs(cache:data()) do - local found = false - if opt.prefix then - found = packagename:startswith(name) - else - found = packagename:find(path.pattern(name)) - end - if not found and opt.description and packagedata.description and packagedata.description:find(name) then - found = true - end - if found then - table.insert(list_result, {name = packagename, data = packagedata}) + for cachekey, packagedata in pairs(cache:data()) do + -- we only search the packages with the given kind, e.g. nil (package), "addon" + local packagename = packagedata.name or cachekey + if packagedata.kind == opt.kind then + local found = false + if opt.prefix then + found = packagename:startswith(name) + else + found = packagename:find(path.pattern(name)) + end + if not found and opt.description and packagedata.description and packagedata.description:find(name) then + found = true + end + if found then + table.insert(list_result, {name = packagename, data = packagedata}) + end end end return list_result -- cgit v1.3.1 From 7caece0cb3ee8ab207130edc17421d604610e686 Mon Sep 17 00:00:00 2001 From: ruki Date: Sun, 9 Aug 2026 17:57:20 +0800 Subject: improve addon to import includes, rules and toolchains --- tests/actions/addon/test.lua | 334 +++++++++++++++++++-- xmake/actions/addon/main.lua | 9 +- xmake/actions/addon/xmake.lua | 1 + xmake/actions/create/main.lua | 13 + xmake/actions/create/template.lua | 44 ++- xmake/core/base/interpreter.lua | 22 ++ xmake/core/base/task.lua | 16 +- xmake/core/package/addon.lua | 238 ++++++++++++++- xmake/core/project/project.lua | 4 + xmake/core/project/rule.lua | 87 +++++- .../modules/import/core/project/project.lua | 4 + .../sandbox/modules/import/core/sandbox/module.lua | 63 +++- xmake/core/tool/toolchain.lua | 53 +++- .../action/require/impl/actions/install.lua | 12 +- .../private/action/require/impl/package.lua | 19 +- xmake/modules/private/xrepo/action/remove.lua | 3 +- 16 files changed, 854 insertions(+), 68 deletions(-) diff --git a/tests/actions/addon/test.lua b/tests/actions/addon/test.lua index d393c0545..2be6acf94 100644 --- a/tests/actions/addon/test.lua +++ b/tests/actions/addon/test.lua @@ -1,8 +1,17 @@ import("core.base.global") --- write a minimal plugin that prints its name when run +-- the payloads of the mocked addons +-- +-- the plugins and templates are not namespaced, so they are named with the addon name to keep them unique, +-- the other payloads are always referenced with `@addon//` 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. /plugins//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. /templates///xmake.lua +-- write a minimal template, e.g. /templates///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. /rules//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. /toolchains//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. /modules/.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. /includes//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. /addons///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//) 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.")` + 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/")` + 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//` 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//` 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//` 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//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//` 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..` 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//