summaryrefslogtreecommitdiff
path: root/xmake/modules/private/action/require/depgraph.lua
blob: e022bdee457d2471fca44e86bd1f23df4f604976 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
--!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.task")
import("core.base.option")
import("core.base.json")
import("core.project.project")
import("private.action.require.impl.package")
import("private.action.require.impl.repository")
import("private.action.require.impl.environment")
import("private.action.require.impl.utils.get_requires")

-- collect single package entry with its direct dependencies
function _collect_package_entry(instance)
    local deps = {}
    local plaindeps = instance:plaindeps()
    if plaindeps then
        for _, dep in ipairs(plaindeps) do
            table.insert(deps, dep:fullname())
        end
    end
    json.mark_as_array(deps)
    return {
        name = instance:fullname(),
        version = instance:version_str(),
        configs_str = package.get_configs_str(instance),
        deps = deps
    }
end

-- collect the full dependency graph from loaded package instances
--
-- returns a table with:
--   root_packages: packages that are not depended on by others
--   packages: all package entries with their direct deps
--
function _collect_package_graph(instances)
    local targets = {}
    local roots = {}
    local all_deps = {}
    for _, instance in ipairs(instances) do
        local entry = _collect_package_entry(instance)
        table.insert(targets, entry)
        for _, dep in ipairs(entry.deps) do
            all_deps[dep] = true
        end
    end
    for _, entry in ipairs(targets) do
        if not all_deps[entry.name] then
            table.insert(roots, entry.name)
        end
    end
    json.mark_as_array(roots)
    return {
        root_packages = roots,
        packages = targets
    }
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 v1.6.56
--   \-- zlib v1.3.2
--
-- already expanded subtrees are marked with (*) to avoid duplication
--
function _print_dep_tree(packages_map, name, prefix, expanded)
    expanded[name] = true
    local entry = packages_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 = packages_map[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}${dim}%s${clear}", prefix, connector, dep, suffix)
            _print_dep_tree(packages_map, dep, next_prefix, expanded)
        end
    end
end

-- print the package dependency graph as a tree
function _print_package_graph(graph)
    local packages_map = {}
    for _, pkg in ipairs(graph.packages) do
        packages_map[pkg.name] = pkg
    end
    local expanded = {}
    for _, root in ipairs(graph.root_packages) do
        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

-- print the package dependency graph in graphviz DOT format
--
-- e.g.
--   digraph {
--       "zlib"
--       "libpng" -> "zlib"
--   }
--
function _print_dot_graph(graph)
    print("digraph {")
    for _, pkg in ipairs(graph.packages) do
        if #pkg.deps == 0 then
            print(string.format("    \"%s\"", pkg.name))
        else
            for _, dep in ipairs(pkg.deps) do
                print(string.format("    \"%s\" -> \"%s\"", pkg.name, dep))
            end
        end
    end
    print("}")
end

-- show the given package dependency graph
--
-- supported output formats (via --format):
--   plain (default): ASCII tree view
--   json: structured JSON output
--   dot:  graphviz DOT format
--
function main(requires_raw)

    -- get requires and extra config
    local requires, requires_extra = get_requires(requires_raw)
    if not requires or #requires == 0 then
        return
    end

    -- enter environment
    environment.enter()

    -- pull all repositories first if not exists
    if not repository.pulled() then
        task.run("repo", {update = true})
    end

    -- load all packages and collect dependency graph
    local instances = package.load_packages(requires, {requires_extra = requires_extra})
    local graph = _collect_package_graph(instances)

    -- output in the specified format
    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)
    else
        _print_package_graph(graph)
    end

    -- leave environment
    environment.leave()
end