Chandan Singh pushed to branch chandan/dot-graph at BuildStream / buildstream
Commits:
-
ec4bbf35
by Benjamin Schubert at 2019-02-13T14:59:47Z
-
b41a82d3
by Benjamin Schubert at 2019-02-13T14:59:47Z
-
9db7f489
by Benjamin Schubert at 2019-02-13T16:13:16Z
-
f2d72ba9
by Chandan Singh at 2019-02-13T16:15:03Z
5 changed files:
- buildstream/_exceptions.py
- buildstream/_loader/loader.py
- buildstream/_project.py
- buildstream/element.py
- + contrib/bst-graph
Changes:
| ... | ... | @@ -19,6 +19,7 @@ |
| 19 | 19 |
# Tiago Gomes <tiago gomes codethink co uk>
|
| 20 | 20 |
|
| 21 | 21 |
from enum import Enum
|
| 22 |
+import os
|
|
| 22 | 23 |
|
| 23 | 24 |
# Disable pylint warnings for whole file here:
|
| 24 | 25 |
# pylint: disable=global-statement
|
| ... | ... | @@ -50,6 +51,9 @@ def get_last_exception(): |
| 50 | 51 |
# Used by regression tests
|
| 51 | 52 |
#
|
| 52 | 53 |
def get_last_task_error():
|
| 54 |
+ if 'BST_TEST_SUITE' not in os.environ:
|
|
| 55 |
+ raise BstError("Getting the last task error is only supported when running tests")
|
|
| 56 |
+ |
|
| 53 | 57 |
global _last_task_error_domain
|
| 54 | 58 |
global _last_task_error_reason
|
| 55 | 59 |
|
| ... | ... | @@ -67,11 +71,12 @@ def get_last_task_error(): |
| 67 | 71 |
# tests about how things failed in a machine readable way
|
| 68 | 72 |
#
|
| 69 | 73 |
def set_last_task_error(domain, reason):
|
| 70 |
- global _last_task_error_domain
|
|
| 71 |
- global _last_task_error_reason
|
|
| 74 |
+ if 'BST_TEST_SUITE' in os.environ:
|
|
| 75 |
+ global _last_task_error_domain
|
|
| 76 |
+ global _last_task_error_reason
|
|
| 72 | 77 |
|
| 73 |
- _last_task_error_domain = domain
|
|
| 74 |
- _last_task_error_reason = reason
|
|
| 78 |
+ _last_task_error_domain = domain
|
|
| 79 |
+ _last_task_error_reason = reason
|
|
| 75 | 80 |
|
| 76 | 81 |
|
| 77 | 82 |
class ErrorDomain(Enum):
|
| ... | ... | @@ -126,7 +131,8 @@ class BstError(Exception): |
| 126 | 131 |
self.reason = reason
|
| 127 | 132 |
|
| 128 | 133 |
# Hold on to the last raised exception for testing purposes
|
| 129 |
- _last_exception = self
|
|
| 134 |
+ if 'BST_TEST_SUITE' in os.environ:
|
|
| 135 |
+ _last_exception = self
|
|
| 130 | 136 |
|
| 131 | 137 |
|
| 132 | 138 |
# PluginError
|
| ... | ... | @@ -152,8 +152,27 @@ class Loader(): |
| 152 | 152 |
#
|
| 153 | 153 |
ret.append(loader._collect_element(element))
|
| 154 | 154 |
|
| 155 |
+ self._clean_caches()
|
|
| 156 |
+ |
|
| 155 | 157 |
return ret
|
| 156 | 158 |
|
| 159 |
+ # clean_caches()
|
|
| 160 |
+ #
|
|
| 161 |
+ # Clean internal loader caches, recursively
|
|
| 162 |
+ #
|
|
| 163 |
+ # When loading the elements, the loaders use caches in order to not load the
|
|
| 164 |
+ # same element twice. These are kept after loading and prevent garbage
|
|
| 165 |
+ # collection. Cleaning them explicitely is required.
|
|
| 166 |
+ #
|
|
| 167 |
+ def _clean_caches(self):
|
|
| 168 |
+ for loader in self._loaders.values():
|
|
| 169 |
+ # value may be None with nested junctions without overrides
|
|
| 170 |
+ if loader is not None:
|
|
| 171 |
+ loader._clean_caches()
|
|
| 172 |
+ |
|
| 173 |
+ self._meta_elements = {}
|
|
| 174 |
+ self._elements = {}
|
|
| 175 |
+ |
|
| 157 | 176 |
###########################################
|
| 158 | 177 |
# Private Methods #
|
| 159 | 178 |
###########################################
|
| ... | ... | @@ -358,6 +358,8 @@ class Project(): |
| 358 | 358 |
for meta in meta_elements
|
| 359 | 359 |
]
|
| 360 | 360 |
|
| 361 |
+ Element._clear_meta_elements_cache()
|
|
| 362 |
+ |
|
| 361 | 363 |
# Now warn about any redundant source references which may have
|
| 362 | 364 |
# been discovered in the resolve() phase.
|
| 363 | 365 |
redundant_refs = Element._get_redundant_source_refs()
|
| ... | ... | @@ -966,6 +966,21 @@ class Element(Plugin): |
| 966 | 966 |
|
| 967 | 967 |
return element
|
| 968 | 968 |
|
| 969 |
+ # _clear_meta_elements_cache()
|
|
| 970 |
+ #
|
|
| 971 |
+ # Clear the internal meta elements cache.
|
|
| 972 |
+ #
|
|
| 973 |
+ # When loading elements from meta, we cache already instantiated elements
|
|
| 974 |
+ # in order to not have to load the same elements twice.
|
|
| 975 |
+ # This clears the cache.
|
|
| 976 |
+ #
|
|
| 977 |
+ # It should be called whenever we are done loading all elements in order
|
|
| 978 |
+ # to save memory.
|
|
| 979 |
+ #
|
|
| 980 |
+ @classmethod
|
|
| 981 |
+ def _clear_meta_elements_cache(cls):
|
|
| 982 |
+ cls.__instantiated_elements = {}
|
|
| 983 |
+ |
|
| 969 | 984 |
# _get_redundant_source_refs()
|
| 970 | 985 |
#
|
| 971 | 986 |
# Fetches a list of (Source, ref) tuples of all the Sources
|
| 1 |
+#!/usr/bin/env python3
|
|
| 2 |
+'''Print dependency graph of given element(s) in DOT format.
|
|
| 3 |
+ |
|
| 4 |
+This script must be run from the same directory where you would normally
|
|
| 5 |
+run `bst` commands.
|
|
| 6 |
+ |
|
| 7 |
+When `--format` option is specified, the output will also be rendered in the
|
|
| 8 |
+given format. A file with name `bst-graph.{format}` will be created in the same
|
|
| 9 |
+directory. To use this option, you must have the `graphviz` command line tool
|
|
| 10 |
+installed.
|
|
| 11 |
+'''
|
|
| 12 |
+ |
|
| 13 |
+import argparse
|
|
| 14 |
+import subprocess
|
|
| 15 |
+ |
|
| 16 |
+from graphviz import Digraph
|
|
| 17 |
+ |
|
| 18 |
+ |
|
| 19 |
+def parse_args():
|
|
| 20 |
+ '''Handle parsing of command line arguments.
|
|
| 21 |
+ |
|
| 22 |
+ Returns:
|
|
| 23 |
+ A argparse.Namespace object
|
|
| 24 |
+ '''
|
|
| 25 |
+ parser = argparse.ArgumentParser(description=__doc__)
|
|
| 26 |
+ parser.add_argument(
|
|
| 27 |
+ 'ELEMENT', nargs='*',
|
|
| 28 |
+ help='Name of the element'
|
|
| 29 |
+ )
|
|
| 30 |
+ parser.add_argument(
|
|
| 31 |
+ '--format',
|
|
| 32 |
+ help='Redner the graph in given format (`pdf`, `png`, `svg` etc)'
|
|
| 33 |
+ )
|
|
| 34 |
+ parser.add_argument(
|
|
| 35 |
+ '--view', action='store_true',
|
|
| 36 |
+ help='Open the rendered graph with the default application'
|
|
| 37 |
+ )
|
|
| 38 |
+ return parser.parse_args()
|
|
| 39 |
+ |
|
| 40 |
+ |
|
| 41 |
+def parse_graph(lines):
|
|
| 42 |
+ '''Return nodes and edges of the parsed grpah.
|
|
| 43 |
+ |
|
| 44 |
+ Args:
|
|
| 45 |
+ lines: List of lines in format 'NAME|BUILD-DEPS|RUNTIME-DEPS'
|
|
| 46 |
+ |
|
| 47 |
+ Returns:
|
|
| 48 |
+ Tuple of format (nodes,build_deps,runtime_deps)
|
|
| 49 |
+ Each member of build_deps and runtime_deps is also a tuple.
|
|
| 50 |
+ '''
|
|
| 51 |
+ nodes = set()
|
|
| 52 |
+ build_deps = set()
|
|
| 53 |
+ runtime_deps = set()
|
|
| 54 |
+ for line in lines:
|
|
| 55 |
+ # It is safe to split on '|' as it is not a valid character for
|
|
| 56 |
+ # element names.
|
|
| 57 |
+ name, build_dep, runtime_dep = line.split('|')
|
|
| 58 |
+ build_dep = build_dep.lstrip('[').rstrip(']').split(',')
|
|
| 59 |
+ runtime_dep = runtime_dep.lstrip('[').rstrip(']').split(',')
|
|
| 60 |
+ nodes.add(name)
|
|
| 61 |
+ [build_deps.add((name, dep)) for dep in build_dep if dep]
|
|
| 62 |
+ [runtime_deps.add((name, dep)) for dep in runtime_dep if dep]
|
|
| 63 |
+ |
|
| 64 |
+ return nodes, build_deps, runtime_deps
|
|
| 65 |
+ |
|
| 66 |
+ |
|
| 67 |
+def generate_graph(nodes, build_deps, runtime_deps):
|
|
| 68 |
+ '''Generate graph from given nodes and edges.
|
|
| 69 |
+ |
|
| 70 |
+ Args:
|
|
| 71 |
+ nodes: set of nodes
|
|
| 72 |
+ build_deps: set of tuples of build depdencies
|
|
| 73 |
+ runtime_deps: set of tuples of runtime depdencies
|
|
| 74 |
+ |
|
| 75 |
+ Returns:
|
|
| 76 |
+ A graphviz.Digraph object
|
|
| 77 |
+ '''
|
|
| 78 |
+ graph = Digraph()
|
|
| 79 |
+ for node in nodes:
|
|
| 80 |
+ graph.node(node)
|
|
| 81 |
+ for source, target in build_deps:
|
|
| 82 |
+ graph.edge(source, target, label='build-dep')
|
|
| 83 |
+ for source, target in runtime_deps:
|
|
| 84 |
+ graph.edge(source, target, label='runtime-dep')
|
|
| 85 |
+ return graph
|
|
| 86 |
+ |
|
| 87 |
+ |
|
| 88 |
+def main():
|
|
| 89 |
+ args = parse_args()
|
|
| 90 |
+ cmd = ['bst', 'show', '--format', '%{name}|%{build-deps}|%{runtime-deps}']
|
|
| 91 |
+ if 'element' in args:
|
|
| 92 |
+ cmd += args.element
|
|
| 93 |
+ graph_lines = subprocess.check_output(cmd, universal_newlines=True)
|
|
| 94 |
+ # NOTE: We generate nodes and edges before giving them to graphviz as
|
|
| 95 |
+ # the library does not de-deuplicate them.
|
|
| 96 |
+ nodes, build_deps, runtime_deps = parse_graph(graph_lines.splitlines())
|
|
| 97 |
+ graph = generate_graph(nodes, build_deps, runtime_deps)
|
|
| 98 |
+ print(graph.source)
|
|
| 99 |
+ if args.format:
|
|
| 100 |
+ graph.render(cleanup=True,
|
|
| 101 |
+ filename='bst-graph',
|
|
| 102 |
+ format=args.format,
|
|
| 103 |
+ view=args.view)
|
|
| 104 |
+ |
|
| 105 |
+ |
|
| 106 |
+if __name__ == '__main__':
|
|
| 107 |
+ main()
|
