Skip to content

Commit 524c1a4

Browse files
committed
Feature: Add support for generating json from bst show command
This adds a new flag called --json to the `bst show` command which when specified will make bst output a machine readable JSON dump of the dependency state within the current buildstream project. Having JSON support makes it easier to write tools that need to inspect the configuration of a buildstream enabled project.
1 parent d62a313 commit 524c1a4

File tree

3 files changed

+260
-35
lines changed

3 files changed

+260
-35
lines changed

src/buildstream/_frontend/cli.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
import os
1515
import re
1616
import sys
17+
import json
1718
from functools import partial
1819

1920
import shutil
2021
import click
2122
from .. import _yaml
2223
from .._exceptions import BstError, LoadError, AppError, RemoteError
2324
from .complete import main_bashcomplete, complete_path, CompleteUnhandled
24-
from ..types import _CacheBuildTrees, _SchedulerErrorAction, _PipelineSelection, _HostMount, _Scope
25+
from ..types import _CacheBuildTrees, _ElementKind, _SchedulerErrorAction, _PipelineSelection, _HostMount, _Scope
2526
from .._remotespec import RemoteSpec, RemoteSpecPurpose
2627
from ..utils import UtilError
2728

@@ -74,6 +75,7 @@ def __repr__(self):
7475
# Override of click's main entry point #
7576
##################################################################
7677

