Mike Frysinger | f1ba7ad | 2022-09-12 05:42:57 -0400 | [diff] [blame] | 1 | # Copyright 2010 The ChromiumOS Authors |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Generates pretty dependency graphs for Chrome OS packages.""" |
| 6 | |
| 7 | import json |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 8 | import os |
| 9 | import sys |
| 10 | |
Mike Frysinger | d6d3965 | 2016-09-01 00:27:59 -0400 | [diff] [blame] | 11 | from chromite.lib import commandline |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 12 | from chromite.lib import dot_helper |
| 13 | |
| 14 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 15 | NORMAL_COLOR = "black" |
| 16 | TARGET_COLOR = "red" |
| 17 | SEED_COLOR = "green" |
| 18 | CHILD_COLOR = "grey" |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 19 | |
| 20 | |
| 21 | def GetReverseDependencyClosure(full_name, deps_map): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 22 | """Gets the closure of the reverse dependencies of a node. |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 23 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 24 | Walks the tree along all the reverse dependency paths to find all the nodes |
| 25 | that transitively depend on the input node. |
| 26 | """ |
| 27 | s = set() |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 28 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 29 | def GetClosure(name): |
| 30 | s.add(name) |
| 31 | node = deps_map[name] |
| 32 | for dep in node["rev_deps"]: |
| 33 | if dep in s: |
| 34 | continue |
| 35 | GetClosure(dep) |
| 36 | |
| 37 | GetClosure(full_name) |
| 38 | return s |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 39 | |
| 40 | |
| 41 | def GetOutputBaseName(node, options): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 42 | """Gets the basename of the output file for a node.""" |
| 43 | return "%s_%s-%s.%s" % ( |
| 44 | node["category"], |
| 45 | node["name"], |
| 46 | node["version"], |
| 47 | options.format, |
| 48 | ) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 49 | |
| 50 | |
| 51 | def AddNodeToSubgraph(subgraph, node, options, color): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 52 | """Gets the dot definition for a node.""" |
| 53 | name = node["full_name"] |
| 54 | href = None |
| 55 | if options.link: |
| 56 | filename = GetOutputBaseName(node, options) |
| 57 | href = "%s%s" % (options.base_url, filename) |
| 58 | subgraph.AddNode(name, name, color, href) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 59 | |
| 60 | |
| 61 | def GenerateDotGraph(package, deps_map, options): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 62 | """Generates the dot source for the dependency graph leading to a node. |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 63 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 64 | The output is a list of lines. |
| 65 | """ |
| 66 | deps = GetReverseDependencyClosure(package, deps_map) |
| 67 | node = deps_map[package] |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 68 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 69 | # Keep track of all the emitted nodes so that we don't issue multiple |
| 70 | # definitions |
| 71 | emitted = set() |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 72 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 73 | graph = dot_helper.Graph(package) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 74 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 75 | # Add all the children if we want them, all of them in their own subgraph, |
| 76 | # as a sink. Keep the arcs outside of the subgraph though (it generates |
| 77 | # better layout). |
| 78 | children_subgraph = None |
| 79 | if options.children and node["deps"]: |
| 80 | children_subgraph = graph.AddNewSubgraph("sink") |
| 81 | for child in node["deps"]: |
| 82 | child_node = deps_map[child] |
| 83 | AddNodeToSubgraph( |
| 84 | children_subgraph, child_node, options, CHILD_COLOR |
| 85 | ) |
| 86 | emitted.add(child) |
| 87 | graph.AddArc(package, child) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 88 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 89 | # Add the package in its own subgraph. If we didn't have children, make it |
| 90 | # a sink |
| 91 | if children_subgraph: |
| 92 | rank = "same" |
| 93 | else: |
| 94 | rank = "sink" |
| 95 | package_subgraph = graph.AddNewSubgraph(rank) |
| 96 | AddNodeToSubgraph(package_subgraph, node, options, TARGET_COLOR) |
| 97 | emitted.add(package) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 98 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 99 | # Add all the other nodes, as well as all the arcs. |
| 100 | for dep in deps: |
| 101 | dep_node = deps_map[dep] |
| 102 | if not dep in emitted: |
| 103 | color = NORMAL_COLOR |
| 104 | if dep_node["action"] == "seed": |
| 105 | color = SEED_COLOR |
| 106 | AddNodeToSubgraph(graph, dep_node, options, color) |
| 107 | for j in dep_node["rev_deps"]: |
| 108 | graph.AddArc(j, dep) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 109 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 110 | return graph.Gen() |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 111 | |
| 112 | |
| 113 | def GenerateImages(data, options): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 114 | """Generate the output images for all the nodes in the input.""" |
| 115 | deps_map = json.loads(data) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 116 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 117 | for package in deps_map: |
| 118 | lines = GenerateDotGraph(package, deps_map, options) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 119 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 120 | filename = os.path.join( |
| 121 | options.output_dir, GetOutputBaseName(deps_map[package], options) |
| 122 | ) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 123 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 124 | save_dot_filename = None |
| 125 | if options.save_dot: |
| 126 | save_dot_filename = filename + ".dot" |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 127 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 128 | dot_helper.GenerateImage( |
| 129 | lines, filename, options.format, save_dot_filename |
| 130 | ) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 131 | |
| 132 | |
Mike Frysinger | d6d3965 | 2016-09-01 00:27:59 -0400 | [diff] [blame] | 133 | def GetParser(): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 134 | """Return a command line parser.""" |
| 135 | parser = commandline.ArgumentParser(description=__doc__) |
| 136 | parser.add_argument( |
| 137 | "-f", |
| 138 | "--format", |
| 139 | default="svg", |
| 140 | help="Dot output format (png, svg, etc.).", |
| 141 | ) |
| 142 | parser.add_argument( |
| 143 | "-o", "--output-dir", default=".", help="Output directory." |
| 144 | ) |
| 145 | parser.add_argument( |
| 146 | "-c", "--children", action="store_true", help="Also add children." |
| 147 | ) |
| 148 | parser.add_argument( |
| 149 | "-l", "--link", action="store_true", help="Embed links." |
| 150 | ) |
| 151 | parser.add_argument( |
| 152 | "-b", "--base-url", default="", help="Base url for links." |
| 153 | ) |
| 154 | parser.add_argument( |
| 155 | "-s", "--save-dot", action="store_true", help="Save dot files." |
| 156 | ) |
| 157 | parser.add_argument("inputs", nargs="*", help="Chromium OS package lists") |
| 158 | return parser |
Mike Frysinger | d6d3965 | 2016-09-01 00:27:59 -0400 | [diff] [blame] | 159 | |
| 160 | |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 161 | def main(argv): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 162 | parser = GetParser() |
| 163 | options = parser.parse_args(argv) |
| 164 | options.Freeze() |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 165 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 166 | try: |
| 167 | os.makedirs(options.output_dir) |
| 168 | except OSError: |
| 169 | # The directory already exists. |
| 170 | pass |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 171 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 172 | if not options.inputs: |
| 173 | GenerateImages(sys.stdin.read(), options) |
| 174 | else: |
| 175 | for i in options.inputs: |
Mike Frysinger | 31fdddd | 2023-02-24 15:50:55 -0500 | [diff] [blame^] | 176 | with open(i, encoding="utf-8") as handle: |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 177 | GenerateImages(handle.read(), options) |