Skip to content

Commit 9e9eba9

Browse files
Shell API and CLI: Add option for staging additional runtime targets
This enables users to add additional functionality such as debug tooling in the shell sandbox, without needing to modify the target element. This is achived through introducing a new option to shell. All existing API and UX is maintained, to not break existing scripts. An alternative design was considered, to have the additional elements as positional arguments similar to the existing element, but this would need manual parsing to handle the cases where `--` is present and not present, splitting based on a `.bst` suffix. This UX could be re-visited in future. Example usage: bst shell --with base.bst example.bst -- cat example.txt Where: - example.bst is a simple import element with no dependencies that imports a file called example.txt - base.bst provides a basic alpine sysroot with a standard set of unix tooling (sh, df, cat etc). Changes: - Introduces `test_with_other_targets` integration test to the shell test suite. - Adds `--with` cli option to the shell subcommand and updates it's documentation. - option can be used multiple times by caller, providing a list of targets. - Extends the shell top level calling interface in Buildstream core to accept a list of other_targets - This is where the targets are loaded into elements and checked to make sure they are present - Extends the shell element implementation to accept a list of other targets - This is where the other elements are staged and integrated into the sandbox
1 parent 6e88842 commit 9e9eba9

4 files changed

Lines changed: 137 additions & 6 deletions

File tree

‎src/buildstream/_frontend/cli.py‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,6 @@ def cli(context, **kwargs):
378378
user preferences configuration file.
379379
"""
380380

381-
from .app import App
382-
383381
# Create the App, giving it the main arguments
384382
context.obj = App.create(dict(kwargs))
385383
context.call_on_close(context.obj.cleanup)
@@ -687,6 +685,13 @@ def show(app, elements, deps, except_, order, format_):
687685
metavar="HOSTPATH PATH",
688686
help="Mount a file or directory into the sandbox",
689687
)
688+
@click.option(
689+
"--with",
690+
"other_targets",
691+
type=click.Path(readable=False),
692+
multiple=True,
693+
help="A additional target to stage into an element's sandbox environment",
694+
)
690695
@click.option("--isolate", is_flag=True, help="Create an isolated build sandbox")
691696
@click.option(
692697
"--use-buildtree",
@@ -726,6 +731,7 @@ def show(app, elements, deps, except_, order, format_):
726731
def shell(
727732
app: App,
728733
target,
734+
other_targets,
729735
command,
730736
mount,
731737
isolate,
@@ -749,13 +755,38 @@ def shell(
749755
otherwise bst may respond to them instead. e.g.
750756
751757
\b
752-
bst shell example.bst -- df -h
758+
bst shell base.bst -- df -h
753759
754760
Use the --build option to create a temporary sysroot for
755761
building the element instead.
756762
763+
Use the --with option to stage the artifacts of other elements
764+
into the temporary sysroot to make them available to run e.g.
765+
766+
\b
767+
bst shell --with base.bst example.bst -- cat example.txt
768+
757769
If no COMMAND is specified, the default is to attempt
758770
to run an interactive shell.
771+
772+
# Examples:
773+
774+
\b
775+
# Attempt to run an interactive shell with example.bst
776+
bst shell example.bst
777+
# Attempt to run an df -h with example.bst
778+
bst shell example.bst -- df h
779+
# In a workspace directory, attempt to shell into the workspace element
780+
bst shell
781+
# Attempt to run cat from base.bst to read example.txt from example.bst
782+
bst shell --with base.bst example.bst -- cat example.txt
783+
# Attempt to run an interactive shell with the sources and all dependencies of example.bst
784+
bst shell --build example.bst
785+
786+
For all examples on this page:
787+
- example.bst is a simple import element with no dependencies
788+
that imports a file called example.txt
789+
- base.bst provides a basic alpine sysroot with a standard set of unix tooling (sh, df, cat etc).
759790
"""
760791

