Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bst graph command #1949

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 0 additions & 130 deletions contrib/bst-graph

This file was deleted.

21 changes: 21 additions & 0 deletions src/buildstream/_frontend/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1654,3 +1654,24 @@ def artifact_delete(app, artifacts, deps):
"""Remove artifacts from the local cache"""
with app.initialized():
app.stream.artifact_delete(artifacts, selection=deps)


###################################################################
# Graph Command #
###################################################################
@cli.command(short_help="Output pipeline dependency graph.")
@click.option(
"--format",
"-f",
"format_",
metavar="FORMAT",
default="dot",
type=click.STRING,
help="Output format: e.g. `dot` and `pkl`",
)
@click.argument("target", nargs=1, required=True, type=click.Path())
@click.pass_obj
def graph(app, target, format_):
"""Output dependency graph."""
with app.initialized():
app.stream.graph(target, format_)
33 changes: 32 additions & 1 deletion src/buildstream/_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import shutil
import tarfile
import tempfile

from contextlib import contextmanager, suppress
from collections import deque
from typing import List, Tuple, Optional, Iterable, Callable
Expand All @@ -49,7 +50,7 @@
from ._state import State
from .types import _KeyStrength, _PipelineSelection, _Scope, _HostMount
from .plugin import Plugin
from . import utils, node, _yaml, _site, _pipeline
from . import utils, node, _tree, _yaml, _site, _pipeline


# Stream()
Expand Down Expand Up @@ -1228,6 +1229,36 @@ def redirect_element_names(self, elements):

return list(output_elements)

# graph()
#
# Outputs a dependency graph for the target element, in the given format.
# The dot file format can be rendered using graphviz.
#
# Args:
# target (str): The target element from which to build a dependency graph.
# format_ ('dot'/'pkl'): A .pkl dictionary or graphviz .dot file.
#
def graph(self, target, format_):
def _render(scope):
tree = _tree.Tree()
scope_name = {_Scope.BUILD: 'buildtime', _Scope.RUN: 'runtime'}[scope]

for element in self.load_selection([target], selection=_PipelineSelection.ALL, need_state=False):
dependencies = {d._get_full_name() for d in element._dependencies(scope, recurse=False) if d}

for dep in dependencies:
tree.link(element._get_full_name(), dep)

path = os.path.basename(target)
path, _ = os.path.splitext(path)
path = f'{path}.{scope_name}'
tree.save(path, format_)

self._context.messenger.info(f'Rendered dependency graph: {path}')

_render(_Scope.BUILD)
_render(_Scope.RUN)

# get_state()
#
# Get the State object owned by Stream
Expand Down
42 changes: 42 additions & 0 deletions src/buildstream/_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import os
import pickle as pkl

from typing import List

class Tree:
def __init__(self):
self.nodes = set()
self.links = dict()

def link(self, predecessor: str, successor: str) -> None:
self.links.setdefault(successor, [])
self.links.setdefault(predecessor, [])

if successor == predecessor:
raise ValueError(f'Predecessor and successor are identical: {successor}={predecessor}')
elif successor in self.links[predecessor]:
raise ValueError(f'Cycle detected - not valid tree: {successor}<=>{predecessor}')

for n in [predecessor, successor]:
self.nodes.add(n)

self.links[successor].append(predecessor)

def save(self, path: str, format_: str = 'dot') -> None:
"""Saves the tree to disk.

dot: A graphviz .dot file.
pkl: A treelib compatible dictionary .pkl file.
"""
with open(f'{path}.{format_}', 'w+') as file:
if format_ == 'pkl':
pkl.dump(self.links, file)
elif format_ == 'dot':
lines = [f'digraph "{path}" {{']
lines.extend(f' "{n}" [label="{n}"]' for n in self.nodes)
lines.extend(f' "{p}" -> "{s}"' for s, ps in self.links.items() for p in ps)
lines.append(f'}}')

file.writelines([f'{l}\n' for l in lines])
else:
raise ValueError(f'Unknown format: {format_}')