summaryrefslogtreecommitdiff
path: root/xmake/core/project
diff options
context:
space:
mode:
authorruki <[email protected]>2026-08-16 23:43:27 +0800
committerGitHub <[email protected]>2026-08-16 23:43:27 +0800
commite180e51cfb91f59ea6b4912e4d7bc052f5ef5add (patch)
tree637ba32123a35fbfec26a7b0c977a9c65b7e05fe /xmake/core/project
parentc3debec81c555e5b39c81c4e39e1b9737041a650 (diff)
parentd7fc1e30a747c0457b4f45194946fcff747c76db (diff)
Merge pull request #7714 from xmake-io/addon
move add_addons to xmake.lua
Diffstat (limited to 'xmake/core/project')
-rw-r--r--xmake/core/project/addons.lua110
-rw-r--r--xmake/core/project/project.lua104
2 files changed, 79 insertions, 135 deletions
diff --git a/xmake/core/project/addons.lua b/xmake/core/project/addons.lua
index 169b7d033..c979a6f8d 100644
--- a/xmake/core/project/addons.lua
+++ b/xmake/core/project/addons.lua
@@ -29,23 +29,6 @@ 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", "serial-tools")
--- 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,
@@ -63,100 +46,29 @@ 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
+-- check the addons which a project declares, e.g. add_addons("esp32-devel 1.0.x")
--
--- @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 = {...}}
+-- @return true, or false and errors
--
-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
+function addons.validate(requires)
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
-
+ for _, requirestr in ipairs(requires) do
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)
+ return false, string.format("add_addons(%s): the name is reserved by xmake for the addon references, please rename it!", name)
end
if name == "." or name == ".." or name:find("[/\\:]") then
- return nil, string.format("%s: invalid addon name(%s)!", filepath, name)
+ return false, string.format("add_addons(%s): invalid addon name!", 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)
+ return false, string.format("add_addons(%s): it is declared twice, e.g. `%s` and `%s`, please merge them!",
+ name, declared[name], requirestr)
end
declared[name] = requirestr
end
- return addonsinfo
+ return true
end
-- split the given declaration into the name and the version range
@@ -204,13 +116,13 @@ end
-- @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)
+function addons.satisfied(requires, projectdir)
local locked = addons.locked(projectdir)
if not locked then
return false
end
local installed = addon.addons()
- for _, requirestr in ipairs(addonsinfo.addons) do
+ for _, requirestr in ipairs(requires) do
local name = addons.requirename(requirestr)
local lockinfo = locked[name]
if not addons.locked_valid(requirestr, lockinfo) then
diff --git a/xmake/core/project/project.lua b/xmake/core/project/project.lua
index 0cda89db1..b92c5cfab 100644
--- a/xmake/core/project/project.lua
+++ b/xmake/core/project/project.lua
@@ -234,14 +234,15 @@ end
-- @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()
+function project._install_addons(rootinfo)
-- @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()
+ local result = project._ADDONS_RESULT
+ if result == nil then
+ result = project._do_install_addons(rootinfo)
+ project._ADDONS_RESULT = result
end
- return project._ADDONS_OK, project._ADDONS_ERRORS
+ return result
end
-- activate the addon versions which this project locks
@@ -263,33 +264,45 @@ function project._pin_addons()
end
-- do install the addons which this project declares
-function project._do_install_addons()
+-- install the addons which this project declares, e.g. add_addons("esp32-devel 1.0.x")
+--
+-- @return the result, e.g. {ok = true, installed = true}, {ok = false, errors = ".."}
+--
+function project._do_install_addons(rootinfo)
-- this project declares nothing?
- local addonsinfo, errors = addons.load()
- if errors then
- return false, errors
+ local requires = table.wrap(rootinfo:get("addons"))
+ if #requires == 0 then
+ return {ok = true}
end
- if not addonsinfo or #addonsinfo.addons == 0 then
- return true
+ local ok, errors = addons.validate(requires)
+ if not ok then
+ return {ok = false, errors = errors}
end
-- they have been installed already?
- project._pin_addons()
- if addons.satisfied(addonsinfo) then
- return true
+ if addons.satisfied(requires) then
+ return {ok = 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, ", "))
+ utils.cprint("${color.warning}note: ${clear}this project needs the addons(${bright}%s${clear}), installing them ..",
+ table.concat(requires, ", "))
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
+ -- we pass the declarations to the installer, it must not load this project again,
+ -- @see xmake/modules/private/action/addon/impl/install_addons.lua
+ local datafile = os.tmpfile()
+ local ok, errors = io.save(datafile, {addons = requires, repositories = table.wrap(rootinfo:get("repositories"))})
+ if not ok then
+ return {ok = false, errors = errors}
+ end
+
-- @note we run it in a working directory which has no project, @see addon.workdir(),
-- otherwise it would load this project again
--
@@ -310,9 +323,11 @@ function project._do_install_addons()
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!"
+ table.insert(argv, datafile)
+ local exitcode, errors = os.execv(os.programfile(), argv, {curdir = addon.workdir()})
+ os.rm(datafile)
+ if exitcode ~= 0 then
+ return {ok = false, errors = 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
@@ -320,7 +335,7 @@ function project._do_install_addons()
project._pin_addons()
rule.clear()
task.clear()
- return true
+ return {ok = true, installed = true}
end
-- load the project file
@@ -329,26 +344,13 @@ end
-- - 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
+-- - addons_installed: the addons have been installed, we are loading it again
--
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 opt.skip_addons then
- -- we do not install them here, but we still need to use the locked versions
- project._pin_addons()
- else
- local ok, errors = project._install_addons()
- if not ok then
- return false, errors
- end
- end
+ -- use the locked versions of the addons which this project declares
+ project._pin_addons()
-- has already been loaded?
if project._memcache():get("rootinfo") and not opt.force then
@@ -364,6 +366,12 @@ function project._load(opt)
-- get interpreter
local interp = project.interpreter()
+ -- this project declares the addons which it needs, e.g. add_addons("esp32-devel"),
+ -- but we can only know them after loading it, so this pass must survive the references
+ -- of the addons which are not installed yet, and we load it again after installing them,
+ -- e.g. includes("@addon/esp32-devel/board")
+ interp:includes_unresolved_set(not opt.addons_installed)
+
-- load script
local ok, errors = interp:load(project.rootfile(), {on_load_data = function (data)
for _, xmakerc_file in ipairs(project.rcfiles()) do
@@ -386,6 +394,23 @@ function project._load(opt)
return false, errors
end
+ -- install the addons which this project declares, and then load it again with them
+ --
+ -- @note we do not install them for the option menu, it merges the project tasks in a
+ -- best-effort way and every command builds it, @see project._load_tasks()
+ --
+ if not opt.skip_addons and not opt.addons_installed then
+ local result = project._install_addons(rootinfo)
+ if not result.ok then
+ os.cd(oldir)
+ return false, result.errors
+ end
+ if result.installed then
+ os.cd(oldir)
+ return project._load({force = true, disable_filter = opt.disable_filter, addons_installed = true})
+ end
+ end
+
-- load the root info of the target
local rootinfo_target, errors = project._load_scope("root.target", true, not opt.disable_filter)
if not rootinfo_target then
@@ -780,6 +805,9 @@ function project.apis()
, "add_requires"
, "add_requireconfs"
, "add_repositories"
+ -- the addons which this project needs, they are installed automatically,
+ -- e.g. add_addons("esp32-devel 1.0.x"), @see core/project/addons.lua
+ , "add_addons"
}
, paths =
{
@@ -832,6 +860,10 @@ function project.interpreter()
-- set root scope
interp:rootscope_set("target")
+ -- the project file can reference the includes files of the addons,
+ -- e.g. includes("@addon/esp32-devel/board")
+ interp:includes_resolver_add(addon.find_includes)
+
-- define apis for rule
interp:api_define(rule.apis())