78+
7779
# search_command()
7880
#
7981
# Helper function to get a command and context object
@@ -538,6 +540,7 @@ def build(
538540
@click.option(
539541
"--except", "except_", multiple=True, type=click.Path(readable=False), help="Except certain dependencies"
540542
)
543+
@click.option("--json", "as_json", is_flag=True, help="Output the information as machine readable JSON")
541544
@click.option(
542545
"--deps",
543546
"-d",
@@ -552,7 +555,29 @@ def build(
552555
_PipelineSelection.ALL,
553556
],
554557
),
555-
help="The dependencies to show",
558+
help="Types of Elements to Show",
559+
)
560+
@click.option(
561+
"--kind",
562+
"-k",
563+
default=[],
564+
show_default=True,
565+
multiple=True,
566+
type=FastEnumType(
567+
_ElementKind,
568+
[
569+
_ElementKind.ALL,
570+
_ElementKind.KEY,
571+
_ElementKind.KEY_FULL,
572+
_ElementKind.STATE,
573+
_ElementKind.SOURCES,
574+
_ElementKind.DEPENDENCIES,
575+
_ElementKind.BUILD_DEPENDENCIES,
576+
_ElementKind.RUNTIME_DEPENDENCIES,
577+
_ElementKind.CAS_ARTIFACTS,
578+
],
579+
),
580+
help="Kinds of element information to display in JSON",
556581
)
557582
@click.option(
558583
"--order",
@@ -572,7 +597,7 @@ def build(
572597
)
573598
@click.argument("elements", nargs=-1, type=click.Path(readable=False))
574599
@click.pass_obj
575-
def show(app, elements, deps, except_, order, format_):
600+
def show(app, elements, deps, as_json, except_, order, kind, format_):
576601
"""Show elements in the pipeline
577602
578603
Specifying no elements will result in showing the default targets
@@ -629,19 +654,44 @@ def show(app, elements, deps, except_, order, format_):
629654
\b
630655
bst show target.bst --format \\
631656
$'---------- %{name} ----------\\n%{vars}'
657+
658+
**JSON OUTPUT**
659+
660+
The ``--json`` flag will cause bst to output information in machine readable
661+
JSON. When using this flag you may also specify ``--kind`` to control the
662+
type of information that is output.
663+
664+
To dump everything:
665+
\b
666+
bst show --json --kind all
632667
"""
633668
with app.initialized():
634669

670+
if as_json and format_:
671+
raise AppError("--format and --json are mutually exclusive")
672+
673+
if format_ and kind:
674+
raise AppError("--format does not support --kind")
675+
635676
if not format_:
636677
format_ = app.context.log_element_format
637678

638679
# First determine whether we need to go about querying the local cache
639680
# and spending time setting up remotes.
640-
state_match = re.search(r"%(\{(state)[^%]*?\})", format_)
641-
key_match = re.search(r"%(\{(key)[^%]*?\})", format_)
642-
full_key_match = re.search(r"%(\{(full-key)[^%]*?\})", format_)
643-
artifact_cas_digest_match = re.search(r"%(\{(artifact-cas-digest)[^%]*?\})", format_)
644-
need_state = bool(state_match or key_match or full_key_match or artifact_cas_digest_match)
681+
need_state = False
682+
if not as_json:
683+
state_match = re.search(r"%(\{(state)[^%]*?\})", format_)
684+
key_match = re.search(r"%(\{(key)[^%]*?\})", format_)
685+
full_key_match = re.search(r"%(\{(full-key)[^%]*?\})", format_)
686+
artifact_cas_digest_match = re.search(r"%(\{(artifact-cas-digest)[^%]*?\})", format_)
687+
need_state = bool(state_match or key_match or full_key_match or artifact_cas_digest_match)
688+
else:
689+
need_state = bool(
690+
_ElementKind.STATE in kind
691+
or _ElementKind.KEY in kind
692+
or _ElementKind.KEY_FULL in kind
693+
or _ElementKind.CAS_ARTIFACTS in kind
694+
)
645695

646696
if not elements:
647697
elements = app.project.get_default_targets()
@@ -657,8 +707,12 @@ def show(app, elements, deps, except_, order, format_):
657707
if order == "alpha":
658708
dependencies = sorted(dependencies)
659709

660-
report = app.logger.show_pipeline(dependencies, format_)
661-
click.echo(report)
710+
if as_json:
711+
serialized = app.logger.dump_pipeline(dependencies, kind)
712+
print(json.dumps(serialized))
713+
else:
714+
report = app.logger.show_pipeline(dependencies, format_)
715+
click.echo(report)
662716

663717

664718
##################################################################

src/buildstream/_frontend/widget.py

Lines changed: 132 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import click
2323

2424
from .profile import Profile
25-
from ..types import _Scope
25+
from ..types import _Scope, _ElementKind, _ElementState
2626
from .. import _yaml
2727
from .. import __version__ as bst_version
2828
from .. import FileType
@@ -327,6 +327,120 @@ def __init__(
327327
logfile_tokens = self._parse_logfile_format(context.log_message_format, content_profile, format_profile)
328328
self._columns.extend(logfile_tokens)
329329

330+
def _read_state(self, element):
331+
try:
332+
if not element._has_all_sources_resolved():
333+
return _ElementState.NO_REFERENCE
334+
else:
335+
if element.get_kind() == "junction":
336+
return _ElementState.JUNCTION
337+
elif not element._can_query_cache():
338+
return _ElementState.WAITING
339+
elif element._cached_failure():
340+
return _ElementState.FAILED
341+
elif element._cached_success():
342+
return _ElementState.CACHED
343+
elif not element._can_query_source_cache():
344+
return _ElementState.WAITING
345+
elif element._fetch_needed():
346+
return _ElementState.FETCH_NEEDED
347+
elif element._buildable():
348+
return _ElementState.BUILDABLE
349+
else:
350+
return _ElementState.WAITING
351+
except BstError as e:
352+
# Provide context to plugin error
353+
e.args = ("Failed to determine state for {}: {}".format(element._get_full_name(), str(e)),)
354+
raise e
355+
356+
# Dump the pipeline as a serializable object
357+
def dump_pipeline(self, dependencies, kinds=[]):
358+
359+
show_all = _ElementKind.ALL in kinds
360+
361+
elements = []
362+
363+
for element in dependencies:
364+
365+
# Default values always output
366+
serialized_element = {
367+
"name": element._get_full_name(),
368+
}
369+
370+
description = " ".join(element._description.splitlines())
371+
if description:
372+
serialized_element["description"] = description
373+
374+
workspace = element._get_workspace()
375+
if workspace:
376+
serialized_element["workspace"] = workspace
377+
378+
if show_all or _ElementKind.STATE in kinds:
379+
serialized_element["state"] = self._read_state(element).value
380+
381+
if show_all or _ElementKind.KEY in kinds:
382+
serialized_element["key"] = element._get_display_key().brief
383+
384+
if show_all or _ElementKind.KEY_FULL in kinds:
385+
serialized_element["key_full"] = element._get_display_key().full
386+
387+
if show_all or _ElementKind.VARIABLES in kinds:
388+
serialized_element["variables"] = dict(element._Element__variables)
389+
390+
if show_all or _ElementKind.ENVIRONMENT in kinds:
391+
serialized_element["environment"] = dict(element._Element__environment)
392+
393+
if show_all or _ElementKind.CAS_ARTIFACTS in kinds:
394+
# BUG: Due to the assersion within .get_artifact this will
395+
# error but there is no other way to determine if an artifact
396+
# exists and we only want to show this value for informational
397+
# purposes.
398+
try:
399+
artifact = element._get_artifact()
400+
if artifact.cached():
401+
serialized_element["artifact"] = {
402+
"files": artifact.get_files(),
403+
"digest": artifact_files._get_digest(),
404+
}
405+
except:
406+
pass
407+
408+
if show_all or _ElementKind.SOURCES in kinds:
409+
all_source_infos = []
410+
for source in element.sources():
411+
source_infos = source.collect_source_info()
412+
413+
if source_infos is not None:
414+
serialized_sources = []
415+
for s in source_infos:
416+
serialized = s.serialize()
417+
serialized_sources.append(serialized)
418+
419+
all_source_infos += serialized_sources
420+
serialized_element["sources"] = all_source_infos
421+
422+
# Show dependencies
423+
if show_all or _ElementKind.DEPENDENCIES in kinds:
424+
serialized_element["dependencies"] = [
425+
e._get_full_name() for e in element._dependencies(_Scope.ALL, recurse=False)
426+
]
427+
428+
# Show build dependencies
429+
if show_all or _ElementKind.BUILD_DEPENDENCIES in kinds:
430+
serialized_element["build-dependencies"] = [
431+
e._get_full_name() for e in element._dependencies(_Scope.BUILD, recurse=False)
432+
]
433+
434+
# Show runtime dependencies
435+
if show_all or _ElementKind.RUNTIME_DEPENDENCIES in kinds:
436+
serialized_element["runtime-dependencies"] = [
437+
e._get_full_name() for e in element._dependencies(_Scope.RUN, recurse=False)
438+
]
439+
440+
elements.append(serialized_element)
441+
442+
return elements
443+
330444
# show_pipeline()
331445
#
332446
# Display a list of elements in the specified format.
@@ -359,30 +473,23 @@ def show_pipeline(self, dependencies, format_):
359473
line = p.fmt_subst(line, "full-key", key.full, fg="yellow", dim=dim_keys)
360474
line = p.fmt_subst(line, "description", description, fg="yellow", dim=dim_keys)
361475

362-
try:
363-
if not element._has_all_sources_resolved():
364-
line = p.fmt_subst(line, "state", "no reference", fg="red")
365-
else:
366-
if element.get_kind() == "junction":
367-
line = p.fmt_subst(line, "state", "junction", fg="magenta")
368-
elif not element._can_query_cache():
369-
line = p.fmt_subst(line, "state", "waiting", fg="blue")
370-
elif element._cached_failure():
371-
line = p.fmt_subst(line, "state", "failed", fg="red")
372-
elif element._cached_success():
373-
line = p.fmt_subst(line, "state", "cached", fg="magenta")
374-
elif not element._can_query_source_cache():
375-
line = p.fmt_subst(line, "state", "waiting", fg="blue")
376-
elif element._fetch_needed():
377-
line = p.fmt_subst(line, "state", "fetch needed", fg="red")
378-
elif element._buildable():
379-
line = p.fmt_subst(line, "state", "buildable", fg="green")
380-
else:
381-
line = p.fmt_subst(line, "state", "waiting", fg="blue")
382-
except BstError as e:
383-
# Provide context to plugin error
384-
e.args = ("Failed to determine state for {}: {}".format(element._get_full_name(), str(e)),)
385-
raise e
476+
element_state = self._read_state(element)
477+
if element_state == _ElementState.NO_REFERENCE:
478+
line = p.fmt_subst(line, "state", "no reference", fg="red")
479+
elif element_state == _ElementState.JUNCTION:
480+
line = p.fmt_subst(line, "state", "junction", fg="magenta")
481+
elif element_state == _ElementState.FAILED:
482+
line = p.fmt_subst(line, "state", "failed", fg="red")
483+
elif element_state == _ElementState.CACHED:
484+
line = p.fmt_subst(line, "state", "cached", fg="magenta")
485+
elif element_state == _ElementState.WAITING:
486+
line = p.fmt_subst(line, "state", "waiting", fg="blue")
487+
elif element_state == _ElementState.FETCH_NEEDED:
488+
line = p.fmt_subst(line, "state", "fetch needed", fg="red")
489+
elif element_state == _ElementState.BUILDABLE:
490+
line = p.fmt_subst(line, "state", "buildable", fg="green")
491+
else:
492+
raise BstError(f"Unreachable State: {element_state}")
386493

387494
# Element configuration
388495
if "%{config" in format_:

src/buildstream/types.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,70 @@ def __str__(self):
311311
return str(self.value)
312312

313313

314+
# _ElementKind()
315+
#
316+
# Used to indicate which values you are interested in when dumping the
317+
# pipeline to a serializable format.
318+
class _ElementKind(FastEnum):
319+
320+
# Show all available pipeline information
321+
ALL = "all"
322+
323+
# Show the short version of an element key
324+
KEY = "key"
325+
326+
# Show the full key information
327+
KEY_FULL = "key-full"
328+
329+
# Show the Element State
330+
STATE = "state"
331+
332+
# Show source provenance information
333+
SOURCES = "source"
334+
335+
# Dependencies
336+
DEPENDENCIES = "dependences"
337+
338+
# Build dependencies
339+
BUILD_DEPENDENCIES = "build-dependencies"
340+
341+
# Runtime dependencies
342+
RUNTIME_DEPENDENCIES = "runtime-dependencies"
343+
344+
# CAS Artifacts
345+
CAS_ARTIFACTS = "cas-artifacts"
346+
347+
# Element Variables
348+
VARIABLES = "variables"
349+
350+
# Element Environment
351+
ENVIRONMENT = "environment"
352+
353+
354+
# Used to indicate the state of a given element
355+
class _ElementState(FastEnum):
356+
# Cannot determine the element state
357+
NO_REFERENCE = "no-reference"
358+
359+
# The element has failed
360+
FAILED = "failed"
361+
362+
# The element is a junction
363+
JUNCTION = "junction"
364+
365+
# The element is waiting
366+
WAITING = "waiting"
367+
368+
# The element is cached
369+
CACHED = "cached"
370+
371+
# The element needs to be loaded from a remote source
372+
FETCH_NEEDED = "fetch-needed"
373+
374+
# The element my be built
375+
BUILDABLE = "buildable"
376+
377+
314378
# _ProjectInformation()
315379
#
316380
# A descriptive object about a project.

0 commit comments

Comments
 (0)