summaryrefslogtreecommitdiff
path: root/support/scripts/graph-depends
blob: d5ef6e09dbd5ce2431de88008ed794710dd4ab3d (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
#!/usr/bin/python

# Usage (the graphviz package must be installed in your distribution)
#  ./scripts/graph-depends [package-name] > test.dot
#  dot -Tpdf test.dot -o test.pdf
#
# With no arguments, graph-depends will draw a complete graph of
# dependencies for the current configuration. With an argument,
# graph-depends will draw a graph of dependencies for the given
# package name.
#
# Limitations
#
#  * Some packages have dependencies that depend on the Buildroot
#    configuration. For example, many packages have a dependency on
#    openssl if openssl has been enabled. This tool will graph the
#    dependencies as they are with the current Buildroot
#    configuration.
#
#  * The X.org package definitions are only included when
#    BR2_PACKAGE_XORG7 is enabled, so if this option is not enabled,
#    it isn't possible to graph the dependencies of X.org stack
#    components.
#
# Copyright (C) 2010 Thomas Petazzoni <thomas.petazzoni@free-electrons.com>

import sys
import subprocess

# In FULL_MODE, we draw the full dependency graph for all selected
# packages
FULL_MODE = 1

# In PKG_MODE, we only draw the dependency graph for a given package
PKG_MODE  = 2

mode = 0

if len(sys.argv) == 1:
    mode = FULL_MODE
elif len(sys.argv) == 2:
    mode = PKG_MODE
    rootpkg  = sys.argv[1]
else:
    print "Usage: graph-depends [package-name]"
    sys.exit(1)

allpkgs = []
unknownpkgs = []

# Execute the "make show-targets" command to get the list of the main
# Buildroot TARGETS and return it formatted as a Python list. This
# list is used as the starting point for full dependency graphs
def get_targets():
    sys.stderr.write("Getting targets\n")
    cmd = ["make", "-s", "show-targets"]
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    output = p.communicate()[0].strip()
    if p.returncode != 0:
        return None
    if output == '':
        return []
    return output.split(' ')

# Execute the "make <pkg>-show-depends" command to get the list of
# dependencies of a given package, and return the list of dependencies
# formatted as a Python list.
def get_depends(pkg):
    sys.stderr.write("Getting dependencies for %s\n" % pkg)
    cmd = ["make", "-s", "%s-show-depends" % pkg]
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    output = p.communicate()[0].strip()
    if p.returncode != 0:
        return None
    if output == '':
        return []
    return output.split(' ')

# Recursive function that builds the tree of dependencies for a given
# package. The dependencies are built in a list called 'dependencies',
# which contains tuples of the form (pkg1 ->
# pkg2_on_which_pkg1_depends) and the function finally returns this
# list.
def get_all_depends(pkg):
    dependencies = []

    # We already have the dependencies for this package
    if pkg in allpkgs:
        return
    allpkgs.append(pkg)
    depends = get_depends(pkg)

    # We couldn't get the dependencies of this package, because it
    # doesn't use the generic or autotools infrastructure. Add it to
    # unknownpkgs so that it is later rendered in red color to warn
    # the user.
    if depends == None:
        unknownpkgs.append(pkg)
        return

    # This package has no dependency.
    if depends == []:
        return

    # Add dependencies to the list of dependencies
    for dep in depends:
        dependencies.append((pkg, dep))

    # Recurse into the dependencies
    for dep in depends:
        newdeps = get_all_depends(dep)
        if newdeps != None:
            dependencies += newdeps

    return dependencies

# The Graphviz "dot" utility doesn't like dashes in node names. So for
# node names, we strip all dashes.
def pkg_node_name(pkg):
    return pkg.replace("-","")

# Helper function for remove_redundant_deps(). This function tells
# whether package "pkg" is the dependency of another package that is
# not the special "all" package.
def has_redundant_deps(deps, pkg):
    for dep in deps:
        if dep[0] != "all" and dep[1] == pkg:
            return True
    return False

# This function removes redundant dependencies of the special "all"
# package. This "all" package is created to reflect the origin of the
# selection for all packages that are not themselves selected by any
# other package. So for example if you enable libpng, zlib is enabled
# as a dependency. But zlib being selected by libpng, it also appears
# as a dependency of the "all" package. This needlessly complicates
# the generated dependency graph. So when we have the dependency list
# (all -> zlib, all -> libpn, libpng -> zlib), we get rid of the 'all
# -> zlib' dependency, because zlib is already a dependency of a
# regular package.
def remove_redundant_deps(deps):
    newdeps = []
    for dep in deps:
        if dep[0] != "all":
            newdeps.append(dep)
            continue
        if not has_redundant_deps(deps, dep[1]):
            newdeps.append(dep)
            continue
        sys.stderr.write("Removing redundant dep all -> %s\n" % dep[1])
    return newdeps

TARGET_EXCEPTIONS = [
    "target-generic-issue",
    "target-generic-getty-busybox",
    "target-generic-do-remount-rw",
    "target-finalize",
    "erase-fakeroots",
    "target-generic-hostname",
]

# In full mode, start with the result of get_targets() to get the main
# targets and then use get_all_depends() for each individual target.
if mode == FULL_MODE:
    targets = get_targets()
    dependencies = []
    allpkgs.append('all')
    for tg in targets:
        # Skip uninteresting targets
        if tg in TARGET_EXCEPTIONS:
            continue
        dependencies.append(('all', tg))
        deps = get_all_depends(tg)
        if deps != None:
            dependencies += deps

# In pkg mode, start directly with get_all_depends() on the requested
# package
elif mode == PKG_MODE:
    dependencies = get_all_depends(rootpkg)

dependencies = remove_redundant_deps(dependencies)

# Start printing the graph data
print "digraph G {"

# First, the dependencies. Usage of set allows to remove duplicated
# dependencies in the graph
for dep in set(dependencies):
    print "%s -> %s" % (pkg_node_name(dep[0]), pkg_node_name(dep[1]))

# Then, the node attributes: color, style and label.
for pkg in allpkgs:
    if pkg == 'all':
        print "all [label = \"ALL\"]"
        print "all [color=lightblue,style=filled]"
        continue

    print "%s [label = \"%s\"]" % (pkg_node_name(pkg), pkg)

    if pkg in unknownpkgs:
        print "%s [color=red,style=filled]" % pkg_node_name(pkg)
    elif mode == PKG_MODE and pkg == rootpkg:
        print "%s [color=lightblue,style=filled]" % pkg_node_name(rootpkg)
    else:
        print "%s [color=grey,style=filled]" % pkg_node_name(pkg)

print "}"