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() | 
