blob: 63c73bef6509ebfe64614c89c5f92e533fc4bb11 [file]
#!/usr/bin/env python3
#
# 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.
#
# Authors:
# Chandan Singh <csingh43@bloomberg.net>
#
'''Print dependency graph of given element(s) in DOT format.
This script must be run from the same directory where you would normally
run `bst` commands.
When `--format` option is specified, the output will also be rendered in the
given format. A file with name `bst-graph.{format}` will be created in the same
directory. To use this option, you must have the `graphviz` command line tool
installed.
'''
import argparse
import subprocess
import re
import urllib.parse
from graphviz import Digraph
from ruamel.yaml import YAML
def parse_args():
'''Handle parsing of command line arguments.
Returns:
A argparse.Namespace object
'''
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'element', nargs='*',
help='Name of the element'
)
parser.add_argument(
'--format',
help='Redner the graph in given format (`pdf`, `png`, `svg` etc)'
)
parser.add_argument(
'--view', action='store_true',
help='Open the rendered graph with the default application'
)
return parser.parse_args()
def unique_node_name(s):
'''Generate unique node name for `s`.
Graphviz node names cannot contain colons or backslashes so we use
url-encoding to generate the unique node name. (A cryptographic hash could
be used instead but that would make the graphviz file less readable.)
Args:
s: element name
Returns:
A string containing the unique node name
'''
return urllib.parse.quote_plus(s)
def parse_graph(lines):
'''Return nodes and edges of the parsed grpah.
Args:
lines: List of lines in format 'NAME|BUILD-DEPS|RUNTIME-DEPS'
Returns:
Tuple of format (nodes,build_deps,runtime_deps)
Each member of build_deps and runtime_deps is also a tuple.
'''
parser = YAML(typ="safe")
nodes = set()
build_deps = set()
runtime_deps = set()
for line in lines:
line = line.strip()
if not line:
continue
# It is safe to split on '|' as it is not a valid character for
# element names.
name, build_dep, runtime_dep = line.split('|')
build_dep = parser.load(build_dep)
runtime_dep = parser.load(runtime_dep)
nodes.add(name)
[build_deps.add((name, dep)) for dep in build_dep if dep]
[runtime_deps.add((name, dep)) for dep in runtime_dep if dep]
return nodes, build_deps, runtime_deps
def generate_graph(nodes, build_deps, runtime_deps):
'''Generate graph from given nodes and edges.
Args:
nodes: set of nodes
build_deps: set of tuples of build depdencies
runtime_deps: set of tuples of runtime depdencies
Returns:
A graphviz.Digraph object
'''
graph = Digraph()
for name in nodes:
graph.node(unique_node_name(name), label=name)
for source, target in build_deps:
graph.edge(unique_node_name(source), unique_node_name(target), label='build-dep')
for source, target in runtime_deps:
graph.edge(unique_node_name(source), unique_node_name(target), label='runtime-dep')
return graph
def main():
args = parse_args()
cmd = ['bst', 'show', '--format', '%{name}|%{build-deps}|%{runtime-deps}||']
if 'element' in args:
cmd += args.element
graph_lines = subprocess.check_output(cmd, universal_newlines=True)
# NOTE: We generate nodes and edges before giving them to graphviz as
# the library does not de-deuplicate them.
nodes, build_deps, runtime_deps = parse_graph(re.split(r"\|\|", graph_lines))
graph = generate_graph(nodes, build_deps, runtime_deps)
print(graph.source)
if args.format:
graph.render(cleanup=True,
filename='bst-graph',
format=args.format,
view=args.view)
if __name__ == '__main__':
main()