summaryrefslogtreecommitdiff
path: root/xmake/core/package/addon.lua
diff options
context:
space:
mode:
Diffstat (limited to 'xmake/core/package/addon.lua')
-rw-r--r--xmake/core/package/addon.lua826
1 files changed, 826 insertions, 0 deletions
diff --git a/xmake/core/package/addon.lua b/xmake/core/package/addon.lua
new file mode 100644
index 000000000..d6798f56f
--- /dev/null
+++ b/xmake/core/package/addon.lua
@@ -0,0 +1,826 @@
+--!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 manifest file of an addon, e.g. <sourcedir>/addon.lua
+--
+-- an addon describes itself in this file, so its name and layout never depend on
+-- the package name of the repository which distributes it
+--
+function addon._manifestfile(sourcedir)
+ return path.join(sourcedir, "addon.lua")
+end
+
+-- the interpreter of the addon manifest
+function addon._interpreter()
+ local interp = addon._INTERPRETER
+ if interp == nil then
+ -- we need to load it lazily, the interpreter also depends on this module
+ local interpreter = require("base/interpreter")
+ interp = interpreter.new()
+ interp:api_define(addon.apis())
+ addon._INTERPRETER = interp
+ end
+ return interp
+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
+
+-- 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 table.empty(registry) and not os.isfile(registryfile) then
+ return
+ end
+ local ok, errors = io.save(registryfile, registry)
+ if not ok then
+ utils.warning(errors)
+ end
+end
+
+-- get the payload directory of the given addon
+--
+-- @param name the addon name, e.g. "esp32"
+-- @param kind the payload kind, e.g. "rules", "modules"
+-- @return the directory, e.g. ~/.xmake/addons/esp32/v1.0.0/rules
+--
+function addon._payloaddir(name, kind)
+ local dirname = addon.dirname(name)
+ local addoninfo = addon.addons()[dirname]
+ if addoninfo and table.contains(addoninfo.payloads or {}, kind) then
+ return path.join(addon.installdir(), dirname, addoninfo.version, kind)
+ end
+end
+
+-- get the plugin task names of the given addon directory
+--
+-- the plugins are not namespaced, we need them to check the conflicts
+--
+function addon._plugins_of(addondir)
+ local plugins = {}
+ for _, filepath in ipairs(os.files(path.join(addondir, "plugins", "*", "xmake.lua"))) do
+ local content = io.readfile(filepath)
+ if content then
+ for taskname in content:gmatch("task%s*%(%s*\"(.-)\"") do
+ table.insert(plugins, taskname)
+ end
+ end
+ end
+ return plugins
+end
+
+-- get the template ids of the given addon directory, e.g. {"c/console"}
+--
+-- the templates are not namespaced, we need them to check the conflicts
+--
+function addon._templates_of(addondir)
+ local templates = {}
+ local templatesdir = path.join(addondir, "templates")
+ for _, langdir in ipairs(os.dirs(path.join(templatesdir, "*"))) do
+ local lang = path.filename(langdir)
+ local accepted = {}
+ for _, filepath in ipairs(os.files(path.join(langdir, "**", "xmake.lua"))) do
+ local dir = path.directory(filepath)
+ local relpath = path.relative(dir, langdir)
+ if relpath and relpath ~= "." then
+ local nested = false
+ for _, root in ipairs(accepted) do
+ if dir:startswith(root .. path.sep()) then
+ nested = true
+ break
+ end
+ end
+ if not nested then
+ table.insert(accepted, dir)
+ table.insert(templates, lang .. "/" .. (relpath:gsub("[/\\]", ".")))
+ end
+ end
+ end
+ end
+ return templates
+end
+
+-- check the conflicts of the plugins and templates, they are not namespaced
+--
+-- @param dirname the addon directory name
+-- @param addoninfo the addon information, @see addon.register
+--
+-- @return the errors if there are some conflicts
+--
+function addon._check_conflicts(dirname, addoninfo)
+ local kindnames = {plugins = "plugin", templates = "template", globalmodules = "global module"}
+ for _, kind in ipairs({"plugins", "templates", "globalmodules"}) do
+ for _, name in ipairs(addoninfo[kind] or {}) do
+ for otherdirname, otheraddoninfo in pairs(addon.addons()) do
+ if otherdirname ~= dirname and table.contains(otheraddoninfo[kind] or {}, name) then
+ return string.format("%s(%s) conflicts, it has been provided by the addon(%s)!\nplease remove one of them, e.g. xmake addon --remove %s",
+ kindnames[kind], name, otherdirname, otherdirname)
+ end
+ end
+ end
+ end
+
+ -- the global modules can also conflict with the builtin and the user modules
+ --
+ -- @note the `core.*` modules are in the core directory of the sandbox,
+ -- they are not in `<programdir>/modules`,
+ -- @see core/sandbox/modules/import/core/sandbox/module.lua
+ local moduledirs = {path.join(os.programdir(), "modules"),
+ path.join(os.programdir(), "core", "sandbox", "modules", "import"),
+ path.join(global.directory(), "modules")}
+ for _, name in ipairs(addoninfo.globalmodules or {}) do
+ local modulepath = (name:gsub("%.", "/")) .. ".lua"
+ for _, moduledir in ipairs(moduledirs) do
+ if os.isfile(path.join(moduledir, modulepath)) then
+ return string.format("global module(%s) conflicts, it has been provided by %s!\nplease rename it in the addon manifest.",
+ name, moduledir:startswith(os.programdir()) and "xmake" or moduledir)
+ end
+ end
+ end
+end
+
+-- get the addons which depend on the given addon
+function addon._parents(name)
+ local dirname = addon.dirname(name)
+ local parents
+ for otherdirname, entry in table.orderpairs(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
+ return parents
+end
+
+-- unregister the given addon or only one of its versions
+function addon.unregister(name, version)
+ local dirname = addon.dirname(name)
+ 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
+function addon.apis()
+ return {
+ values = {
+ -- addon.set_xxx
+ "addon.set_description"
+ , "addon.set_homepage"
+ , "addon.set_license"
+ , "addon.set_sourcedir"
+ -- addon.add_xxx
+ , "addon.add_deps"
+ , "addon.add_globalmodules"
+ }
+ }
+end
+
+-- get the manifest of the given addon directory
+--
+-- @param sourcedir the addon source or install directory, which contains `addon.lua`
+--
+-- @return the manifest, e.g. {name = "esp32-devel", description = "...", sourcedir = "src", deps = {"serial-tools"}}
+-- it will be nil if this addon does not describe itself
+--
+function addon.manifest(sourcedir)
+
+ -- we may resolve a lot of `@self` references, so we need to cache them
+ local manifests = addon._MANIFESTS
+ if manifests == nil then
+ manifests = {}
+ addon._MANIFESTS = manifests
+ end
+ local cachekey = path.absolute(sourcedir)
+ local cacheinfo = manifests[cachekey]
+ if cacheinfo ~= nil then
+ return cacheinfo or nil
+ end
+
+ local manifestfile = addon._manifestfile(sourcedir)
+ if not os.isfile(manifestfile) then
+ manifests[cachekey] = false
+ return
+ end
+ local interp = addon._interpreter()
+ local ok, errors = interp:load(manifestfile)
+ if not ok then
+ return nil, errors
+ end
+ local results, errors = interp:make("addon", true, true)
+ if not results then
+ return nil, errors
+ end
+ local manifest
+ for name, addoninfo in pairs(results) do
+ if manifest then
+ return nil, string.format("%s: only one addon() scope is allowed!", manifestfile)
+ end
+ manifest = {name = name,
+ description = addoninfo:get("description"),
+ homepage = addoninfo:get("homepage"),
+ license = addoninfo:get("license"),
+ sourcedir = addoninfo:get("sourcedir"),
+ deps = table.wrap(addoninfo:get("deps")),
+ globalmodules = table.wrap(addoninfo:get("globalmodules"))}
+ end
+ if not manifest then
+ return nil, string.format("%s: no addon() scope found!", manifestfile)
+ end
+ manifests[cachekey] = manifest
+ 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")
+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
+
+-- is the given reference an addon reference?
+--
+-- e.g. "@addon/esp32/flash", "@self/flash", "@addon.esp32.sdkconfig", "@self.sdkconfig"
+--
+function addon.is_reference(reference, sep)
+ return reference:startswith("@addon" .. sep) or reference:startswith("@self" .. sep)
+end
+
+-- get the addon which owns the given script directory
+--
+-- it's used to resolve the `@self` references inside an addon,
+-- so that the addon code never needs to know its own installed name
+--
+-- @param scriptdir the script directory, e.g. ~/.xmake/addons/esp32/v1.0.0/rules/flash
+-- it will be the directory of the caller script by default
+-- @return the addon name and its root directory, e.g. esp32, ~/.xmake/addons/esp32/v1.0.0
+--
+function addon.owner(scriptdir)
+ if not scriptdir then
+ -- we can get it from the sandbox of the caller script, e.g. the rule script of an addon
+ local sandbox = require("sandbox/sandbox")
+ local instance = sandbox.instance()
+ scriptdir = instance and instance:rootdir()
+ end
+ if not scriptdir then
+ return
+ end
+ scriptdir = path.absolute(scriptdir)
+
+ -- the installed addons, e.g. ~/.xmake/addons/<name>/<version>/...
+ local installdir = path.absolute(addon.installdir())
+ if scriptdir:startswith(installdir .. path.sep()) then
+ local parts = path.split(path.relative(scriptdir, installdir))
+ if #parts >= 2 then
+ local addondir = path.join(installdir, parts[1], parts[2])
+ -- the registry keeps the raw addon name, the directory name is only
+ -- its normalized form, e.g. "myns::foo" -> "myns_foo"
+ local addoninfo = addon.addons()[parts[1]]
+ return addoninfo and addoninfo.name or parts[1], addondir
+ end
+ return
+ end
+
+ -- the addon source directory, we can also run the addon code in place when developing it
+ local dir = scriptdir
+ while dir and #dir > 0 do
+ -- the addon describes itself? we get its name from the manifest
+ local manifest = addon.manifest(dir)
+ if manifest then
+ return manifest.name, dir
+ end
+ -- otherwise we can only guess it from the payload directories
+ for _, payloaddir in ipairs(addon._payloaddirs()) do
+ if os.isdir(path.join(dir, payloaddir)) then
+ return path.filename(dir), dir
+ end
+ end
+ local parentdir = path.directory(dir)
+ if not parentdir or parentdir == dir then
+ break
+ end
+ dir = parentdir
+ end
+end
+
+-- resolve the given addon reference to its payload directory
+--
+-- the addon resources are referenced with the addon name from the outside,
+-- and with `@self` from the addon code itself, e.g.
+--
+-- add_rules("@addon/esp32/flash"), import("@addon.esp32.sdkconfig") -- from a project
+-- add_rules("@self/flash"), import("@self.sdkconfig") -- from the addon itself
+--
+-- @param reference the reference, e.g. "@addon/esp32/flash", "@self.sdkconfig"
+-- @param sep the separator, e.g. "/", "."
+-- @param kind the payload kind, e.g. "rules", "modules"
+-- @param opt the options, e.g. {scriptdir = "..."}, it's used to resolve `@self`
+--
+-- @return the reference information and errors,
+-- e.g. {dir = "~/.xmake/addons/esp32/v1.0.0/rules", name = "flash", addon = "esp32"}
+--
+function addon.resolve_reference(reference, sep, kind, opt)
+ opt = opt or {}
+
+ -- resolve the `@self` reference from the addon which owns the current script
+ if reference:startswith("@self" .. sep) then
+ local name = reference:sub(#("@self" .. sep) + 1)
+ if name == "" then
+ return nil, string.format("invalid addon reference(%s)!", reference)
+ end
+ local addonname, addondir = addon.owner(opt.scriptdir)
+ if not addondir then
+ return nil, string.format("%s: cannot resolve `@self`, it can only be used inside an addon!", reference)
+ end
+ return {dir = path.join(addondir, kind), name = name, addon = addonname}
+ end
+
+ -- resolve the `@addon` reference, the addon name is always required
+ local prefix = "@addon" .. sep
+ if not reference:startswith(prefix) then
+ return
+ end
+ local pos = reference:find(sep, #prefix + 1, true)
+ local addonname = pos and reference:sub(#prefix + 1, pos - 1)
+ local name = pos and reference:sub(pos + 1)
+ if not addonname or addonname == "" or not name or name == "" then
+ return nil, string.format("invalid addon reference(%s), it should be `@addon%s<addon>%s<name>`", reference, sep, sep)
+ end
+ local payloaddir = addon._payloaddir(addonname, kind)
+ if not payloaddir then
+ return nil, string.format("%s not found!\nplease install the addon which provides it first: xmake addon --install %s", reference, addonname)
+ end
+ return {dir = payloaddir, name = name, addon = addonname}
+end
+
+-- 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 addoninfo = addon._registry()[addon.dirname(name)]
+ return table.orderkeys(addoninfo and addoninfo.versions or {})
+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
+--
+-- @return the addons table, e.g. {["hello-world"] = {version = "latest", payloads = {"plugins"}}}
+--
+function addon.addons(opt)
+ opt = opt or {}
+ if opt.force then
+ addon._registry({force = true})
+ end
+
+ -- get the really installed versions instead of the pinned ones? e.g. locking them
+ -- @note we do not cache it, the cache is the pinned view of this project
+ if opt.unpinned then
+ local addons = {}
+ for dirname, addoninfo in pairs(addon._registry()) do
+ local versioninfo = addoninfo.versions and addoninfo.versions[addoninfo.active]
+ if versioninfo then
+ addons[dirname] = versioninfo
+ end
+ end
+ return addons
+ end
+
+ local addons = addon._ADDONS
+ if addons == nil then
+ addons = {}
+ 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
+ return addons
+end
+
+-- get the install directory of the given addon, e.g. ~/.xmake/addons/<name>/<version>
+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 modules which the installed addons export as the global modules
+--
+-- they are declared in the addon manifest, e.g. add_globalmodules("core.tools.esptool"),
+-- so that they can be imported with their plain names by the internal calls,
+-- e.g. import("core.tools.esptool"), find_tool("esptool")
+--
+-- @return the modules table, e.g. {["core.tools.esptool"] = "~/.xmake/addons/esp32/v1.0.0/modules"}
+--
+function addon.globalmodules()
+ local globalmodules = addon._GLOBALMODULES
+ if globalmodules == nil then
+ globalmodules = {}
+ for dirname, addoninfo in pairs(addon.addons()) do
+ for _, name in ipairs(addoninfo.globalmodules or {}) do
+ globalmodules[name] = path.join(addon.installdir(), dirname, addoninfo.version, "modules")
+ end
+ end
+ addon._GLOBALMODULES = globalmodules
+ end
+ return globalmodules
+end
+
+-- find the include files of the given addon reference, e.g. includes("@addon/esp32/board")
+--
+-- @param interp the interpreter which is loading the file, @see interpreter:includes_resolver_add
+-- @param reference the reference, e.g. "@addon/esp32/board", "@self/board"
+--
+-- @return the files, or nil and errors
+--
+function addon.find_includes(interp, reference)
+ if not addon.is_reference(reference, "/") then
+ return
+ end
+ local referenceinfo, errors = addon.resolve_reference(reference, "/", "includes", {scriptdir = interp:scriptdir()})
+ if not referenceinfo then
+ return nil, errors
+ end
+ local name = referenceinfo.name
+ local files
+ if name:endswith(".lua") then
+ files = os.files(path.join(referenceinfo.dir, name))
+ else
+ files = os.files(path.join(referenceinfo.dir, name, "xmake.lua"))
+ end
+
+ -- the addon is installed, but it does not provide this file, we cannot ignore it
+ if not files or #files == 0 then
+ os.raise("includes(%s) not found!", reference)
+ end
+ return files
+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 _, payloadinfo in ipairs(addon.payloadinfos(kind)) do
+ table.insert(payloads, payloadinfo.dir)
+ end
+ return payloads
+end
+
+-- get the payload information of the given kind from all installed addons
+--
+-- @param kind the payload kind, e.g. "plugins", "rules"
+-- @return the payload infos, e.g. {{name = "hello-world", version = "latest", dir = "~/.xmake/addons/hello-world/latest/plugins"}}
+--
+function addon.payloadinfos(kind)
+ local payloadinfos = {}
+ for name, addoninfo in table.orderpairs(addon.addons()) do
+ if table.contains(addoninfo.payloads or {}, kind) then
+ table.insert(payloadinfos, {
+ name = name,
+ version = addoninfo.version,
+ dir = path.join(addon.installdir(), name, addoninfo.version, kind)})
+ end
+ end
+ return payloadinfos
+end
+
+-- get the payload root directory of the given addon source directory
+--
+-- an addon repository has its own files, e.g. tests, ci scripts and documents,
+-- so its payloads can be placed in the `src` subdirectory, and we only install them
+--
+-- e.g.
+-- esp32-devel/src/{plugins,rules,toolchains,templates} -- with the `src` layout
+-- hello-world/{plugins} -- without it, for the simple addons
+--
+-- @param sourcedir the addon source directory
+-- @return the payload root directory, it will be nil if no payload is found
+--
+function addon.payloadroot(sourcedir)
+
+ -- the addon can set its payload root directory explicitly, e.g. set_sourcedir("src")
+ local manifest = addon.manifest(sourcedir)
+ if manifest and manifest.sourcedir then
+ local payloadroot = path.join(sourcedir, manifest.sourcedir)
+ if #addon.payloads_of(payloadroot) > 0 then
+ return payloadroot
+ end
+ return
+ end
+
+ local srcdir = path.join(sourcedir, "src")
+ if #addon.payloads_of(srcdir) > 0 then
+ return srcdir
+ end
+ if #addon.payloads_of(sourcedir) > 0 then
+ return sourcedir
+ end
+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 sourcedir = os.curdir()
+
+ -- the addon name is its identity, e.g. the install directory, the registry key
+ -- and the `@addon/<name>/xxx` references, so the package must be distributed with the same name
+ local manifest, errors = addon.manifest(sourcedir)
+ if errors then
+ os.raise(errors)
+ end
+ if manifest and addon.dirname(manifest.name) ~= addon.dirname(package:name()) then
+ os.raise("addon(%s) does not match the package name(%s) in the repository!\nplease fix the package recipe or the addon manifest.",
+ manifest.name, package:name())
+ end
+
+ local payloadroot = addon.payloadroot(sourcedir)
+ if not payloadroot then
+ os.raise("addon(%s): no payload directory found, e.g. plugins!", package:name())
+ end
+ for _, payloaddir in ipairs(addon.payloads_of(payloadroot)) do
+ os.cp(path.join(payloadroot, payloaddir), package:installdir())
+ end
+
+ -- we need not install the manifest, the package manifest(manifest.txt) and the addons
+ -- registry already have all the information, we just pass it to the registration
+ if manifest then
+ package:data_set("addon.manifest", manifest)
+ end
+ end
+end
+
+-- register the given installed addon
+--
+-- @param name the addon name
+-- @param version the addon version, e.g. "1.0.1", "latest"
+-- @param opt the options, e.g. {description = "...", deps = {"foo"}}
+--
+-- @note the addon manifest(addon.lua) is only read when installing, everything which
+-- is needed later is recorded here, so we never parse it again
+--
+-- @return true or false and errors
+--
+function addon.register(name, version, opt)
+ opt = opt or {}
+ local dirname = addon.dirname(name)
+ local addondir = path.join(addon.installdir(), dirname, version)
+ local addoninfo = {version = version,
+ -- we need to keep the raw name, the directory name is only its
+ -- normalized form, e.g. "myns::foo" -> "myns_foo"
+ 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
+ manifest_deps = opt.manifest_deps,
+ globalmodules = opt.globalmodules,
+ payloads = addon.payloads_of(addondir),
+ plugins = addon._plugins_of(addondir),
+ templates = addon._templates_of(addondir)}
+
+ -- we need to check the conflicts of the plugins and templates first,
+ -- they are not namespaced and we do not know which one will be used
+ local errors = addon._check_conflicts(dirname, addoninfo)
+ if errors then
+ return false, errors
+ end
+
+ -- 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
+
+-- remove the given installed addon
+--
+-- @param name the addon name
+-- @return true or false and errors
+--
+function addon.remove(name, opt)
+ opt = opt or {}
+ local dirname = addon.dirname(name)
+ local installdir = path.join(addon.installdir(), dirname)
+ if not os.isdir(installdir) then
+ return false, string.format("addon(%s) not found!", name)
+ end
+
+ -- we cannot remove it if the other addons depend on it
+ if not opt.force then
+ local parents = addon._parents(name)
+ if parents then
+ return false, string.format("addon(%s) cannot be removed, it's depended on by the addon(%s)!\nplease remove them first, or pass --force to remove it anyway",
+ name, table.concat(parents, ", "))
+ end
+ end
+ -- we need to remove the symlinks first, we cannot remove them recursively,
+ -- otherwise the linked files would be removed too
+ --
+ -- e.g. the user may link the install directory to the addon source directory when developing it
+ for _, versiondir in ipairs(os.dirs(path.join(installdir, "*"))) do
+ if os.islink(versiondir) then
+ os.rmfile(versiondir)
+ end
+ end
+ if os.islink(installdir) then
+ return os.rmfile(installdir)
+ end
+
+ local ok, errors = os.rm(installdir)
+ if not ok then
+ return false, errors
+ end
+ addon.unregister(name)
+ 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
+
+-- return module
+return addon