diff options
| author | ruki <[email protected]> | 2026-08-15 10:38:51 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-08-15 10:38:51 +0800 |
| commit | e4314dc3b2f483ef97b397ed5cf11fcdd397c9a0 (patch) | |
| tree | a28db860b0f7af1a5a66bf2ed29c87126a29324a | |
| parent | 6c246c12a09524780e5bfdc53bc27a892e4793a9 (diff) | |
| parent | 045ce8a7a62e38357af0fd250675f279f14beafe (diff) | |
Merge pull request #7702 from xmake-io/preload
Auto fetch addons
28 files changed, 1063 insertions, 100 deletions
diff --git a/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua b/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua index 443690792..c0ed37914 100644 --- a/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua +++ b/tests/actions/addon/custom-toolchain/src/modules/detect/tools/find_mycl6x.lua @@ -9,7 +9,7 @@ function main(opt) for _, name in ipairs({"gcc", "clang", "cc"}) do local program = find_program(name, opt) if program then - return {program = program} + return program end end end diff --git a/tests/actions/addon/projects/autofetch-badinclude/xmake-addons.lua b/tests/actions/addon/projects/autofetch-badinclude/xmake-addons.lua new file mode 100644 index 000000000..6931855ae --- /dev/null +++ b/tests/actions/addon/projects/autofetch-badinclude/xmake-addons.lua @@ -0,0 +1,2 @@ +-- this file only declares the addons, it cannot reference them +includes("@addon/custom-include/check") diff --git a/tests/actions/addon/projects/autofetch-badinclude/xmake.lua b/tests/actions/addon/projects/autofetch-badinclude/xmake.lua new file mode 100644 index 000000000..0d94cf61c --- /dev/null +++ b/tests/actions/addon/projects/autofetch-badinclude/xmake.lua @@ -0,0 +1,2 @@ +target("test") + set_kind("phony") diff --git a/tests/actions/addon/projects/autofetch-badname/xmake-addons.lua b/tests/actions/addon/projects/autofetch-badname/xmake-addons.lua new file mode 100644 index 000000000..165325fb7 --- /dev/null +++ b/tests/actions/addon/projects/autofetch-badname/xmake-addons.lua @@ -0,0 +1,2 @@ +-- the `addon` name is reserved for the addon references +add_addons("addon") diff --git a/tests/actions/addon/projects/autofetch-badname/xmake.lua b/tests/actions/addon/projects/autofetch-badname/xmake.lua new file mode 100644 index 000000000..0d94cf61c --- /dev/null +++ b/tests/actions/addon/projects/autofetch-badname/xmake.lua @@ -0,0 +1,2 @@ +target("test") + set_kind("phony") diff --git a/tests/actions/addon/projects/autofetch-build/src/main.c b/tests/actions/addon/projects/autofetch-build/src/main.c new file mode 100644 index 000000000..112afe090 --- /dev/null +++ b/tests/actions/addon/projects/autofetch-build/src/main.c @@ -0,0 +1,11 @@ +// the addons must provide the option and the toolchain of this project +#ifndef MYOPTION +# error the option of the custom-include addon is not found! +#endif +#ifndef MY_C6000 +# error the toolchain of the custom-toolchain addon is not used! +#endif + +int main(int argc, char** argv) { + return 0; +} diff --git a/tests/actions/addon/projects/autofetch-build/xmake-addons.lua b/tests/actions/addon/projects/autofetch-build/xmake-addons.lua new file mode 100644 index 000000000..2091e3b7f --- /dev/null +++ b/tests/actions/addon/projects/autofetch-build/xmake-addons.lua @@ -0,0 +1,2 @@ +-- the addons which this project needs, they are installed automatically when we load it +add_addons("custom-include", "custom-rule", "custom-toolchain") diff --git a/tests/actions/addon/projects/autofetch-build/xmake.lua b/tests/actions/addon/projects/autofetch-build/xmake.lua new file mode 100644 index 000000000..bad3f8928 --- /dev/null +++ b/tests/actions/addon/projects/autofetch-build/xmake.lua @@ -0,0 +1,11 @@ +-- the option comes from the includes file of an addon +includes("@addon/custom-include/check") + +target("hello") + set_kind("binary") + add_files("src/main.c") + add_rules("@addon/custom-rule/hello") + set_toolchains("@addon/custom-toolchain/my-c6000") + if has_config("myoption") then + add_defines("MYOPTION") + end diff --git a/tests/actions/addon/projects/autofetch/xmake-addons.lua b/tests/actions/addon/projects/autofetch/xmake-addons.lua new file mode 100644 index 000000000..a85e0fe11 --- /dev/null +++ b/tests/actions/addon/projects/autofetch/xmake-addons.lua @@ -0,0 +1 @@ +add_addons("custom-include") diff --git a/tests/actions/addon/projects/autofetch/xmake.lua b/tests/actions/addon/projects/autofetch/xmake.lua new file mode 100644 index 000000000..4eaca0785 --- /dev/null +++ b/tests/actions/addon/projects/autofetch/xmake.lua @@ -0,0 +1,5 @@ +-- the addon is installed automatically, so we can use its includes file here +includes("@addon/custom-include/check") + +target("test") + set_kind("phony") diff --git a/tests/actions/addon/test.lua b/tests/actions/addon/test.lua index 3542c907b..d59ae6f4b 100644 --- a/tests/actions/addon/test.lua +++ b/tests/actions/addon/test.lua @@ -29,10 +29,14 @@ function _with_addons(names, func) func, finally { - function () + -- @note try() swallows the errors if we do not re-raise them here + function (ok, errors) for _, name in ipairs(names) do _remove(name) end + if not ok then + raise(errors) + end end } } @@ -54,6 +58,14 @@ function _with_repo(recipes, func) local cachefile = path.join(global.cachedir(), "repository") local cache = os.isfile(cachefile) and io.load(cachefile) or {} cache.repositories = cache.repositories or {} + + -- a killed test run may leave its temporary repository registered, and a dangling + -- repository breaks every following xrepo command, so we drop them here + for name, dirs in pairs(cache.repositories) do + if name:startswith("addon-test-repo-") and not os.isdir(dirs[1]) then + cache.repositories[name] = nil + end + end cache.repositories[reponame] = {repodir} io.save(cachefile, cache) os.tryrm(path.join(global.cachedir(), "quick_search")) @@ -65,7 +77,8 @@ function _with_repo(recipes, func) end, finally { - function () + -- @note try() swallows the errors if we do not re-raise them here + function (ok, errors) for name, _ in pairs(recipes) do _remove(name) end @@ -76,6 +89,9 @@ function _with_repo(recipes, func) io.save(cachefile, cache) os.tryrm(path.join(global.cachedir(), "quick_search")) os.tryrm(repodir) + if not ok then + raise(errors) + end end } } @@ -114,6 +130,36 @@ function _config_project(content) return _run_project(content, {"config", "-y"}) end +-- copy the given fixture project to a temporary directory and run the given function in it +-- +-- @note we cannot run them in place, they would generate the lock and the build files, +-- @see tests/actions/addon/projects +-- +function _with_project(name, func) + local projectdir = os.tmpfile() .. ".addon-project" + os.tryrm(projectdir) + os.mkdir(projectdir) + os.cp(path.join(os.scriptdir(), "projects", name, "*"), projectdir) + local oldir = os.cd(projectdir) + try + { + function () + func(projectdir) + end, + finally + { + -- @note try() swallows the errors if we do not re-raise them here + function (ok, errors) + os.cd(oldir) + os.tryrm(projectdir) + if not ok then + raise(errors) + end + end + } + } +end + -- only the payloads should be installed, our own files should not function test_install(t) _with_addons({"custom-toolchain"}, function () @@ -223,6 +269,92 @@ target("test") end) end +-- the addons which a project declares in `xmake-addons.lua` are installed automatically +-- +-- @note they must be installed before loading the project, it may use their includes files +function test_autofetch(t) + local recipes = {["custom-include"] = ("set_sourcedir(%q)"):format(_addondir("custom-include"))} + _with_repo(recipes, function () + _remove("custom-include") + _with_project("autofetch", function (projectdir) + + -- it should be installed when loading the project, so that its includes file can be found, + -- and we should tell the user why we install something, it may need to confirm and download + local output = os.iorunv("xmake", {"config", "-y"}) + t:require(output:find("custom-include: includes check is loaded", 1, true)) + t:require(output:find("this project needs the addons", 1, true)) + + -- and it should be locked + local lockfile = path.join(projectdir, "xmake-addons.lock") + t:require(os.isfile(lockfile)) + t:require(io.load(lockfile)["custom-include"] ~= nil) + + -- we should not install it again + t:require_not(os.iorunv("xmake", {"config", "-y"}):find("install custom-include", 1, true)) + end) + end) +end + +-- every command builds the option menu, which merges the project tasks in a best-effort way, +-- so the commands which need not the project should never install its addons +function test_autofetch_skipped_for_option_menu(t) + local recipes = {["custom-include"] = ("set_sourcedir(%q)"):format(_addondir("custom-include"))} + _with_repo(recipes, function () + _remove("custom-include") + _with_project("autofetch", function (projectdir) + os.runv("xmake", {"addon", "--list"}) + os.runv("xmake", {"lua", "-c", "print(\"hello\")"}) + t:require_not(os.isfile(path.join(projectdir, "xmake-addons.lock"))) + end) + end) +end + +-- a complete project which declares its addons in `xmake-addons.lua` and builds with them, +-- @see tests/actions/addon/projects/autofetch-build +function test_autofetch_build(t) + + -- @note the compiler of the custom toolchain is just the host one, + -- so we can only build it if there is one + if not (find_program("gcc") or find_program("clang") or find_program("cc")) then + return + end + + local names = {"custom-include", "custom-rule", "custom-toolchain"} + local recipes = {} + for _, name in ipairs(names) do + recipes[name] = ("set_sourcedir(%q)"):format(_addondir(name)) + end + _with_repo(recipes, function () + for _, name in ipairs(names) do + _remove(name) + end + _with_project("autofetch-build", function (projectdir) + + -- all of them should be installed when loading the project, and it should build + -- with their includes file, rule and toolchain, @see src/main.c + local output = os.iorunv("xmake", {"build", "-y"}) + t:require(output:find("custom-include: includes check is loaded", 1, true)) + t:require(output:find("custom-rule: hello from custom-rule: hello", 1, true)) + t:require(output:find("build ok", 1, true)) + + -- and all of them should be locked + local lockinfo = io.load(path.join(projectdir, "xmake-addons.lock")) + for _, name in ipairs(names) do + t:require(lockinfo[name] ~= nil) + end + end) + end) +end + +-- the addons file only declares the addons, it cannot reference them +function test_autofetch_invalid(t) + for _, name in ipairs({"autofetch-badinclude", "autofetch-badname"}) do + _with_project(name, function () + t:require_not(try { function () os.runv("xmake", {"config", "-y"}); return true end }) + end) + end +end + -- the addons can be installed from a repository, by plain name and by repo@name function test_install_from_repo(t) local recipes = {["custom-plugin"] = ("set_sourcedir(%q)"):format(_addondir("custom-plugin"))} @@ -235,8 +367,12 @@ function test_install_from_repo(t) t:require(os.iorunv("xmake", {"hello_addon"}):find("hello from custom-plugin", 1, true)) -- it should be searchable, and the addons should not be found by the package search + -- + -- @note we cannot run the `xrepo` program here, it may not be in the PATH, e.g. on the ci, + -- and `xrepo search` is just a wrapper of it + -- t:require(os.iorunv("xmake", {"addon", "--search", "custom-plugin"}):find("custom-plugin", 1, true)) - t:require_not(os.iorunv("xrepo", {"search", "custom-plugin"}):find("custom-plugin", 1, true)) + t:require_not(os.iorunv("xmake", {"lua", "private.xrepo", "search", "custom-plugin"}):find("custom-plugin", 1, true)) end) end diff --git a/xmake/actions/addon/main.lua b/xmake/actions/addon/main.lua index 7af106c05..29df38b0a 100644 --- a/xmake/actions/addon/main.lua +++ b/xmake/actions/addon/main.lua @@ -22,6 +22,8 @@ import("core.base.option") import("core.package.addon") import("devel.git") +import("private.action.addon.impl.install_addons") +import("private.action.addon.impl.xrepo", {alias = "xrepo_addon"}) import("private.action.require.impl.environment") import("private.action.require.impl.search_packages") @@ -49,30 +51,10 @@ function _get_addondir(name, version) return addondir end --- 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") - end - if option.get("verbose") then - table.insert(argv, "-v") - end - 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 - -- 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}) + xrepo_addon("install", {reponame and (reponame .. "@" .. name) or name}, {force = option.get("force")}) end -- install a single addon from a source directory (as the given name, default to the directory name) @@ -225,15 +207,41 @@ function _install() end -- remove the given installed addons +-- +-- @note `xrepo remove --addon` removes them in the same way, +-- @see xmake/modules/private/xrepo/action/remove.lua +-- function _remove() - local names = assert(option.get("addons"), "please specify the addon name to be removed!") - _xrepo("remove", names) + local names = option.get("addons") + local force = option.get("force") + + -- remove all the installed addons? e.g. xmake addon --remove --all + if option.get("all") then + names = table.keys(addon.addons()) + if #names == 0 then + wprint("no installed addons!") + return + end + -- the dependencies between them do not matter, they are all removed + force = true + end + + assert(names, "please specify the addon name to be removed!") + for _, name in ipairs(names) do + addon.remove(name, {force = force}) + cprint("${color.success}remove ${bright}%s${clear} ok!", name) + end +end + +-- upgrade the addons which the current project declares in its `xmake-addons.lua` +function _upgrade() + install_addons(os.projectdir(), {upgrade = true}) 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) + xrepo_addon("search", patterns) end -- collect the installed addons from the addons registry @@ -243,8 +251,12 @@ end -- function _collect_installed_addons() local entries = {} - for name, addoninfo in pairs(addon.rescan()) do + for name, addoninfo in pairs(addon.addons()) do + -- an addon can be installed with several versions, we show the active one, + -- the projects can lock the other ones, @see core/project/addons.lua + local versions = addon.versions(name) table.insert(entries, {name = name, version = addoninfo.version, + versions = #versions > 1 and versions or nil, description = addoninfo.description, payloads = addoninfo.payloads}) end table.sort(entries, function (a, b) return a.name < b.name end) @@ -285,7 +297,11 @@ function _list() if #installed > 0 then for _, entry in ipairs(installed) do exclude[entry.name] = true - _print_addon(entry, string.format(" ${dim}(%s)${clear}", table.concat(entry.payloads, ", "))) + local suffix = string.format(" ${dim}(%s)${clear}", table.concat(entry.payloads, ", ")) + if entry.versions then + suffix = suffix .. string.format(" ${dim}[installed: %s]${clear}", table.concat(entry.versions, ", ")) + end + _print_addon(entry, suffix) end else print(" (none)") @@ -308,22 +324,16 @@ function _list() 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("upgrade") then + _upgrade() elseif option.get("list") then _list() elseif option.get("search") then _search() - elseif option.get("clear") then - _clear() end end diff --git a/xmake/actions/addon/xmake.lua b/xmake/actions/addon/xmake.lua index 3c814cc6d..a0266aa4f 100644 --- a/xmake/actions/addon/xmake.lua +++ b/xmake/actions/addon/xmake.lua @@ -29,7 +29,8 @@ task("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."}, + {'u', "upgrade", "k", nil, "Upgrade the addons which the current project declares."}, + {nil, "all", "k", nil, "Remove all installed addons, e.g. xmake addon --remove --all"}, {'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.", @@ -40,6 +41,8 @@ task("addon") " $ xmake addon --install xmake-repo@serial-monitor", " $ xmake addon --install serial-monitor", " $ xmake addon --remove serial-monitor", + " $ xmake addon --remove --all", + " $ xmake addon --upgrade", " $ xmake addon --search serial", " $ xmake addon --list"} } diff --git a/xmake/core/base/interpreter.lua b/xmake/core/base/interpreter.lua index ede772de0..01b689fe6 100644 --- a/xmake/core/base/interpreter.lua +++ b/xmake/core/base/interpreter.lua @@ -884,6 +884,36 @@ end -- -- the root api will affect these scopes -- +-- get the root file name of the included directories, e.g. includes("subdir") -> subdir/xmake.lua +function interpreter:includes_rootfilename() + return self._PRIVATE._INCLUDES_ROOTFILENAME or "xmake.lua" +end + +-- set the root file name of the included directories +-- +-- e.g. interp:includes_rootfilename_set("xmake-addons.lua") -> includes("subdir") -> subdir/xmake-addons.lua +-- +function interpreter:includes_rootfilename_set(filename) + self._PRIVATE._INCLUDES_ROOTFILENAME = filename +end + +-- can we include the referenced files? e.g. includes("@builtin/check"), includes("@addon/esp32/board") +function interpreter:includes_references() + return self._PRIVATE._INCLUDES_REFERENCES ~= false +end + +-- enable/disable the referenced files of includes() +-- +-- @param enabled enable them or not +-- @param hint the extra hint of the error message +-- +-- @note the addons file is loaded before the addons are installed, so it cannot reference them +-- +function interpreter:includes_references_set(enabled, hint) + self._PRIVATE._INCLUDES_REFERENCES = enabled + self._PRIVATE._INCLUDES_REFERENCES_HINT = hint +end + function interpreter:rootscope_set(scope_kind) assert(self and self._PRIVATE) self._PRIVATE._ROOTSCOPE = scope_kind @@ -1826,6 +1856,12 @@ function interpreter:api_builtin_includes(...) local subpaths_matched = {} for _, subpath in ipairs(subpaths) do local found = false + -- the referenced files are not always available, e.g. the addons file + if subpath:startswith("@") and not self:includes_references() then + local hint = self._PRIVATE._INCLUDES_REFERENCES_HINT + os.raise("includes(%s): the referenced files are not supported in %s!%s", + subpath, path.filename(curfile), hint and ("\n" .. hint) or "") + end -- attempt to find files from programdir/includes/*.lua -- e.g. includes("@builtin/check") if subpath:startswith("@builtin/") then @@ -1848,7 +1884,7 @@ function interpreter:api_builtin_includes(...) files = os.files(subpath) else -- @see https://github.com/xmake-io/xmake/issues/6026 - files = os.files(path.join(subpath, "xmake.lua")) + files = os.files(path.join(subpath, self:includes_rootfilename())) end if files and #files > 0 then table.join2(subpaths_matched, files) diff --git a/xmake/core/base/semver.lua b/xmake/core/base/semver.lua index 7c2b6ea8d..fc989e18d 100644 --- a/xmake/core/base/semver.lua +++ b/xmake/core/base/semver.lua @@ -232,5 +232,15 @@ function semver.match(str, pos, pattern) end end +-- is a valid semantic version? e.g. "1.2.3", "v1.2.3-beta" +function semver.is_valid(version) + return semver.parse(version) ~= nil +end + +-- is a valid semantic version range? e.g. ">=1.0 <2.0", "^1.2", "master || >1.4" +function semver.is_valid_range(range) + return semver.satisfies("1.0", range) ~= nil +end + -- return module: semver return semver diff --git a/xmake/core/base/task.lua b/xmake/core/base/task.lua index a901490eb..2cd1fa58a 100644 --- a/xmake/core/base/task.lua +++ b/xmake/core/base/task.lua @@ -426,6 +426,12 @@ function task._is_conflicting(taskname, taskfile, filepath) return true end +-- clear the loaded tasks, e.g. some addons may be installed just now +function task.clear() + task._TASKS = nil + task._DIRECTORIES = nil +end + -- get all registered tasks -- -- @return the tasks table {name = task, ...} diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua index 586a3ea15..4a4c8f50a 100644 --- a/xmake/core/package/addon.lua +++ b/xmake/core/package/addon.lua @@ -70,15 +70,16 @@ function addon._registryfile() return path.join(addon.installdir(), "addons.conf") end --- save the given addons to the registry file -function addon._save(addons) - addon._ADDONS = addons +-- save the given registry to the registry file +function addon._save(registry) + addon._REGISTRY = registry + addon._ADDONS = nil 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 + if table.empty(registry) and not os.isfile(registryfile) then return end - local ok, errors = io.save(registryfile, addons) + local ok, errors = io.save(registryfile, registry) if not ok then utils.warning(errors) end @@ -182,10 +183,15 @@ end function addon._parents(name) local dirname = addon.dirname(name) local parents - for otherdirname, addoninfo in pairs(addon.addons()) do - if otherdirname ~= dirname and table.contains(addoninfo.deps or {}, dirname) then - parents = parents or {} - table.insert(parents, otherdirname) + for otherdirname, entry in pairs(addon._registry()) do + if otherdirname ~= dirname then + for _, addoninfo in pairs(entry.versions or {}) do + if table.contains(addoninfo.deps or {}, dirname) then + parents = parents or {} + table.insert(parents, otherdirname) + break + end + end end end if parents then @@ -194,14 +200,27 @@ function addon._parents(name) return parents end --- unregister the given addon -function addon._unregister(name) +-- unregister the given addon or only one of its versions +function addon._unregister(name, version) local dirname = addon.dirname(name) - local addons = addon.addons() - if addons[dirname] then - addons[dirname] = nil - addon._save(addons) + local registry = addon._registry() + local entry = registry[dirname] + if entry == nil then + return end + if version then + entry.versions[version] = nil + if entry.active == version then + -- we need to select the other one deterministically + entry.active = addon.versions(name)[1] + end + if table.empty(entry.versions) then + registry[dirname] = nil + end + else + registry[dirname] = nil + end + addon._save(registry) end -- get the apis of the addon manifest @@ -275,6 +294,26 @@ function addon.manifest(sourcedir) return manifest end +-- get a working directory which has no project +-- +-- we need it to run the sub-processes of the addons, e.g. `xrepo install --addon`, +-- otherwise they would load the project of the current directory again +-- +-- @note we cannot use `os.tmpdir()` directly, it is shared by all the commands, +-- e.g. a stray `xmake.lua` in it would break the isolation +-- +-- @note we can share it between the processes, we only use it as the working directory +-- and never write anything into it, @see private/action/addon/impl/xrepo.lua +-- +function addon.workdir() + local workdir = path.join(os.tmpdir(), "addons", "working") + if not os.isdir(workdir) then + -- it may be created by the other processes at the same time, we can ignore it + os.mkdir(workdir) + end + return workdir +end + -- the install directory of addons, e.g. ~/.xmake/addons function addon.installdir() return path.join(global.directory(), "addons") @@ -400,7 +439,64 @@ function addon.resolve_reference(reference, sep, kind, opt) return {dir = payloaddir, name = name, addon = addonname} end --- get all installed addons +-- get the registry of the installed addons +-- +-- an addon can be installed with several versions at the same time, e.g. the projects +-- may lock the different versions of it, so we save all of them +-- +-- @return the registry, e.g. {["esp32"] = {active = "1.0.3", versions = {["1.0.3"] = {...}}}} +-- +function addon._registry(opt) + local registry = addon._REGISTRY + if opt and opt.force then + registry = nil + end + if registry == nil then + registry = {} + local registryfile = addon._registryfile() + if os.isfile(registryfile) then + registry = io.load(registryfile) or {} + end + -- migrate the old registry, it only saved one version for each addon + for dirname, addoninfo in pairs(registry) do + if addoninfo.versions == nil then + registry[dirname] = {active = addoninfo.version, + versions = {[addoninfo.version] = addoninfo}} + end + end + addon._REGISTRY = registry + addon._ADDONS = nil + end + return registry +end + +-- pin the active version of the given addon for this process +-- +-- @note a project locks the versions of its addons, so we need to activate them +-- when we load it, @see core/project/addons.lua +-- +function addon.pin(name, version) + local pinned = addon._PINNED + if pinned == nil then + pinned = {} + addon._PINNED = pinned + end + pinned[addon.dirname(name)] = version + addon._ADDONS = nil +end + +-- get all the installed versions of the given addon, e.g. {"1.0.2", "1.0.3"} +function addon.versions(name) + local versions = {} + local addoninfo = addon._registry()[addon.dirname(name)] + for version, _ in pairs(addoninfo and addoninfo.versions or {}) do + table.insert(versions, version) + end + table.sort(versions) + return versions +end + +-- get all installed addons, only the active version of each addon -- -- @param opt the options, e.g. {force = true}, we need it to reload the registry -- if the addons have been installed by another process @@ -408,15 +504,20 @@ end -- @return the addons table, e.g. {["hello-world"] = {version = "latest", payloads = {"plugins"}}} -- function addon.addons(opt) - local addons = addon._ADDONS if opt and opt.force then - addons = nil + addon._registry({force = true}) end + local addons = addon._ADDONS if addons == nil then addons = {} - local registryfile = addon._registryfile() - if os.isfile(registryfile) then - addons = io.load(registryfile) or {} + local pinned = addon._PINNED or {} + for dirname, addoninfo in pairs(addon._registry()) do + -- the project may lock another version of it, @see addon.pin + local version = pinned[dirname] or addoninfo.active + local versioninfo = addoninfo.versions and addoninfo.versions[version] + if versioninfo then + addons[dirname] = versioninfo + end end addon._ADDONS = addons end @@ -592,6 +693,8 @@ function addon.register(name, version, opt) name = name ~= dirname and name or nil, description = opt.description, deps = opt.deps, + -- where it comes from, e.g. {url = ..., commit = ..., branch = ...} + repo = opt.repo, -- the deps which the addon itself declares in its manifest, they are -- recorded whenever this addon has one, so that the repositories can -- check that the manifest and the package recipe are kept in sync @@ -608,9 +711,17 @@ function addon.register(name, version, opt) return false, errors end - local addons = addon.addons() - addons[dirname] = addoninfo - addon._save(addons) + -- we can install several versions of an addon at the same time, + -- and the version which we install now is always the active one + local registry = addon._registry() + local entry = registry[dirname] + if entry == nil or entry.versions == nil then + entry = {versions = {}} + registry[dirname] = entry + end + entry.versions[version] = addoninfo + entry.active = version + addon._save(registry) return true end @@ -656,21 +767,34 @@ function addon.remove(name, opt) return true end +-- reload the addons registry and the caches which are built from it +-- +-- @note we need it if the addons have been installed by another process, +-- e.g. the addons which a project declares, @see core/project/project.lua +-- +function addon.reload() + addon._REGISTRY = nil + addon._ADDONS = nil + addon._MANIFESTS = nil + addon._GLOBALMODULES = nil +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 oldaddons = addon.addons() - local addons = {} + local oldregistry = addon._registry() + local registry = {} for _, versiondir in ipairs(os.dirs(path.join(addon.installdir(), "*", "*"))) do local payloads = addon.payloads_of(versiondir) if #payloads > 0 then local dirname = path.filename(path.directory(versiondir)) local version = path.filename(versiondir) - local oldaddoninfo = oldaddons[dirname] + local oldentry = oldregistry[dirname] + local oldaddoninfo = oldentry and oldentry.versions and oldentry.versions[version] local description, deps - if oldaddoninfo and oldaddoninfo.version == version then + if oldaddoninfo then -- we need to keep them, we cannot get them from the installed payloads description = oldaddoninfo.description deps = oldaddoninfo.deps @@ -685,24 +809,22 @@ function addon.rescan() description = manifest.description or description end end - addons[dirname] = {version = version, name = name or (oldaddoninfo and oldaddoninfo.name), - description = description, deps = deps, payloads = payloads, - plugins = addon._plugins_of(versiondir), templates = addon._templates_of(versiondir)} + local entry = registry[dirname] + if entry == nil then + entry = {versions = {}} + registry[dirname] = entry + end + entry.versions[version] = {version = version, name = name or (oldaddoninfo and oldaddoninfo.name), + description = description, deps = deps, payloads = payloads, + plugins = addon._plugins_of(versiondir), templates = addon._templates_of(versiondir)} + -- we keep the active version if it's still installed, otherwise we use the last one + if entry.active == nil or (oldentry and oldentry.active == version) then + entry.active = version + end 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 = {} - addon._MANIFESTS = nil - addon._GLOBALMODULES = nil + addon._save(registry) + return addon.addons() end -- return module diff --git a/xmake/core/project/addons.lua b/xmake/core/project/addons.lua new file mode 100644 index 000000000..3b105840e --- /dev/null +++ b/xmake/core/project/addons.lua @@ -0,0 +1,229 @@ +--!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 addons.lua +-- + +-- define module +local addons = addons or {} + +-- load modules +local os = require("base/os") +local io = require("base/io") +local path = require("base/path") +local table = require("base/table") +local semver = require("base/semver") +local addon = require("package/addon") + +-- the file which declares the addons of a project, e.g. <projectdir>/xmake-addons.lua +-- +-- it's loaded before the project file, so that the addons are always installed when +-- we load the project, e.g. includes("@addon/esp32-devel/board") +-- +-- e.g. +-- add_addons("esp32-devel 1.0.x") +-- add_addons("serial-tools", {optional = true}) +-- add_repositories("myrepo [email protected]:me/myrepo.git") +-- +function addons.filename() + return "xmake-addons.lua" +end + +function addons.file(projectdir) + return path.join(projectdir or os.projectdir(), addons.filename()) +end + +-- the lock file of the declared addons, e.g. <projectdir>/xmake-addons.lock +-- +-- @note it's independent of `xmake-requires.lock`, the addons are always locked, +-- they provide the build scripts and we should never change them silently +-- +function addons.lockfile(projectdir) + return path.join(projectdir or os.projectdir(), "xmake-addons.lock") +end + +-- get the format version of the addons lock file +-- +-- @see xmake/core/project/project.lua, project.requireslock_version() +-- +function addons.lockfile_version() + return "1.0" +end + +-- get the apis of the addons file +-- +-- @note it only declares which addons this project needs, the addon resources +-- are always referenced from the project file, e.g. add_rules("@addon/esp32-devel/app") +-- +function addons.apis() + return { + values = { + "add_addons" + -- the repositories which provide them, e.g. add_repositories("myrepo [email protected]:me/myrepo.git") + , "add_repositories" + } + } +end + +-- the interpreter of the addons file +function addons._interpreter() + local interp = addons._INTERPRETER + if interp == nil then + -- we need to load it lazily, the interpreter also depends on the addon module + local interpreter = require("base/interpreter") + interp = interpreter.new() + interp:api_define(addons.apis()) + -- the sub-projects declare their addons in this file too, + -- e.g. includes("sub") -> sub/xmake-addons.lua + interp:includes_rootfilename_set(addons.filename()) + -- and we cannot reference the addons here, they have not been installed yet, + -- e.g. includes("@addon/esp32-devel/board") + interp:includes_references_set(false, "please move it to the project file(xmake.lua)!") + addons._INTERPRETER = interp + end + return interp +end + +-- load the declared addons of the given project directory +-- +-- @return the addons information and errors, it will be nil if this project declares nothing, +-- e.g. {addons = {"esp32-devel 1.0.x"}, addons_extra = {...}} +-- +function addons.load(projectdir) + local filepath = addons.file(projectdir) + if not os.isfile(filepath) then + return + end + + -- enter the project directory, the include paths are relative to it, + -- e.g. includes("sub") + local oldir, errors = os.cd(path.directory(filepath)) + if not oldir then + return nil, errors + end + + local rootinfo + local interp = addons._interpreter() + local ok, errors = interp:load(filepath) + if ok then + rootinfo, errors = interp:make("root", true, true) + end + os.cd(oldir) + if not rootinfo then + return nil, errors + end + + local addonsinfo = {addons = table.wrap(rootinfo:get("addons")), + addons_extra = rootinfo:extraconf("addons"), + repositories = table.wrap(rootinfo:get("repositories"))} + + -- check the declared addons + local declared = {} + for _, requirestr in ipairs(addonsinfo.addons) do + + -- this file is loaded before the addons are installed, so it cannot reference them + if requirestr:startswith("@") then + return nil, string.format("%s: cannot reference the addon resources(%s) here, please move it to the project file(xmake.lua)!", + filepath, requirestr) + end + + local name = addons.requirename(requirestr) + if name == "addon" or name == "self" then + return nil, string.format("%s: the addon name(%s) is reserved by xmake for the addon references, please rename it!", + filepath, name) + end + if name == "." or name == ".." or name:find("[/\\:]") then + return nil, string.format("%s: invalid addon name(%s)!", filepath, name) + end + + -- we can only install one version of an addon for a project + if declared[name] then + return nil, string.format("%s: the addon(%s) is declared twice, e.g. `%s` and `%s`, please merge them!", + filepath, name, declared[name], requirestr) + end + declared[name] = requirestr + end + return addonsinfo +end + +-- split the given declaration into the name and the version range +-- +-- @note the version range can contain spaces, so everything after the name belongs to it, +-- e.g. "esp32-devel 1.0.x", "esp32-devel >=1.0 <2.0", "esp32-devel master || >1.4", +-- @see xmake/modules/private/utils/package.lua +-- +function addons.requirename(requirestr) + local splitinfo = requirestr:split("%s+", {limit = 2}) + return splitinfo[1], splitinfo[2] +end + +-- is the locked version still valid for the given declaration? +-- +-- @note the declaration is authoritative, the lock only pins a version inside it, +-- so we need to resolve it again if the user has changed the declared version +-- +function addons.locked_valid(requirestr, lockinfo) + if not lockinfo or not lockinfo.version then + return false + end + local _, range = addons.requirename(requirestr) + if range then + -- @note we can only compare the semantic versions, e.g. the local addons are always `latest` + return semver.is_valid(lockinfo.version) and semver.satisfies(lockinfo.version, range) + end + return true +end + +-- get the locked addons, e.g. {["esp32-devel"] = {version = "1.0.3"}} +function addons.locked(projectdir) + local lockfile = addons.lockfile(projectdir) + if os.isfile(lockfile) then + local lockinfo = io.load(lockfile) + if lockinfo then + lockinfo.__meta__ = nil + return lockinfo + end + end +end + +-- are all the declared addons installed already? +-- +-- @note we need to check it in-process for every command which loads the project, +-- so we only check the locked versions here, the installer will resolve them again +-- +function addons.satisfied(addonsinfo, projectdir) + local locked = addons.locked(projectdir) + if not locked then + return false + end + local installed = addon.addons() + for _, requirestr in ipairs(addonsinfo.addons) do + local name = addons.requirename(requirestr) + local lockinfo = locked[name] + if not addons.locked_valid(requirestr, lockinfo) then + return false + end + local addoninfo = installed[addon.dirname(name)] + if not addoninfo or addoninfo.version ~= lockinfo.version then + return false + end + end + return true +end + +-- return module +return addons diff --git a/xmake/core/project/project.lua b/xmake/core/project/project.lua index 803bb5d9b..3b975cccd 100644 --- a/xmake/core/project/project.lua +++ b/xmake/core/project/project.lua @@ -45,6 +45,8 @@ local option = require("project/option") local policy = require("project/policy") local project_package = require("project/package") local deprecated_project = require("project/deprecated/project") +local addon = require("package/addon") +local addons = require("project/addons") local package = require("package/package") local platform = require("platform/platform") local toolchain = require("tool/toolchain") @@ -227,11 +229,121 @@ function project._api_add_toolchaindirs(interp, ...) end end +-- install the addons which this project declares +-- +-- @note we cannot install them here, we are loading the project, so we do it in a +-- sub-process, @see xmake/modules/private/action/addon/impl/install_addons.lua +-- +function project._install_addons() + -- @note we need to cache the result, the project may be loaded many times, + -- otherwise the failure would be ignored by the next load + if not project._ADDONS_CHECKED then + project._ADDONS_CHECKED = true + project._ADDONS_OK, project._ADDONS_ERRORS = project._do_install_addons() + end + return project._ADDONS_OK, project._ADDONS_ERRORS +end + +-- activate the addon versions which this project locks +-- +-- @note an addon can be installed with several versions at the same time, the other +-- projects may lock the other versions of it, @see core/package/addon.lua +-- +function project._pin_addons() + for name, lockinfo in pairs(addons.locked() or {}) do + if lockinfo.version then + addon.pin(name, lockinfo.version) + end + end +end + +-- do install the addons which this project declares +function project._do_install_addons() + + -- this project declares nothing? + local addonsinfo, errors = addons.load() + if errors then + return false, errors + end + if not addonsinfo or #addonsinfo.addons == 0 then + return true + end + + -- they have been installed already? + project._pin_addons() + if addons.satisfied(addonsinfo) then + return true + end + + -- tell the user why we are installing something, it may need to confirm and download, + -- e.g. `xmake --help` in a project directory which declares some addons + utils.cprint("${color.warning}note: ${clear}%s: this project needs the addons(${bright}%s${clear}), installing them ..", + addons.filename(), table.concat(addonsinfo.addons, ", ")) + if baseoption.get("help") then + -- the help menu also shows the options which the addons provide, but the user + -- did not ask for an installation, so we tell them how to skip it + utils.cprint("${dim}we can run it outside of the project directory to skip the installation${clear}") + end + + -- @note we run it in a working directory which has no project, @see addon.workdir(), + -- otherwise it would load this project again + -- + -- @note we may be called when building the option menu, the command line has not + -- been parsed yet, so we can only get the common flags from the raw arguments + -- + local argv = {"lua"} + local flags = {["-y"] = "--yes", ["--yes"] = "--yes", + ["-v"] = "--verbose", ["--verbose"] = "--verbose", + ["-D"] = "--diagnosis", ["--diagnosis"] = "--diagnosis"} + local flags_added = {} + for _, arg in ipairs(xmake._COMMAND_ARGV or {}) do + local flag = flags[arg] + if flag and not flags_added[flag] then + table.insert(argv, flag) + flags_added[flag] = true + end + end + table.insert(argv, "private.action.addon.impl.install_addons") + table.insert(argv, os.projectdir()) + local ok, errors = os.execv(os.programfile(), argv, {curdir = addon.workdir()}) + if ok ~= 0 then + return false, errors or "install the addons of this project failed!" + end + + -- we have loaded the registry and its caches before installing them, so we need to reload it + addon.reload() + project._pin_addons() + rule.clear() + task.clear() + return true +end + -- load the project file -function project._load(force, disable_filter) +-- +-- @param opt the options +-- - force: load the project file again even if it has been loaded +-- - disable_filter: disable the interpreter filter, e.g. `$(plat)` +-- - skip_addons: do not install the addons which this project declares +-- +function project._load(opt) + opt = opt or {} + + -- install the addons which this project declares in `xmake-addons.lua` first, + -- it may use their rules, toolchains and includes files, + -- e.g. includes("@addon/esp32-devel/board") + -- + -- @note we need to check it before the cache, the project file may have been loaded + -- already without them, e.g. by the option menu + -- + if not opt.skip_addons then + local ok, errors = project._install_addons() + if not ok then + return false, errors + end + end -- has already been loaded? - if project._memcache():get("rootinfo") and not force then + if project._memcache():get("rootinfo") and not opt.force then return true end @@ -261,13 +373,13 @@ function project._load(force, disable_filter) end -- load the root info of the project - local rootinfo, errors = project._load_scope("root", true, not disable_filter) + local rootinfo, errors = project._load_scope("root", true, not opt.disable_filter) if not rootinfo then return false, errors end -- load the root info of the target - local rootinfo_target, errors = project._load_scope("root.target", true, not disable_filter) + local rootinfo_target, errors = project._load_scope("root.target", true, not opt.disable_filter) if not rootinfo_target then return false, errors end @@ -311,6 +423,11 @@ function project._load_scope(scope_kind, deduplicate, enable_filter) end -- load tasks +-- +-- @note we should not install the addons which this project declares here, the option menu +-- merges the project tasks in a best-effort way and every command builds it, +-- e.g. `xmake lua`, `xmake addon --remove --all`, @see xmake/core/main.lua +-- function project._load_tasks() -- the project file is not found? @@ -319,7 +436,7 @@ function project._load_tasks() end -- load the project file first and disable filter - local ok, errors = project._load(true, true) + local ok, errors = project._load({force = true, disable_filter = true, skip_addons = true}) if not ok then return nil, errors end @@ -400,7 +517,7 @@ function project._load_targets() -- load all requires first and reload the project file to ensure has_package() works for targets local requires = project.required_packages() - local ok, errors = project._load(true) + local ok, errors = project._load({force = true}) if not ok then return nil, errors end @@ -481,7 +598,7 @@ function project._load_options(disable_filter) end -- reload the project file to ensure `if is_plat() then add_packagedirs() end` works - local ok, errors = project._load(true, disable_filter) + local ok, errors = project._load({force = true, disable_filter = disable_filter}) if not ok then return nil, errors end @@ -1129,7 +1246,7 @@ function project.requires_str() if not requires_str then -- reload the project file to handle `has_config()` - local ok, errors = project._load(true) + local ok, errors = project._load({force = true}) if not ok then os.raise(errors) end diff --git a/xmake/core/sandbox/modules/import/core/base/semver.lua b/xmake/core/sandbox/modules/import/core/base/semver.lua index 45ba8b74f..04df315eb 100644 --- a/xmake/core/sandbox/modules/import/core/base/semver.lua +++ b/xmake/core/sandbox/modules/import/core/base/semver.lua @@ -50,15 +50,10 @@ function sandbox_core_base_semver.match(str, pos, pattern) end -- is valid version? -function sandbox_core_base_semver.is_valid(version) - return semver.parse(version) ~= nil -end +sandbox_core_base_semver.is_valid = semver.is_valid -- is valid version range? -function sandbox_core_base_semver.is_valid_range(range) - local ok = semver.satisfies("1.0", range) - return ok ~= nil -end +sandbox_core_base_semver.is_valid_range = semver.is_valid_range -- compare two version strings -- diff --git a/xmake/core/sandbox/modules/import/core/package/addon.lua b/xmake/core/sandbox/modules/import/core/package/addon.lua index b297347c6..74610c693 100644 --- a/xmake/core/sandbox/modules/import/core/package/addon.lua +++ b/xmake/core/sandbox/modules/import/core/package/addon.lua @@ -27,6 +27,7 @@ local raise = require("sandbox/modules/raise") -- inherit some builtin interfaces sandbox_core_package_addon.installdir = addon.installdir +sandbox_core_package_addon.workdir = addon.workdir sandbox_core_package_addon.dirname = addon.dirname sandbox_core_package_addon.owner = addon.owner sandbox_core_package_addon.is_reference = addon.is_reference @@ -35,9 +36,10 @@ sandbox_core_package_addon.payloadinfos = addon.payloadinfos sandbox_core_package_addon.payloads_of = addon.payloads_of sandbox_core_package_addon.payloadroot = addon.payloadroot sandbox_core_package_addon.addons = addon.addons +sandbox_core_package_addon.versions = addon.versions +sandbox_core_package_addon.pin = addon.pin sandbox_core_package_addon.addondir = addon.addondir sandbox_core_package_addon.rescan = addon.rescan -sandbox_core_package_addon.clear = addon.clear -- get the manifest of the given addon directory, e.g. <sourcedir>/addon.lua -- diff --git a/xmake/core/sandbox/modules/import/core/project/addons.lua b/xmake/core/sandbox/modules/import/core/project/addons.lua new file mode 100644 index 000000000..542cbaf4f --- /dev/null +++ b/xmake/core/sandbox/modules/import/core/project/addons.lua @@ -0,0 +1,52 @@ +--!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 addons.lua +-- + +-- define module +local sandbox_core_project_addons = sandbox_core_project_addons or {} + +-- load modules +local addons = require("project/addons") +local raise = require("sandbox/modules/raise") + +-- inherit some builtin interfaces +sandbox_core_project_addons.file = addons.file +sandbox_core_project_addons.filename = addons.filename +sandbox_core_project_addons.lockfile = addons.lockfile +sandbox_core_project_addons.lockfile_version = addons.lockfile_version +sandbox_core_project_addons.locked = addons.locked +sandbox_core_project_addons.locked_valid = addons.locked_valid +sandbox_core_project_addons.requirename = addons.requirename +sandbox_core_project_addons.satisfied = addons.satisfied + +-- load the declared addons of the given project directory +-- +-- @param projectdir the project directory +-- @return the addons information, it will be nil if this project declares nothing +-- +function sandbox_core_project_addons.load(projectdir) + local addonsinfo, errors = addons.load(projectdir) + if errors then + raise(errors) + end + return addonsinfo +end + +-- return module +return sandbox_core_project_addons diff --git a/xmake/core/sandbox/modules/import/core/sandbox/module.lua b/xmake/core/sandbox/modules/import/core/sandbox/module.lua index 4cf419b44..294cf7556 100644 --- a/xmake/core/sandbox/modules/import/core/sandbox/module.lua +++ b/xmake/core/sandbox/modules/import/core/sandbox/module.lua @@ -452,6 +452,14 @@ end -- find module function core_sandbox_module.find(name) + + -- an addon can export some modules as the global modules, they are also visible here, + -- e.g. find_toolname() looks for `detect.tools.find_xxx` with it, @see addon.globalmodules() + local globalmodulesdir = addon.globalmodules()[name] + if globalmodulesdir and core_sandbox_module._find(globalmodulesdir, name) then + return true + end + for _, moduledir in ipairs(core_sandbox_module.directories()) do if (core_sandbox_module._find(moduledir, name)) then return true diff --git a/xmake/modules/private/action/addon/impl/install_addons.lua b/xmake/modules/private/action/addon/impl/install_addons.lua new file mode 100644 index 000000000..a20ed740b --- /dev/null +++ b/xmake/modules/private/action/addon/impl/install_addons.lua @@ -0,0 +1,133 @@ +--!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 install_addons.lua +-- + +-- imports +import("core.package.addon") +import("core.project.addons") +import("private.action.addon.impl.xrepo", {alias = "xrepo_addon"}) + +-- get the requires of the declared addons +-- +-- @note we install the locked versions, but the declaration is authoritative, so we +-- resolve them again if the user has changed it or upgrades them +-- +function _get_requires(addonsinfo, locked) + local requires = {} + for _, requirestr in ipairs(addonsinfo.addons) do + local name = addons.requirename(requirestr) + local lockinfo = locked and locked[name] + if addons.locked_valid(requirestr, lockinfo) then + requirestr = name .. " " .. lockinfo.version + end + table.insert(requires, requirestr) + end + return requires +end + +-- lock the installed addons, so that we always get the same ones +-- +-- @note we get the installed versions from the addons registry, they are +-- registered when installing them, @see core/package/addon.lua +-- +function _lock_addons(projectdir, addonsinfo) + local lockinfo = {} + local locked = addons.locked(projectdir) or {} + -- @note we need to reload the registry, they have been installed by another process + local installed = addon.addons({force = true}) + for _, requirestr in ipairs(addonsinfo.addons) do + local name = addons.requirename(requirestr) + local addoninfo = installed[addon.dirname(name)] + if addoninfo then + local oldversion = locked[name] and locked[name].version + if oldversion and oldversion ~= addoninfo.version then + cprint("${color.success}upgrade ${bright}%s${clear}: %s -> %s", name, oldversion, addoninfo.version) + end + -- we lock the repository too, so that the other users get it from the same source, + -- @see xmake/modules/private/action/require/impl/lock_packages.lua + lockinfo[name] = {version = addoninfo.version, repo = addoninfo.repo} + end + end + lockinfo.__meta__ = {version = addons.lockfile_version()} + + -- @note we need to write it deterministically, the key order of a lua table is random, + -- otherwise the lock file would change even if nothing changed, + -- @see xmake/modules/private/action/require/impl/lock_packages.lua + local content = string.serialize(lockinfo, {orderkeys = true}) + local tmpfile = os.tmpfile() + io.writefile(tmpfile, content, {encoding = "binary"}) + + -- and we only write it if the content is different, so we can keep the file time + os.cp(tmpfile, addons.lockfile(projectdir), {copy_if_different = true}) + os.rm(tmpfile) +end + +-- install the addons which the given project declares in its `xmake-addons.lua` +-- +-- @note we are also called from a sub-process, the project cannot be loaded until +-- its addons are installed, @see core/project/project.lua +-- +function main(projectdir, opt) + opt = opt or {} + projectdir = projectdir or os.projectdir() + local addonsinfo = addons.load(projectdir) + if not addonsinfo or #addonsinfo.addons == 0 then + return + end + + -- upgrade them? we need to resolve the declared versions again + local locked = not opt.upgrade and addons.locked(projectdir) or nil + + -- install them with xrepo, it installs the packages in its own working directory, + -- so we need not a project here + + -- this project declares its own repositories? we pass them to xrepo, + -- they are only used by this installation, we do not register them globally + local rcfile + if #addonsinfo.repositories > 0 then + rcfile = os.tmpfile() .. ".lua" + local file = io.open(rcfile, "w") + for _, repo in ipairs(addonsinfo.repositories) do + file:print("add_repositories(%q)", repo) + end + file:close() + end + + try + { + function () + xrepo_addon("install", _get_requires(addonsinfo, locked), {includes = rcfile}) + end, + finally + { + -- @note try() swallows the errors if we do not re-raise them here + function (ok, errors) + if rcfile then + os.tryrm(rcfile) + end + if not ok then + raise(errors) + end + end + } + } + + -- and lock them, so that the other users get the same versions + _lock_addons(projectdir, addonsinfo) +end diff --git a/xmake/modules/private/action/addon/impl/xrepo.lua b/xmake/modules/private/action/addon/impl/xrepo.lua new file mode 100644 index 000000000..46ada2d01 --- /dev/null +++ b/xmake/modules/private/action/addon/impl/xrepo.lua @@ -0,0 +1,57 @@ +--!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 xrepo.lua +-- + +-- imports +import("core.base.option") +import("core.package.addon") + +-- run the given xrepo action for the addons +-- +-- @note xrepo installs the packages in its own working project, so it works anywhere, +-- and we need not implement the download/dependencies/confirm logic again +-- +-- @param action the action name, e.g. "install", "search" +-- @param names the addon names, urls or require strings, e.g. {"esp32-devel 1.0.x"} +-- @param opt the options, e.g. {force = true, includes = "/tmp/xxx.lua"} +-- +-- @note we always run it in a working directory which has no project, @see addon.workdir() +-- +function main(action, names, opt) + opt = opt or {} + local argv = {"lua", "private.xrepo", action, "--addon"} + + -- we need to pass the common options to the sub-process, e.g. -y, -v, -D + for _, name in ipairs({"yes", "verbose", "diagnosis"}) do + if option.get(name) then + table.insert(argv, "--" .. name) + end + end + if opt.force then + table.insert(argv, "--force") + end + + -- the extra lua configuration files, e.g. the repositories which a project declares + if opt.includes then + table.insert(argv, "--includes=" .. opt.includes) + end + + table.join2(argv, names) + os.execv(os.programfile(), argv, {curdir = opt.curdir or addon.workdir()}) +end diff --git a/xmake/modules/private/action/require/impl/actions/install.lua b/xmake/modules/private/action/require/impl/actions/install.lua index 65750c1bc..7ddb66cca 100644 --- a/xmake/modules/private/action/require/impl/actions/install.lua +++ b/xmake/modules/private/action/require/impl/actions/install.lua @@ -318,8 +318,12 @@ function _register_addon(package) end end + -- we also record where it comes from, so that the projects can lock it, + -- @see xmake/modules/private/action/addon/impl/install_addons.lua + local repo = package:repo() addon.register(package:name(), package:version_str() or "latest", {description = description, deps = deps, + repo = repo and {url = repo:url(), commit = repo:commit(), branch = repo:branch()} or nil, manifest_deps = manifest and manifest.deps or nil, globalmodules = manifest and #manifest.globalmodules > 0 and manifest.globalmodules or nil}) end diff --git a/xmake/modules/private/action/require/impl/install_packages.lua b/xmake/modules/private/action/require/impl/install_packages.lua index 7ab247351..2d633109e 100644 --- a/xmake/modules/private/action/require/impl/install_packages.lua +++ b/xmake/modules/private/action/require/impl/install_packages.lua @@ -211,6 +211,8 @@ function _get_confirm(packages, opt) -- show tips if opt.toolchain then cprint("${bright color.warning}note: ${clear}install or modify (m) these ${bright}toolchain${clear} packages first (pass -y to skip confirm)?") + elseif opt.packagekind == "addon" then + cprint("${bright color.warning}note: ${clear}install or modify (m) these ${bright}addons${clear} (pass -y to skip confirm)?") else cprint("${bright color.warning}note: ${clear}install or modify (m) these packages (pass -y to skip confirm)?") end diff --git a/xmake/modules/private/action/require/impl/package.lua b/xmake/modules/private/action/require/impl/package.lua index 65d5f4153..1105384c8 100644 --- a/xmake/modules/private/action/require/impl/package.lua +++ b/xmake/modules/private/action/require/impl/package.lua @@ -1702,6 +1702,9 @@ function get_configs_str(package) end if requireinfo.kind then table.insert(configs, requireinfo.kind) + elseif package:is_addon() then + -- @note the kind is only set for the dependencies, e.g. add_deps("foo", {kind = "addon"}) + table.insert(configs, "addon") end local ignored_configs_for_buildhash = hashset.from(requireinfo.ignored_configs_for_buildhash or {}) local configs_overrided = requireinfo.configs_overrided or {} |
