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/core/base/task.lua | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) (limited to 'xmake/core/base/task.lua') 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 -- 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 (limited to 'xmake/core/base/task.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 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(-) (limited to 'xmake/core/base/task.lua') 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//