761792
# Buildtree can only be used with build shells
@@ -777,6 +808,7 @@ def shell(
777808
target,
778809
scope,
779810
app.shell_prompt,
811+
other_targets=other_targets,
780812
mounts=mounts,
781813
isolate=isolate,
782814
command=command,

‎src/buildstream/_stream.py‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ def query_cache(self, elements, *, sources_of_cached_elements=False, only_source
243243
# target: The name of the element to run the shell for
244244
# scope: The scope for the shell, only BUILD or RUN are valid (_Scope)
245245
# prompt: A function to return the prompt to display in the shell
246+
# other_targets (Iterable[str]): The name of other elements to stage in the shell
246247
# unique_id: (str): A unique_id to use to lookup an Element instance
247248
# mounts: Additional directories to mount into the sandbox
248249
# isolate (bool): Whether to isolate the environment like we do in builds
@@ -262,6 +263,7 @@ def shell(
262263
scope: _Scope,
263264
prompt: Callable[[Element], str],
264265
*,
266+
other_targets: Iterable[str] = (),
265267
unique_id: Optional[str] = None,
266268
mounts: Optional[List[_HostMount]] = None,
267269
isolate: bool = False,
@@ -357,8 +359,46 @@ def shell(
357359
self._fetch([element])
358360
_pipeline.assert_sources_cached(self._context, [element])
359361

362+
# Load the other targets
363+
try:
364+
other_elements = self.load_selection(
365+
other_targets,
366+
selection=_PipelineSelection.RUN,
367+
load_artifacts=True,
368+
connect_artifact_cache=True,
369+
connect_source_cache=True,
370+
artifact_remotes=artifact_remotes,
371+
source_remotes=source_remotes,
372+
ignore_project_artifact_remotes=ignore_project_artifact_remotes,
373+
ignore_project_source_remotes=ignore_project_source_remotes,
374+
)
375+
except StreamError as e:
376+
if e.reason == "deps-not-supported":
377+
raise StreamError(
378+
"Only buildtrees are supported with artifact names",
379+
detail="Use the --build and --use-buildtree options to shell into a cached build tree",
380+
reason="only-buildtrees-supported",
381+
) from e
382+
raise
383+
384+
self.query_cache(other_elements)
385+
self._pull_missing_artifacts(other_elements)
386+
missing_deps = [dep for dep in _pipeline.dependencies(other_elements, _Scope.RUN) if not dep._cached()]
387+
if missing_deps:
388+
raise StreamError(
389+
"Elements need to be built or downloaded before staging a shell environment",
390+
detail="\n".join(list(map(lambda x: x._get_full_name(), missing_deps))),
391+
reason="shell-missing-deps",
392+
)
393+
360394
return element._shell(
361-
scope, mounts=mounts, isolate=isolate, prompt=prompt(element), command=command, usebuildtree=usebuildtree
395+
scope,
396+
mounts=mounts,
397+
isolate=isolate,
398+
prompt=prompt(element),
399+
command=command,
400+
usebuildtree=usebuildtree,
401+
other_elements=other_elements,
362402
)
363403

364404
# build()

‎src/buildstream/element.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
---------------
6363
"""
6464

65+
# For 3.7+ support, not necessary and deprecated in 3.14+
66+
from __future__ import annotations
67+
6568
import os
6669
import re
6770
import stat
@@ -74,6 +77,7 @@
7477
from threading import Lock
7578
from typing import cast, TYPE_CHECKING, Dict, Iterator, Iterable, List, Optional, Set, Sequence
7679

80+
7781
from pyroaring import BitMap # pylint: disable=no-name-in-module
7882

7983
from . import _yaml
@@ -2058,6 +2062,7 @@ def _push(self):
20582062
# prompt (str): A suitable prompt string for PS1
20592063
# command (list): An argv to launch in the sandbox
20602064
# usebuildtree (bool): Use the buildtree as its source
2065+
# other_elements (List[Element]): Optional list of other runtime elements to stage in the sandbox
20612066
#
20622067
# Returns: Exit code
20632068
def _shell(
@@ -2069,6 +2074,7 @@ def _shell(
20692074
prompt: str | None = None,
20702075
command: List[str] | None = None,
20712076
usebuildtree: bool = False,
2077+
other_elements: List[Element] | None = None,
20722078
):
20732079

20742080
with self._prepare_sandbox(scope, shell=True, usebuildtree=usebuildtree) as sandbox:
@@ -2085,6 +2091,17 @@ def _shell(
20852091
if prompt is not None:
20862092
environment["PS1"] = prompt
20872093

2094+
with self.timed_activity("Staging other_targets", silent_nested=True), self.__collect_overlaps(sandbox):
2095+
self.stage_dependency_artifacts(sandbox, other_elements)
2096+
2097+
if other_elements:
2098+
# Stage artifacts from other_elements into the sandbox.
2099+
for element in other_elements:
2100+
# Stage deps in the sandbox root
2101+
with element.timed_activity("Integrating sandbox"), sandbox.batch():
2102+
for dep in element._dependencies(_Scope.RUN):
2103+
dep.integrate(sandbox)
2104+
20882105
# Special configurations for non-isolated sandboxes
20892106
if not isolate:
20902107

‎tests/integration/shell.py‎

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
# pylint: disable=redefined-outer-name
1717

1818
import os
19+
from typing import Dict, List, Tuple
1920
import uuid
2021

22+
2123
import pytest
2224

2325
from buildstream import _yaml
2426
from buildstream._testing import cli_integration as cli # pylint: disable=unused-import
27+
from buildstream._testing.runcli import CliIntegration
2528
from buildstream._testing._utils.site import HAVE_SANDBOX, BUILDBOX_RUN
2629
from buildstream.exceptions import ErrorDomain
2730
from buildstream import utils
@@ -46,18 +49,35 @@
4649
# mount (tuple): A (host, target) tuple for the `--mount` option
4750
# element (str): The element to build and run a shell with
4851
# isolate (bool): Whether to pass --isolate to `bst shell`
49-
#
50-
def execute_shell(cli, project, command, *, config=None, mount=None, element="base.bst", isolate=False):
52+
# other_elements (list(str)): Other elements to stage in the sandbox
53+
def execute_shell(
54+
cli: CliIntegration,
55+
project: str,
56+
command: List[str],
57+
*,
58+
config: None | Dict = None,
59+
mount: Tuple[str, str] | None = None,
60+
element: str = "base.bst",
61+
isolate: bool = False,
62+
other_elements: List[str] | None = None
63+
):
5164
# Ensure the element is built
5265
result = cli.run_project_config(project=project, project_config=config, args=["build", element])
5366
assert result.exit_code == 0
67+
if other_elements is not None:
68+
for other_element in other_elements:
69+
result = cli.run_project_config(project=project, project_config=config, args=["build", other_element])
70+
assert result.exit_code == 0
5471

5572
args = ["shell"]
5673
if isolate:
5774
args += ["--isolate"]
5875
if mount is not None:
5976
host_path, target_path = mount
6077
args += ["--mount", host_path, target_path]
78+
if other_elements is not None:
79+
for other_element in other_elements:
80+
args += ["--with", other_element]
6181
args += [element, "--", *command]
6282

6383
return cli.run_project_config(project=project, project_config=config, args=args)
@@ -86,6 +106,28 @@ def test_executable(cli, datafiles):
86106
assert result.output == "Horseys!\n"
87107

88108

109+
# Test staging and running additional targets in the shell of the main target for debugging.
110+
@pytest.mark.datafiles(DATA_DIR)
111+
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
112+
def test_with_other_targets(cli, datafiles):
113+
project = str(datafiles)
114+
115+
# Show we can't cat in a shell for manual/import-file.bst
116+
result = execute_shell(cli, project, ["/bin/cat", "test.txt"], element="manual/import-file.bst")
117+
assert (
118+
result.exit_code == -1
119+
), "Shouldn't be able to read content of test.txt as manual/import-file.bst is a simple import element with no dependencies"
120+
121+
# Show we can now cat with base.bst in a shell for manual/import-file.bst
122+
result = execute_shell(
123+
cli, project, ["/bin/cat", "test.txt"], element="manual/import-file.bst", other_elements=["base.bst"]
124+
)
125+
assert (
126+
result.exit_code == 0
127+
), "Should be able to read content of test.txt as we now stage in base.bst that provides /bin/cat"
128+
assert result.output == "This is a test\n"
129+
130+
89131
# Test shell environment variable explicit assignments
90132
@pytest.mark.parametrize("animal", [("Horse"), ("Pony")])
91133
@pytest.mark.datafiles(DATA_DIR)

0 commit comments

Comments
 (0)