summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2026-04-20 17:35:48 +0800
committerGitHub <[email protected]>2026-04-20 17:35:48 +0800
commit55f8d448564f7cc198edb4db506ec7c18ca8f0f6 (patch)
tree6822b239d43091d20037a99b6ffa9fd7e449f1db
parent3a1b4e1719ba50b259afa27b450f06c39548003d (diff)
parent4e3f6b16db88b9c1afad898a7f5dfeaecdfd0f27 (diff)
Merge pull request #7490 from xmake-io/show
Export target dependency graph as json/dot
-rw-r--r--tests/plugins/show/src/core.c1
-rw-r--r--tests/plugins/show/src/main.c1
-rw-r--r--tests/plugins/show/src/net.c1
-rw-r--r--tests/plugins/show/src/ui.c1
-rw-r--r--tests/plugins/show/test.lua81
-rw-r--r--tests/plugins/show/xmake.lua20
-rw-r--r--xmake/actions/require/xmake.lua4
-rw-r--r--xmake/modules/private/action/require/depgraph.lua54
-rw-r--r--xmake/modules/private/xrepo/action/info.lua4
-rw-r--r--xmake/plugins/show/info/basic.lua13
-rw-r--r--xmake/plugins/show/info/depgraph.lua159
-rw-r--r--xmake/plugins/show/info/target.lua20
-rw-r--r--xmake/plugins/show/list.lua12
-rw-r--r--xmake/plugins/show/main.lua22
-rw-r--r--xmake/plugins/show/xmake.lua20
15 files changed, 377 insertions, 36 deletions
diff --git a/tests/plugins/show/src/core.c b/tests/plugins/show/src/core.c
new file mode 100644
index 000000000..87a413095
--- /dev/null
+++ b/tests/plugins/show/src/core.c
@@ -0,0 +1 @@
+int core(void) { return 0; }
diff --git a/tests/plugins/show/src/main.c b/tests/plugins/show/src/main.c
new file mode 100644
index 000000000..78f2de106
--- /dev/null
+++ b/tests/plugins/show/src/main.c
@@ -0,0 +1 @@
+int main(void) { return 0; }
diff --git a/tests/plugins/show/src/net.c b/tests/plugins/show/src/net.c
new file mode 100644
index 000000000..abde7c554
--- /dev/null
+++ b/tests/plugins/show/src/net.c
@@ -0,0 +1 @@
+int net(void) { return 0; }
diff --git a/tests/plugins/show/src/ui.c b/tests/plugins/show/src/ui.c
new file mode 100644
index 000000000..c15794f4d
--- /dev/null
+++ b/tests/plugins/show/src/ui.c
@@ -0,0 +1 @@
+int ui(void) { return 0; }
diff --git a/tests/plugins/show/test.lua b/tests/plugins/show/test.lua
new file mode 100644
index 000000000..dea98c204
--- /dev/null
+++ b/tests/plugins/show/test.lua
@@ -0,0 +1,81 @@
+import("core.base.json")
+
+function test_depgraph_json(t)
+ local outdata = os.iorunv("xmake", {"show", "--info=depgraph", "--json"})
+ local graph = json.decode(outdata)
+
+ t:are_equal(graph.root_targets, {"app"})
+ t:require(#graph.targets == 4)
+
+ local entries = {}
+ for _, target in ipairs(graph.targets) do
+ entries[target.name] = target
+ end
+ t:are_equal(entries.core.deps, {})
+ t:are_equal(entries.ui.deps, {"core"})
+ t:are_equal(entries["ext::net"].deps, {"core"})
+ t:are_equal(entries.app.deps, {"core", "ui", "ext::net"})
+end
+
+function test_depgraph_json_for_single_target(t)
+ local outdata = os.iorunv("xmake", {"show", "--info=depgraph", "--target=app", "--json"})
+ local graph = json.decode(outdata)
+
+ t:are_equal(graph.root_targets, {"app"})
+ t:require(#graph.targets == 4)
+
+ local names = {}
+ for _, target in ipairs(graph.targets) do
+ table.insert(names, target.name)
+ end
+ t:require(table.contains(names, "core"))
+ t:require(table.contains(names, "ui"))
+ t:require(table.contains(names, "ext::net"))
+ t:require(table.contains(names, "app"))
+end
+
+function test_depgraph_json_namespace(t)
+ local outdata = os.iorunv("xmake", {"show", "--info=depgraph", "--target=ext::net", "--json"})
+ local graph = json.decode(outdata)
+
+ t:are_equal(graph.root_targets, {"ext::net"})
+ t:require(#graph.targets == 2)
+
+ local entries = {}
+ for _, target in ipairs(graph.targets) do
+ entries[target.name] = target
+ end
+ t:require(entries["ext::net"])
+ t:are_equal(entries["ext::net"].deps, {"core"})
+ t:are_equal(entries.core.deps, {})
+end
+
+function test_depgraph_tree(t)
+ local outdata = os.iorunv("xmake", {"show", "--info=depgraph"})
+ t:require(outdata:find("app", 1, true))
+ t:require(outdata:find("core", 1, true))
+ t:require(outdata:find("ui", 1, true))
+ t:require(outdata:find("ext::net", 1, true))
+ t:require(outdata:find("|-- ", 1, true))
+ t:require(outdata:find("\\-- ", 1, true))
+end
+
+function test_depgraph_dot(t)
+ local outdata = os.iorunv("xmake", {"show", "--info=depgraph", "--format=dot"})
+ t:require(outdata:find("digraph {", 1, true))
+ t:require(outdata:find('"core"', 1, true))
+ t:require(outdata:find('"ui" -> "core"', 1, true))
+ t:require(outdata:find('"app" -> "core"', 1, true))
+ t:require(outdata:find('"app" -> "ui"', 1, true))
+ t:require(outdata:find('"ext::net" -> "core"', 1, true))
+ t:require(outdata:find('"app" -> "ext::net"', 1, true))
+ t:require(outdata:find("}", 1, true))
+end
+
+function test_depgraph_dot_for_single_target(t)
+ local outdata = os.iorunv("xmake", {"show", "--info=depgraph", "--target=ui", "--format=dot"})
+ t:require(outdata:find("digraph {", 1, true))
+ t:require(outdata:find('"core"', 1, true))
+ t:require(outdata:find('"ui" -> "core"', 1, true))
+ t:require(not outdata:find('"app"', 1, true))
+end
diff --git a/tests/plugins/show/xmake.lua b/tests/plugins/show/xmake.lua
new file mode 100644
index 000000000..15344a72a
--- /dev/null
+++ b/tests/plugins/show/xmake.lua
@@ -0,0 +1,20 @@
+add_rules("mode.debug", "mode.release")
+
+target("core")
+ set_kind("static")
+ add_files("src/core.c")
+
+target("ui")
+ set_kind("static")
+ add_deps("core")
+ add_files("src/ui.c")
+
+target("ext::net")
+ set_kind("static")
+ add_deps("core")
+ add_files("src/net.c")
+
+target("app")
+ set_kind("binary")
+ add_deps("core", "ui", "ext::net")
+ add_files("src/main.c")
diff --git a/xmake/actions/require/xmake.lua b/xmake/actions/require/xmake.lua
index 3a0b15802..e1596da52 100644
--- a/xmake/actions/require/xmake.lua
+++ b/xmake/actions/require/xmake.lua
@@ -74,8 +74,8 @@ task("require")
"e.g.",
" $ xmake require --info --format=json zlib",
" $ xmake require --depgraph --format=dot libpng",
- "values: json (for --info/--depgraph), tree/dot (for --depgraph only)",
- values = {"tree", "json", "dot"} }
+ "values: json (for --info/--depgraph), dot (for --depgraph only)",
+ values = {"plain", "json", "dot"} }
, {nil, "check", "k", nil, "Check whether the given package is supported",
"e.g.",
" $ xmake require --check tbox" }
diff --git a/xmake/modules/private/action/require/depgraph.lua b/xmake/modules/private/action/require/depgraph.lua
index ba1fb9ff4..e022bdee4 100644
--- a/xmake/modules/private/action/require/depgraph.lua
+++ b/xmake/modules/private/action/require/depgraph.lua
@@ -41,6 +41,7 @@ function _collect_package_entry(instance)
return {
name = instance:fullname(),
version = instance:version_str(),
+ configs_str = package.get_configs_str(instance),
deps = deps
}
end
@@ -74,11 +75,27 @@ function _collect_package_graph(instances)
}
end
+-- format version and configs as a suffix string for plain output
+-- e.g. " v1.3.2 [shared:y, debug:n]"
+function _format_package_suffix(entry)
+ local parts = {}
+ if entry.version then
+ table.insert(parts, entry.version)
+ end
+ if entry.configs_str and #entry.configs_str > 0 then
+ table.insert(parts, entry.configs_str)
+ end
+ if #parts > 0 then
+ return " " .. table.concat(parts, " ")
+ end
+ return ""
+end
+
-- print dependency tree recursively
--
-- e.g.
--- libpng
--- \-- zlib
+-- libpng v1.6.56
+-- \-- zlib v1.3.2
--
-- already expanded subtrees are marked with (*) to avoid duplication
--
@@ -91,11 +108,24 @@ function _print_dep_tree(packages_map, name, prefix, expanded)
local connector = is_last and "\\-- " or "|-- "
local next_prefix = prefix .. (is_last and " " or "| ")
local dep_entry = packages_map[dep]
- local dep_deps = dep_entry and dep_entry.deps or {}
- if expanded[dep] and #dep_deps > 0 then
- cprint("%s%s${color.dump.reference}%s${clear} ${dim}(*)${clear}", prefix, connector, dep)
+ local suffix = dep_entry and _format_package_suffix(dep_entry) or ""
+ if expanded[dep] then
+ -- already expanded, show with (*) and list direct children as references
+ local dep_deps = dep_entry and dep_entry.deps or {}
+ if #dep_deps > 0 then
+ cprint("%s%s${color.dump.reference}%s${clear}${dim}%s${clear}", prefix, connector, dep, suffix)
+ for j, subdep in ipairs(dep_deps) do
+ local sub_is_last = (j == #dep_deps)
+ local sub_connector = sub_is_last and "\\-- " or "|-- "
+ local sub_entry = packages_map[subdep]
+ local sub_suffix = sub_entry and _format_package_suffix(sub_entry) or ""
+ cprint("%s%s${dim}%s%s (*)${clear}", next_prefix, sub_connector, subdep, sub_suffix)
+ end
+ else
+ cprint("%s%s${color.dump.reference}%s${clear}${dim}%s (*)${clear}", prefix, connector, dep, suffix)
+ end
else
- cprint("%s%s${color.dump.reference}%s${clear}", prefix, connector, dep)
+ cprint("%s%s${color.dump.reference}%s${clear}${dim}%s${clear}", prefix, connector, dep, suffix)
_print_dep_tree(packages_map, dep, next_prefix, expanded)
end
end
@@ -109,7 +139,9 @@ function _print_package_graph(graph)
end
local expanded = {}
for _, root in ipairs(graph.root_packages) do
- cprint("${color.dump.string}%s${clear}", root)
+ local entry = packages_map[root]
+ local suffix = entry and _format_package_suffix(entry) or ""
+ cprint("${color.dump.string}%s${clear}${dim}%s${clear}", root, suffix)
_print_dep_tree(packages_map, root, "", expanded)
end
end
@@ -139,7 +171,7 @@ end
-- show the given package dependency graph
--
-- supported output formats (via --format):
--- tree (default): ASCII tree view
+-- plain (default): ASCII tree view
-- json: structured JSON output
-- dot: graphviz DOT format
--
@@ -164,8 +196,12 @@ function main(requires_raw)
local graph = _collect_package_graph(instances)
-- output in the specified format
- local format = option.get("format") or "tree"
+ local format = option.get("format") or "plain"
if format == "json" then
+ -- strip configs_str, it's only for plain display
+ for _, pkg in ipairs(graph.packages) do
+ pkg.configs_str = nil
+ end
print(json.encode(graph, {pretty = true, orderkeys = true}))
elseif format == "dot" then
_print_dot_graph(graph)
diff --git a/xmake/modules/private/xrepo/action/info.lua b/xmake/modules/private/xrepo/action/info.lua
index d7420533d..0a9c1c514 100644
--- a/xmake/modules/private/xrepo/action/info.lua
+++ b/xmake/modules/private/xrepo/action/info.lua
@@ -49,8 +49,8 @@ function menu_options()
"e.g.",
" - xrepo info --format=json zlib",
" - xrepo info --depgraph --format=dot libpng",
- "values: json (for --info/--depgraph), tree/dot (for --depgraph only)",
- values = {"tree", "json", "dot"}},
+ "values: json (for --info/--depgraph), dot (for --depgraph only)",
+ values = {"plain", "json", "dot"}},
{},
{nil, "packages", "vs", nil, "The packages list.",
"e.g.",
diff --git a/xmake/plugins/show/info/basic.lua b/xmake/plugins/show/info/basic.lua
index 4ad14fe98..8a29dc2c0 100644
--- a/xmake/plugins/show/info/basic.lua
+++ b/xmake/plugins/show/info/basic.lua
@@ -128,8 +128,13 @@ function main()
config.load()
+ -- support --format=json, with --json/--pretty backward compatibility
+ local format = option.get("format") or "plain"
+ if format == "plain" and option.get("json") then
+ format = "json"
+ end
local opt = {
- json = option.get("json"),
+ json = format == "json",
pretty = option.get("pretty")
}
local result = opt.json and {} or nil
@@ -138,9 +143,9 @@ function main()
result = _show_project_info(opt, result)
if opt.json then
- local json_opt
- if opt.pretty then
- json_opt = {pretty = true, orderkeys = true}
+ local json_opt = {pretty = true, orderkeys = true}
+ if option.get("json") and not option.get("pretty") then
+ json_opt = nil
end
print(json.encode(result or {}, json_opt))
end
diff --git a/xmake/plugins/show/info/depgraph.lua b/xmake/plugins/show/info/depgraph.lua
new file mode 100644
index 000000000..2c73f3abc
--- /dev/null
+++ b/xmake/plugins/show/info/depgraph.lua
@@ -0,0 +1,159 @@
+--!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 depgraph.lua
+--
+
+-- imports
+import("core.base.option")
+import("core.base.json")
+import("core.project.config")
+import("core.project.project")
+import("private.detect.check_targetname")
+
+function _collect_target_entry(target)
+ local deps = {}
+ for _, depname in ipairs(table.wrap(target:get("deps"))) do
+ local dep = target:dep(depname)
+ if dep then
+ table.insert(deps, dep:fullname())
+ end
+ end
+ json.mark_as_array(deps)
+ return {
+ name = target:fullname(),
+ deps = deps
+ }
+end
+
+function _collect_target_graph(root_target)
+ local targets = {}
+ local selected = {}
+ if root_target then
+ selected[root_target:fullname()] = true
+ for _, dep in ipairs(root_target:orderdeps() or {}) do
+ selected[dep:fullname()] = true
+ end
+ end
+
+ for _, target in ipairs(project.ordertargets()) do
+ if not root_target or selected[target:fullname()] then
+ table.insert(targets, _collect_target_entry(target))
+ end
+ end
+
+ local roots = {}
+ if root_target then
+ table.insert(roots, root_target:fullname())
+ else
+ local indegrees = {}
+ for _, target in ipairs(targets) do
+ indegrees[target.name] = 0
+ end
+ for _, target in ipairs(targets) do
+ for _, depname in ipairs(target.deps) do
+ if indegrees[depname] ~= nil then
+ indegrees[depname] = indegrees[depname] + 1
+ end
+ end
+ end
+ for _, target in ipairs(targets) do
+ if indegrees[target.name] == 0 then
+ table.insert(roots, target.name)
+ end
+ end
+ end
+
+ json.mark_as_array(roots)
+ return {
+ root_targets = roots,
+ targets = targets
+ }
+end
+
+function _print_dep_tree(targets_map, name, prefix, expanded)
+ expanded[name] = true
+ local entry = targets_map[name]
+ local deps = entry and entry.deps or {}
+ for i, dep in ipairs(deps) do
+ local is_last = (i == #deps)
+ local connector = is_last and "\\-- " or "|-- "
+ local next_prefix = prefix .. (is_last and " " or "| ")
+ local dep_entry = targets_map[dep]
+ local dep_deps = dep_entry and dep_entry.deps or {}
+ if expanded[dep] and #dep_deps > 0 then
+ cprint("%s%s${color.dump.reference}%s${clear} ${dim}(*)${clear}", prefix, connector, dep)
+ else
+ cprint("%s%s${color.dump.reference}%s${clear}", prefix, connector, dep)
+ _print_dep_tree(targets_map, dep, next_prefix, expanded)
+ end
+ end
+end
+
+function _print_target_graph(graph)
+ local targets_map = {}
+ for _, target in ipairs(graph.targets) do
+ targets_map[target.name] = target
+ end
+ local expanded = {}
+ for _, root in ipairs(graph.root_targets) do
+ cprint("${color.dump.string}%s${clear}", root)
+ _print_dep_tree(targets_map, root, "", expanded)
+ end
+end
+
+function _print_dot_graph(graph)
+ print("digraph {")
+ for _, target in ipairs(graph.targets) do
+ if #target.deps == 0 then
+ print(string.format(" \"%s\"", target.name))
+ else
+ for _, dep in ipairs(target.deps) do
+ print(string.format(" \"%s\" -> \"%s\"", target.name, dep))
+ end
+ end
+ end
+ print("}")
+end
+
+function main(name)
+ config.load()
+
+ local root_target
+ if name then
+ root_target = assert(check_targetname(name))
+ end
+
+ local graph = _collect_target_graph(root_target)
+
+ -- support --format=json/dot/plain, with --json/--pretty backward compatibility
+ local format = option.get("format") or "plain"
+ if format == "plain" and option.get("json") then
+ format = "json"
+ end
+ if format == "json" then
+ local json_opt = {pretty = true, orderkeys = true}
+ if option.get("json") and not option.get("pretty") then
+ json_opt = nil
+ end
+ print(json.encode(graph, json_opt))
+ elseif format == "dot" then
+ _print_dot_graph(graph)
+ else
+ _print_target_graph(graph)
+ end
+end
diff --git a/xmake/plugins/show/info/target.lua b/xmake/plugins/show/info/target.lua
index e63a51ea7..75f31f9a5 100644
--- a/xmake/plugins/show/info/target.lua
+++ b/xmake/plugins/show/info/target.lua
@@ -363,18 +363,22 @@ function main(name)
-- get target
config.load()
- local opt = {
- json = option.get("json"),
- pretty = option.get("pretty")
- }
+
+ -- support --format=json, with --json/--pretty backward compatibility
+ local format = option.get("format") or "plain"
+ if format == "plain" and option.get("json") then
+ format = "json"
+ end
+
+ assert(name, "please specify the target name, e.g. xmake show --info=target --target=xxx")
local target = assert(check_targetname(name))
local info = _collect_target_info(target)
- if opt.json then
+ if format == "json" then
info.api_entries = nil
- local json_opt
- if opt.pretty then
- json_opt = {pretty = true, orderkeys = true}
+ local json_opt = {pretty = true, orderkeys = true}
+ if option.get("json") and not option.get("pretty") then
+ json_opt = nil
end
print(json.encode(info or {}, json_opt))
else
diff --git a/xmake/plugins/show/list.lua b/xmake/plugins/show/list.lua
index fb2914964..301048ce2 100644
--- a/xmake/plugins/show/list.lua
+++ b/xmake/plugins/show/list.lua
@@ -29,3 +29,15 @@ function lists()
end
return values
end
+
+-- get all info values
+function infos()
+ local values = {}
+ for _, filepath in ipairs(os.files(path.join(os.scriptdir(), "info", "*.lua"))) do
+ local name = path.basename(filepath)
+ if name ~= "basic" and name ~= "target" then
+ table.insert(values, name)
+ end
+ end
+ return values
+end
diff --git a/xmake/plugins/show/main.lua b/xmake/plugins/show/main.lua
index 9dc4f519a..7e04b7a43 100644
--- a/xmake/plugins/show/main.lua
+++ b/xmake/plugins/show/main.lua
@@ -32,14 +32,20 @@ function main()
local listname = option.get("list")
if listname then
return _show_list(listname)
- else
- -- show the information of the given object
- for _, filepath in ipairs(os.files(path.join(os.scriptdir(), "info", "*.lua"))) do
- local name = path.basename(filepath)
- if option.get(name) then
- local show_info = assert(import("info." .. name, {try = true, anonymous = true}), "unknown option name(%s)", name)
- return show_info(option.get(name))
- end
+ end
+
+ local infoname = option.get("info")
+ if infoname then
+ local show_info = assert(import("info." .. infoname, {try = true, anonymous = true}), "unknown info name(%s)", infoname)
+ return show_info(option.get("target"))
+ end
+
+ -- fallback to legacy options format
+ for _, filepath in ipairs(os.files(path.join(os.scriptdir(), "info", "*.lua"))) do
+ local name = path.basename(filepath)
+ if option.get(name) then
+ local show_info = assert(import("info." .. name, {try = true, anonymous = true}), "unknown option name(%s)", name)
+ return show_info(option.get(name))
end
end
diff --git a/xmake/plugins/show/xmake.lua b/xmake/plugins/show/xmake.lua
index 331aeb468..ef3ea0fe2 100644
--- a/xmake/plugins/show/xmake.lua
+++ b/xmake/plugins/show/xmake.lua
@@ -30,8 +30,23 @@ task("show")
return import("list").lists()
end},
{'g', "group", "kv", nil, "Filter targets by the given group name."},
- {nil, "json", "k", false, "Show information with json format."},
- {nil, "pretty", "k", false, "Enable pretty formatted json output."},
+ {nil, "json", "k", false, "Show information with json format (deprecated, use --format=json)."},
+ {nil, "pretty", "k", false, "Enable pretty formatted output."},
+ {nil, "format", "kv", nil, "Set the output format.",
+ "e.g.",
+ " - xmake show --format=json",
+ " - xmake show --info=depgraph --format=dot",
+ "values: json (for all), dot (for --info=depgraph only)",
+ values = {"plain", "json", "dot"}},
+ {'i', "info", "kv", nil, "Show the given information.",
+ "e.g.",
+ " - xmake show --info=depgraph",
+ " - xmake show --info=depgraph --target=app",
+ " - xmake show --info=depgraph --format=json",
+ " - xmake show --info=depgraph --format=dot",
+ values = function (complete, opt)
+ return import("list").infos()
+ end},
{'t', "target", "kv", nil, "Show the information of the given target.",
values = function (complete, opt)
return import("private.utils.complete_helper.targets")(complete, opt)
@@ -40,4 +55,3 @@ task("show")
}
-