diff options
| author | ruki <[email protected]> | 2026-08-02 00:24:14 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-08-02 00:24:14 +0800 |
| commit | 33ed4e501281de51b2b8dcfa442b176f0dd6d2d2 (patch) | |
| tree | f60409df69b8e5f3de5e43bbfef4d689233482fd | |
| parent | b41ed3acf227011f4904d95a439a30be9580e5cf (diff) | |
| parent | 8d7a74306556a4c292c0345f1a9aa833116ebcec (diff) | |
Merge pull request #7680 from xmake-io/plugin
Support for plugin distribution based on xmake-repo
| -rw-r--r-- | tests/plugins/repository/test.lua | 105 | ||||
| -rw-r--r-- | xmake/plugins/plugin/main.lua | 311 | ||||
| -rw-r--r-- | xmake/plugins/plugin/xmake.lua | 15 |
3 files changed, 324 insertions, 107 deletions
diff --git a/tests/plugins/repository/test.lua b/tests/plugins/repository/test.lua new file mode 100644 index 000000000..f1cfba8c7 --- /dev/null +++ b/tests/plugins/repository/test.lua @@ -0,0 +1,105 @@ +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 + +-- create a temporary plugin repository (packages layout: plugins/<first-letter>/<name>) 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(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", 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", 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", 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", "plugin-test-missing"}); return true end }) + t:require_not(try { function () os.runv("xmake", {"plugin", "--install", "somerepo@.."}); return true end }) +end diff --git a/xmake/plugins/plugin/main.lua b/xmake/plugins/plugin/main.lua index b362381e4..5158352c0 100644 --- a/xmake/plugins/plugin/main.lua +++ b/xmake/plugins/plugin/main.lua @@ -21,87 +21,128 @@ -- imports import("core.base.option") import("core.base.global") +import("core.package.repository") import("devel.git") -import("net.fasturl") import("private.action.require.impl.environment") --- get plugin urls -function _plugin_urls() - local urls = option.get("plugins") - if urls then - local result = {} - for _, url in ipairs(urls) do - table.insert(result, git.asgiturl(url) or url) +-- validate a plugin directory name +function _check_plugin_name(name) + assert(type(name) == "string" and name ~= "" and name ~= "." and not name:find("..", 1, true) and not name:find("[/\\:]"), "invalid plugin name(%s)!", name) + return name +end + +-- get plugin directory in ~/.xmake/plugins +function _get_plugindir(name) + local plugindir = path.join(global.directory(), "plugins") + return name and path.join(plugindir, _check_plugin_name(name)) or plugindir +end + +-- get local and global repositories, with local taking precedence +function _repositories() + return table.join(repository.repositories({global = false}), repository.repositories({global = true})) +end + +-- find a plugin directory in the given repository directory +-- +-- plugins in a repository follow the same layout as packages: +-- <repodir>/plugins/<first-letter>/<name>/xmake.lua +function _find_plugin_in_repo(repodir, name) + local dir = path.join(repodir, "plugins", name:sub(1, 1):lower(), name) + if os.isdir(dir) and os.isfile(path.join(dir, "xmake.lua")) then + return dir + end +end + +-- install a plugin from the given repository or the first repository containing it +function _install_plugins_from_repo(name, reponame) + _check_plugin_name(name) + for _, repo in ipairs(_repositories()) do + if not reponame or repo:name() == reponame then + local srcdir = _find_plugin_in_repo(repo:directory(), name) + if srcdir then + local dstdir = _get_plugindir(name) + assert(not os.isdir(dstdir), "plugin(%s) already exists!", name) + os.vcp(srcdir, dstdir) + cprint("${color.success}install ${bright}%s${clear} from repository ${bright}%s${clear} ok!", name, repo:name()) + return + end end - urls = result - else - urls = { - "https://github.com/xmake-io/xmake-plugins.git", - "https://gitlab.com/tboox/xmake-plugins.git", - "https://gitee.com/tboox/xmake-plugins.git"} - urls = fasturl.add(urls) - urls = fasturl.sort(urls) end - return urls + if reponame then + raise("plugin(%s): not found in repository %s!", name, reponame) + end + raise("plugin(%s): not found in any repository! try ${bright}xrepo update-repo${clear} first.", name) end --- get manifest path -function _manifest_path() - return path.join(global.directory(), "plugins", "manifest.txt") +-- install a single plugin from a source directory (as the given name, default to the directory name) +function _install_from_local(dir, name) + assert(os.isdir(dir) and os.isfile(path.join(dir, "xmake.lua")), "plugin path(%s): ${bright}xmake.lua${clear} not found!", dir) + name = name or path.filename(path.absolute(dir)) + local dstdir = _get_plugindir(name) + assert(not os.isdir(dstdir), "plugin(%s) already exists!", name) + os.vcp(dir, dstdir) + cprint("${color.success}install ${bright}%s${clear} ok!", name) end --- load manifest -function _load_manifest() - local manifest_path = _manifest_path() - if os.isfile(manifest_path) then - return io.load(manifest_path) +-- install a single plugin from a git url or github shortcut, e.g. https://github.com/xmake-io/hello-world +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 --- save manifest -function _save_manifest(manifest) - io.save(_manifest_path(), manifest) +-- install a single plugin +function _install_one(name) + -- parse repo@plugin format + local i = name:find("@", 1, true) + if i and not name:find("[/\\:]") then + local reponame = name:sub(1, i - 1) + local pluginname = name:sub(i + 1) + _install_plugins_from_repo(pluginname, 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_plugins_from_repo(name) end -- install plugins function _install() - - -- enter environment + local names = assert(option.get("plugins"), "please specify the plugins to be installed!") environment.enter() - try { function () - - -- do install - local urls = _plugin_urls() - local tmpdir = os.tmpfile() .. ".dir" - local plugindir = path.join(global.directory(), "plugins") - local installed_url - for _, url in ipairs(urls) do - cprint("installing plugins from %s ..", url) - git.clone(url, {verbose = option.get("verbose"), outputdir = tmpdir}) - installed_url = url - break + for _, name in ipairs(names) do + _install_one(name) end - for _, filepath in ipairs(os.files(path.join(tmpdir, "*", "xmake.lua"))) do - local srcdir = path.directory(filepath) - local name = path.filename(srcdir) - local dstdir = path.join(plugindir, name) - assert(not os.isdir(dstdir), "plugin(%s) already exists!", name) - os.vcp(srcdir, dstdir) - cprint(" ${yellow}->${clear} %s", name) - end - os.tryrm(tmpdir) - - -- save manifest - if installed_url then - local manifest = _load_manifest() or {} - manifest.urls = manifest.urls or {} - table.join2(manifest.urls, installed_url) - _save_manifest(manifest) - end - cprint("${bright}all plugins have been installed in %s!", plugindir) end, catch { @@ -110,57 +151,120 @@ function _install() end } } - - -- leave environment environment.leave() end --- update plugins -function _update() - - -- enter environment - environment.enter() +-- remove the given installed plugin +function _remove() + local names = assert(option.get("plugins"), "please specify the plugin name to be removed!") + assert(#names == 1, "please specify only one plugin name to be removed!") + local name = names[1] + local dir = _get_plugindir(name) + assert(os.isdir(dir), "plugin(%s) not found!", name) + os.rmdir(dir) + cprint("${color.success}remove ${bright}%s${clear} ok!", name) +end - try - { - function () +-- get the description of a plugin from its xmake.lua menu +function _plugin_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("description%s*=%s*\"(.-)\"") + end + end +end - -- do update - local manifest = _load_manifest() - assert(manifest and manifest.urls, "3rd plugins not found!") - local urls = manifest.urls - local plugindir = path.join(global.directory(), "plugins") - for _, url in ipairs(urls) do - cprint("updating plugins from %s ..", url) - local tmpdir = os.tmpfile() .. ".dir" - git.clone(url, {verbose = option.get("verbose"), outputdir = tmpdir}) - for _, filepath in ipairs(os.files(path.join(tmpdir, "*", "xmake.lua"))) do - local srcdir = path.directory(filepath) - local name = path.filename(srcdir) - local dstdir = path.join(plugindir, name) - os.tryrm(dstdir) - os.vcp(srcdir, dstdir) - cprint(" ${yellow}->${clear} %s", name) - end - os.tryrm(tmpdir) - end - cprint("${bright}all plugins have been updated in %s!", plugindir) - end, - catch - { - function (errors) - raise(errors) +-- collect plugins (each subdirectory containing xmake.lua) under the given root +-- +-- built-in/installed plugins are flat (<root>/<name>), while plugins in a +-- repository follow the packages layout (<root>/<first-letter>/<name>) +function _collect_plugins(root, seen, nested) + local entries = {} + local pattern = nested and path.join(root, "*", "*") or path.join(root, "*") + for _, dir in ipairs(os.dirs(pattern) or {}) do + local name = path.filename(dir) + if os.isfile(path.join(dir, "xmake.lua")) and (not seen or not seen[name]) then + if seen then + seen[name] = true end - } - } + table.insert(entries, {name = name, description = _plugin_description(dir)}) + end + end + return entries +end - -- leave environment - environment.leave() +-- print a plugin entry with its description aligned on the right +function _print_plugin(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 plugins +function _list() + local seen = {} + + -- collect plugins from every source + local builtin = _collect_plugins(path.join(os.programdir(), "plugins"), seen) + local plugindir = _get_plugindir() + local installed = _collect_plugins(plugindir, seen) + local avail = {} + for _, repo in ipairs(_repositories()) do + table.join2(avail, _collect_plugins(path.join(repo:directory(), "plugins"), seen, true)) + end + + -- compute the alignment width from all plugin names + local width = 0 + for _, entries in ipairs({builtin, installed, avail}) do + for _, entry in ipairs(entries) do + width = math.max(width, #entry.name + 4) + end + end + + -- built-in plugins + cprint("${bright}the built-in plugins:${clear}") + if #builtin > 0 then + for _, entry in ipairs(builtin) do + _print_plugin(entry.name, entry.description, width) + end + else + print(" (none)") + end + + -- installed plugins + cprint("${bright}the installed plugins:${clear}") + if #installed > 0 then + for _, entry in ipairs(installed) do + _print_plugin(entry.name, entry.description, width) + end + else + print(" (none)") + end + + -- plugins 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 plugin --install %s to install)", entry.name) + _print_plugin(entry.name, entry.description, width, note) + end + else + print(" (none)") + end end -- clear all installed plugins function _clear() - local plugindir = path.join(global.directory(), "plugins") + local plugindir = _get_plugindir() if os.isdir(plugindir) then os.rmdir(plugindir) end @@ -170,10 +274,11 @@ end function main() if option.get("install") then _install() - elseif option.get("update") then - _update() + 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/plugin/xmake.lua b/xmake/plugins/plugin/xmake.lua index 05d9e3631..4aab3daa4 100644 --- a/xmake/plugins/plugin/xmake.lua +++ b/xmake/plugins/plugin/xmake.lua @@ -26,11 +26,18 @@ task("plugin") description = "Manage plugins of xmake.", options = { {'i', "install", "k", nil, "Install plugins."}, - {'u', "update", "k", nil, "Update plugins."}, + {'r', "remove", "k", nil, "Remove the given installed plugin."}, + {'l', "list", "k", nil, "List all installed plugins."}, {'c', "clear", "k", nil, "Clear all installed plugins."}, - {nil, "plugins", "v", nil, "The plugins path or url.", + {nil, "plugins", "vs", nil, "The plugin paths, urls or names.", "e.g.", - " $ xmake plugin --install https://github.com/xmake-io/xmake-plugins", - " $ xmake plugin --update"} + " $ xmake plugin --install https://github.com/myrepo/hello", + " $ xmake plugin --install github:myrepo/hello", + " $ xmake plugin --install github:myrepo/hello#dev", + " $ xmake plugin --install /tmp/my-plugin", + " $ xmake plugin --install xmake-repo@hello", + " $ xmake plugin --install hello", + " $ xmake plugin --remove hello", + " $ xmake plugin --list"} } } |
