richardmaw-codethink pushed to branch richardmaw/shell-multi-stage at BuildStream / buildstream
Commits:
-
21b6ba36
by Richard Maw at 2018-12-11T17:56:26Z
-
10a5243b
by Richard Maw at 2018-12-11T17:56:26Z
-
f87b7fd3
by Richard Maw at 2018-12-11T17:56:26Z
-
2441ab39
by Richard Maw at 2018-12-11T17:56:26Z
-
87b660fa
by Richard Maw at 2018-12-11T17:56:26Z
-
f7954ef5
by Richard Maw at 2018-12-11T17:56:26Z
9 changed files:
- NEWS
- buildstream/_frontend/app.py
- buildstream/_frontend/cli.py
- buildstream/_stream.py
- buildstream/element.py
- + tests/integration/project/elements/shell/adds-bar.bst
- + tests/integration/project/elements/shell/adds-foo.bst
- tests/integration/project/elements/integration.bst → tests/integration/project/elements/shell/integration.bst
- tests/integration/shell.py
Changes:
... | ... | @@ -2,6 +2,12 @@ |
2 | 2 |
buildstream 1.3.1
|
3 | 3 |
=================
|
4 | 4 |
|
5 |
+ o `bst shell` learned to accept multiple elements for staging
|
|
6 |
+ provided the element's kind flags `BST_STAGE_INTEGRATES` as false.
|
|
7 |
+ |
|
8 |
+ As a side-effect it is no longer possible to intersperse options and flags
|
|
9 |
+ with arguments in the `bst shell` command-line.
|
|
10 |
+ |
|
5 | 11 |
o All elements must now be suffixed with `.bst`
|
6 | 12 |
Attempting to use an element that does not have the `.bst` extension,
|
7 | 13 |
will result in a warning.
|
... | ... | @@ -599,7 +599,7 @@ class App(): |
599 | 599 |
click.echo("\nDropping into an interactive shell in the failed build sandbox\n", err=True)
|
600 | 600 |
try:
|
601 | 601 |
prompt = self.shell_prompt(element)
|
602 |
- self.stream.shell(element, Scope.BUILD, prompt, isolate=True)
|
|
602 |
+ self.stream.shell([(element, Scope.BUILD)], prompt, isolate=True)
|
|
603 | 603 |
except BstError as e:
|
604 | 604 |
click.echo("Error while attempting to create interactive shell: {}".format(e), err=True)
|
605 | 605 |
elif choice == 'log':
|
... | ... | @@ -573,7 +573,7 @@ def show(app, elements, deps, except_, order, format_): |
573 | 573 |
##################################################################
|
574 | 574 |
@cli.command(short_help="Shell into an element's sandbox environment")
|
575 | 575 |
@click.option('--build', '-b', 'build_', is_flag=True, default=False,
|
576 |
- help='Stage dependencies and sources to build')
|
|
576 |
+ help='Stage dependencies and sources to build the first element')
|
|
577 | 577 |
@click.option('--sysroot', '-s', default=None,
|
578 | 578 |
type=click.Path(exists=True, file_okay=False, readable=True),
|
579 | 579 |
help="An existing sysroot")
|
... | ... | @@ -582,11 +582,11 @@ def show(app, elements, deps, except_, order, format_): |
582 | 582 |
help="Mount a file or directory into the sandbox")
|
583 | 583 |
@click.option('--isolate', is_flag=True, default=False,
|
584 | 584 |
help='Create an isolated build sandbox')
|
585 |
-@click.argument('element',
|
|
586 |
- type=click.Path(readable=False))
|
|
587 |
-@click.argument('command', type=click.STRING, nargs=-1)
|
|
585 |
+@click.argument('elements',
|
|
586 |
+ type=click.Path(readable=False), nargs=-1,
|
|
587 |
+ metavar="[ELEMENT]... [--] [COMMAND]...")
|
|
588 | 588 |
@click.pass_obj
|
589 |
-def shell(app, element, sysroot, mount, isolate, build_, command):
|
|
589 |
+def shell(app, elements, sysroot, mount, isolate, build_):
|
|
590 | 590 |
"""Run a command in the target element's sandbox environment
|
591 | 591 |
|
592 | 592 |
This will stage a temporary sysroot for running the target
|
... | ... | @@ -600,6 +600,15 @@ def shell(app, element, sysroot, mount, isolate, build_, command): |
600 | 600 |
directory or with a checkout of the given target, in order
|
601 | 601 |
to use a specific sysroot.
|
602 | 602 |
|
603 |
+ Multiple element target names ending in .bst may be provided,
|
|
604 |
+ optionally followed by the command to run in the shell.
|
|
605 |
+ |
|
606 |
+ The first argument that doesn't end in .bst is assumed to be the beginning of COMMAND.
|
|
607 |
+ |
|
608 |
+ A -- argument may be used to disambiguate between element target names and command arguments
|
|
609 |
+ and should be used when being driven as part of a script, to prevent mis-parsing,
|
|
610 |
+ in addition to a -- before the element list.
|
|
611 |
+ |
|
603 | 612 |
If no COMMAND is specified, the default is to attempt
|
604 | 613 |
to run an interactive shell.
|
605 | 614 |
"""
|
... | ... | @@ -607,13 +616,22 @@ def shell(app, element, sysroot, mount, isolate, build_, command): |
607 | 616 |
from .._project import HostMount
|
608 | 617 |
from .._pipeline import PipelineSelection
|
609 | 618 |
|
610 |
- if build_:
|
|
611 |
- scope = Scope.BUILD
|
|
612 |
- else:
|
|
613 |
- scope = Scope.RUN
|
|
619 |
+ targets = []
|
|
620 |
+ command = []
|
|
621 |
+ for i, arg in enumerate(elements):
|
|
622 |
+ if arg == '--':
|
|
623 |
+ command = elements[i + 1:]
|
|
624 |
+ break
|
|
625 |
+ if not arg.endswith('.bst'):
|
|
626 |
+ command = elements[i:]
|
|
627 |
+ break
|
|
628 |
+ targets.append(arg)
|
|
629 |
+ |
|
630 |
+ if not targets:
|
|
631 |
+ raise AppError('No elements specified to open a shell in')
|
|
614 | 632 |
|
615 | 633 |
with app.initialized():
|
616 |
- dependencies = app.stream.load_selection((element,), selection=PipelineSelection.NONE)
|
|
634 |
+ dependencies = app.stream.load_selection(targets, selection=PipelineSelection.NONE)
|
|
617 | 635 |
element = dependencies[0]
|
618 | 636 |
prompt = app.shell_prompt(element)
|
619 | 637 |
mounts = [
|
... | ... | @@ -621,7 +639,9 @@ def shell(app, element, sysroot, mount, isolate, build_, command): |
621 | 639 |
for host_path, path in mount
|
622 | 640 |
]
|
623 | 641 |
try:
|
624 |
- exitcode = app.stream.shell(element, scope, prompt,
|
|
642 |
+ elements = tuple((e, Scope.BUILD if i == 0 and build_ else Scope.RUN)
|
|
643 |
+ for (i, e) in enumerate(dependencies))
|
|
644 |
+ exitcode = app.stream.shell(elements, prompt,
|
|
625 | 645 |
directory=sysroot,
|
626 | 646 |
mounts=mounts,
|
627 | 647 |
isolate=isolate,
|
... | ... | @@ -25,15 +25,17 @@ import stat |
25 | 25 |
import shlex
|
26 | 26 |
import shutil
|
27 | 27 |
import tarfile
|
28 |
-from contextlib import contextmanager
|
|
28 |
+from contextlib import contextmanager, ExitStack
|
|
29 | 29 |
from tempfile import TemporaryDirectory
|
30 | 30 |
|
31 | 31 |
from ._exceptions import StreamError, ImplError, BstError, set_last_task_error
|
32 | 32 |
from ._message import Message, MessageType
|
33 |
-from ._scheduler import Scheduler, SchedStatus, TrackQueue, FetchQueue, BuildQueue, PullQueue, PushQueue
|
|
34 | 33 |
from ._pipeline import Pipeline, PipelineSelection
|
34 |
+from ._platform import Platform
|
|
35 |
+from .sandbox._config import SandboxConfig
|
|
36 |
+from ._scheduler import Scheduler, SchedStatus, TrackQueue, FetchQueue, BuildQueue, PullQueue, PushQueue
|
|
35 | 37 |
from . import utils, _yaml, _site
|
36 |
-from . import Scope, Consistency
|
|
38 |
+from . import SandboxFlags, Scope, Consistency
|
|
37 | 39 |
|
38 | 40 |
|
39 | 41 |
# Stream()
|
... | ... | @@ -117,8 +119,7 @@ class Stream(): |
117 | 119 |
# Run a shell
|
118 | 120 |
#
|
119 | 121 |
# Args:
|
120 |
- # element (Element): An Element object to run the shell for
|
|
121 |
- # scope (Scope): The scope for the shell (Scope.BUILD or Scope.RUN)
|
|
122 |
+ # elements (List of (Element, Scope) tuples): Elements to run the shell for and their dependency scopes.
|
|
122 | 123 |
# prompt (str): The prompt to display in the shell
|
123 | 124 |
# directory (str): A directory where an existing prestaged sysroot is expected, or None
|
124 | 125 |
# mounts (list of HostMount): Additional directories to mount into the sandbox
|
... | ... | @@ -128,7 +129,7 @@ class Stream(): |
128 | 129 |
# Returns:
|
129 | 130 |
# (int): The exit code of the launched shell
|
130 | 131 |
#
|
131 |
- def shell(self, element, scope, prompt, *,
|
|
132 |
+ def shell(self, elements, prompt, *,
|
|
132 | 133 |
directory=None,
|
133 | 134 |
mounts=None,
|
134 | 135 |
isolate=False,
|
... | ... | @@ -138,16 +139,118 @@ class Stream(): |
138 | 139 |
# in which case we just blindly trust the directory, using the element
|
139 | 140 |
# definitions to control the execution environment only.
|
140 | 141 |
if directory is None:
|
141 |
- missing_deps = [
|
|
142 |
- dep._get_full_name()
|
|
143 |
- for dep in self._pipeline.dependencies([element], scope)
|
|
144 |
- if not dep._cached()
|
|
145 |
- ]
|
|
142 |
+ missing_deps = []
|
|
143 |
+ for element, scope in elements:
|
|
144 |
+ missing_deps.extend(
|
|
145 |
+ dep._get_full_name()
|
|
146 |
+ for dep in self._pipeline.dependencies((element,), scope)
|
|
147 |
+ if not dep._cached())
|
|
146 | 148 |
if missing_deps:
|
147 | 149 |
raise StreamError("Elements need to be built or downloaded before staging a shell environment",
|
148 | 150 |
detail="\n".join(missing_deps))
|
149 | 151 |
|
150 |
- return element._shell(scope, directory, mounts=mounts, isolate=isolate, prompt=prompt, command=command)
|
|
152 |
+ # Assert we're not mixing virtual directory compatible
|
|
153 |
+ # and non-virtual directory compatible elements
|
|
154 |
+ if (any(e.BST_VIRTUAL_DIRECTORY for (e, _) in elements) and
|
|
155 |
+ not all(e.BST_VIRTUAL_DIRECTORY for (e, _) in elements)):
|
|
156 |
+ raise StreamError(
|
|
157 |
+ "Elements do not support multiple-element staging",
|
|
158 |
+ detail=("Multi-element staging is not supported" +
|
|
159 |
+ " because elements {} support BST_VIRTUAL_DIRECTORY and {} do not.").format(
|
|
160 |
+ ', '.join(e.name for (e, _) in elements if e.BST_VIRTUAL_DIRECTORY),
|
|
161 |
+ ', '.join(e.name for (e, _) in elements if not e.BST_VIRTUAL_DIRECTORY)))
|
|
162 |
+ |
|
163 |
+ with ExitStack() as stack:
|
|
164 |
+ # Creation logic duplicated from Element.__sandbox
|
|
165 |
+ # since most of it is creating the tmpdir
|
|
166 |
+ # and deciding whether to make a remote sandbox,
|
|
167 |
+ # which we don't want to.
|
|
168 |
+ if directory is None:
|
|
169 |
+ os.makedirs(self._context.builddir, exist_ok=True)
|
|
170 |
+ rootdir = stack.enter_context(TemporaryDirectory(dir=self._context.builddir))
|
|
171 |
+ else:
|
|
172 |
+ rootdir = directory
|
|
173 |
+ |
|
174 |
+ # SandboxConfig comes from project, element defaults and MetaElement sandbox config
|
|
175 |
+ # In the absence of it being exposed to other APIs and a merging strategy
|
|
176 |
+ # just make it from the project sandbox config.
|
|
177 |
+ sandbox_config = SandboxConfig(_yaml.node_get(self._project._sandbox, int, 'build-uid'),
|
|
178 |
+ _yaml.node_get(self._project._sandbox, int, 'build-gid'))
|
|
179 |
+ platform = Platform.get_platform()
|
|
180 |
+ sandbox = platform.create_sandbox(context=self._context,
|
|
181 |
+ project=self._project,
|
|
182 |
+ directory=rootdir,
|
|
183 |
+ stdout=None, stderr=None, config=sandbox_config,
|
|
184 |
+ bare_directory=directory is not None,
|
|
185 |
+ allow_real_directory=not any(e.BST_VIRTUAL_DIRECTORY
|
|
186 |
+ for (e, _) in elements))
|
|
187 |
+ |
|
188 |
+ # Configure the sandbox with the last element taking precedence for config.
|
|
189 |
+ for e, _ in elements:
|
|
190 |
+ e.configure_sandbox(sandbox)
|
|
191 |
+ |
|
192 |
+ # Stage contents if not passed --sysroot
|
|
193 |
+ if not directory:
|
|
194 |
+ if any(e.BST_STAGE_INTEGRATES for (e, _) in elements):
|
|
195 |
+ if len(elements) > 1:
|
|
196 |
+ raise StreamError(
|
|
197 |
+ "Elements do not support multiple-element staging",
|
|
198 |
+ detail=("Elements {} do not support multi-element staging " +
|
|
199 |
+ " because element kinds {} set BST_STAGE_INTEGRATES").format(
|
|
200 |
+ ', '.join(e.name for (e, _) in elements if e.BST_STAGE_INTEGRATES),
|
|
201 |
+ ', '.join(set(e.get_kind() for (e, _) in elements))))
|
|
202 |
+ elements[0][0].stage(sandbox)
|
|
203 |
+ else:
|
|
204 |
+ visited = {}
|
|
205 |
+ for e, scope in elements:
|
|
206 |
+ if scope is Scope.BUILD:
|
|
207 |
+ e.stage(sandbox, visited=visited)
|
|
208 |
+ else:
|
|
209 |
+ e.stage_dependency_artifacts(sandbox, scope, visited=visited)
|
|
210 |
+ |
|
211 |
+ visited = {}
|
|
212 |
+ for e, scope in elements:
|
|
213 |
+ e.integrate_dependency_artifacts(sandbox, scope, visited=visited)
|
|
214 |
+ |
|
215 |
+ environment = {}
|
|
216 |
+ for e, _ in elements:
|
|
217 |
+ environment.update(e.get_environment())
|
|
218 |
+ flags = SandboxFlags.INTERACTIVE | SandboxFlags.ROOT_READ_ONLY
|
|
219 |
+ shell_command, shell_environment, shell_host_files = self._project.get_shell_config()
|
|
220 |
+ environment['PS1'] = prompt
|
|
221 |
+ # Special configurations for non-isolated sandboxes
|
|
222 |
+ if not isolate:
|
|
223 |
+ |
|
224 |
+ # Open the network, and reuse calling uid/gid
|
|
225 |
+ #
|
|
226 |
+ flags |= SandboxFlags.NETWORK_ENABLED | SandboxFlags.INHERIT_UID
|
|
227 |
+ |
|
228 |
+ # Apply project defined environment vars to set for a shell
|
|
229 |
+ for key, value in _yaml.node_items(shell_environment):
|
|
230 |
+ environment[key] = value
|
|
231 |
+ |
|
232 |
+ # Setup any requested bind mounts
|
|
233 |
+ if mounts is None:
|
|
234 |
+ mounts = []
|
|
235 |
+ |
|
236 |
+ for mount in shell_host_files + mounts:
|
|
237 |
+ if not os.path.exists(mount.host_path):
|
|
238 |
+ if not mount.optional:
|
|
239 |
+ self._message(MessageType.WARN,
|
|
240 |
+ "Not mounting non-existing host file: {}".format(mount.host_path))
|
|
241 |
+ else:
|
|
242 |
+ sandbox.mark_directory(mount.path)
|
|
243 |
+ sandbox._set_mount_source(mount.path, mount.host_path)
|
|
244 |
+ |
|
245 |
+ if command:
|
|
246 |
+ argv = list(command)
|
|
247 |
+ else:
|
|
248 |
+ argv = shell_command
|
|
249 |
+ |
|
250 |
+ self._message(MessageType.STATUS, "Running command", detail=" ".join(argv))
|
|
251 |
+ |
|
252 |
+ # Run shells with network enabled and readonly root.
|
|
253 |
+ return sandbox.run(argv, flags, env=environment)
|
|
151 | 254 |
|
152 | 255 |
# build()
|
153 | 256 |
#
|
... | ... | @@ -75,7 +75,6 @@ Class Reference |
75 | 75 |
import os
|
76 | 76 |
import re
|
77 | 77 |
import stat
|
78 |
-import copy
|
|
79 | 78 |
from collections import OrderedDict
|
80 | 79 |
from collections.abc import Mapping
|
81 | 80 |
import contextlib
|
... | ... | @@ -1382,7 +1381,7 @@ class Element(Plugin): |
1382 | 1381 |
# is used to stage things by the `bst checkout` codepath
|
1383 | 1382 |
#
|
1384 | 1383 |
@contextmanager
|
1385 |
- def _prepare_sandbox(self, scope, directory, shell=False, integrate=True):
|
|
1384 |
+ def _prepare_sandbox(self, scope, directory, integrate=True):
|
|
1386 | 1385 |
# bst shell and bst checkout require a local sandbox.
|
1387 | 1386 |
bare_directory = True if directory else False
|
1388 | 1387 |
with self.__sandbox(directory, config=self.__sandbox_config, allow_remote=False,
|
... | ... | @@ -1393,20 +1392,15 @@ class Element(Plugin): |
1393 | 1392 |
|
1394 | 1393 |
# Stage something if we need it
|
1395 | 1394 |
if not directory:
|
1396 |
- if shell and scope == Scope.BUILD:
|
|
1397 |
- self.stage(sandbox)
|
|
1398 |
- if not self.BST_STAGE_INTEGRATES:
|
|
1395 |
+ # Stage deps in the sandbox root
|
|
1396 |
+ with self.timed_activity("Staging dependencies", silent_nested=True):
|
|
1397 |
+ self.stage_dependency_artifacts(sandbox, scope)
|
|
1398 |
+ |
|
1399 |
+ # Run any integration commands provided by the dependencies
|
|
1400 |
+ # once they are all staged and ready
|
|
1401 |
+ if integrate:
|
|
1402 |
+ with self.timed_activity("Integrating sandbox"):
|
|
1399 | 1403 |
self.integrate_dependency_artifacts(sandbox, scope)
|
1400 |
- else:
|
|
1401 |
- # Stage deps in the sandbox root
|
|
1402 |
- with self.timed_activity("Staging dependencies", silent_nested=True):
|
|
1403 |
- self.stage_dependency_artifacts(sandbox, scope)
|
|
1404 |
- |
|
1405 |
- # Run any integration commands provided by the dependencies
|
|
1406 |
- # once they are all staged and ready
|
|
1407 |
- if integrate:
|
|
1408 |
- with self.timed_activity("Integrating sandbox"):
|
|
1409 |
- self.integrate_dependency_artifacts(sandbox, scope)
|
|
1410 | 1404 |
|
1411 | 1405 |
yield sandbox
|
1412 | 1406 |
|
... | ... | @@ -1885,71 +1879,6 @@ class Element(Plugin): |
1885 | 1879 |
# Notify successful upload
|
1886 | 1880 |
return True
|
1887 | 1881 |
|
1888 |
- # _shell():
|
|
1889 |
- #
|
|
1890 |
- # Connects the terminal with a shell running in a staged
|
|
1891 |
- # environment
|
|
1892 |
- #
|
|
1893 |
- # Args:
|
|
1894 |
- # scope (Scope): Either BUILD or RUN scopes are valid, or None
|
|
1895 |
- # directory (str): A directory to an existing sandbox, or None
|
|
1896 |
- # mounts (list): A list of (str, str) tuples, representing host/target paths to mount
|
|
1897 |
- # isolate (bool): Whether to isolate the environment like we do in builds
|
|
1898 |
- # prompt (str): A suitable prompt string for PS1
|
|
1899 |
- # command (list): An argv to launch in the sandbox
|
|
1900 |
- #
|
|
1901 |
- # Returns: Exit code
|
|
1902 |
- #
|
|
1903 |
- # If directory is not specified, one will be staged using scope
|
|
1904 |
- def _shell(self, scope=None, directory=None, *, mounts=None, isolate=False, prompt=None, command=None):
|
|
1905 |
- |
|
1906 |
- with self._prepare_sandbox(scope, directory, shell=True) as sandbox:
|
|
1907 |
- environment = self.get_environment()
|
|
1908 |
- environment = copy.copy(environment)
|
|
1909 |
- flags = SandboxFlags.INTERACTIVE | SandboxFlags.ROOT_READ_ONLY
|
|
1910 |
- |
|
1911 |
- # Fetch the main toplevel project, in case this is a junctioned
|
|
1912 |
- # subproject, we want to use the rules defined by the main one.
|
|
1913 |
- context = self._get_context()
|
|
1914 |
- project = context.get_toplevel_project()
|
|
1915 |
- shell_command, shell_environment, shell_host_files = project.get_shell_config()
|
|
1916 |
- |
|
1917 |
- if prompt is not None:
|
|
1918 |
- environment['PS1'] = prompt
|
|
1919 |
- |
|
1920 |
- # Special configurations for non-isolated sandboxes
|
|
1921 |
- if not isolate:
|
|
1922 |
- |
|
1923 |
- # Open the network, and reuse calling uid/gid
|
|
1924 |
- #
|
|
1925 |
- flags |= SandboxFlags.NETWORK_ENABLED | SandboxFlags.INHERIT_UID
|
|
1926 |
- |
|
1927 |
- # Apply project defined environment vars to set for a shell
|
|
1928 |
- for key, value in _yaml.node_items(shell_environment):
|
|
1929 |
- environment[key] = value
|
|
1930 |
- |
|
1931 |
- # Setup any requested bind mounts
|
|
1932 |
- if mounts is None:
|
|
1933 |
- mounts = []
|
|
1934 |
- |
|
1935 |
- for mount in shell_host_files + mounts:
|
|
1936 |
- if not os.path.exists(mount.host_path):
|
|
1937 |
- if not mount.optional:
|
|
1938 |
- self.warn("Not mounting non-existing host file: {}".format(mount.host_path))
|
|
1939 |
- else:
|
|
1940 |
- sandbox.mark_directory(mount.path)
|
|
1941 |
- sandbox._set_mount_source(mount.path, mount.host_path)
|
|
1942 |
- |
|
1943 |
- if command:
|
|
1944 |
- argv = [arg for arg in command]
|
|
1945 |
- else:
|
|
1946 |
- argv = shell_command
|
|
1947 |
- |
|
1948 |
- self.status("Running command", detail=" ".join(argv))
|
|
1949 |
- |
|
1950 |
- # Run shells with network enabled and readonly root.
|
|
1951 |
- return sandbox.run(argv, flags, env=environment)
|
|
1952 |
- |
|
1953 | 1882 |
# _open_workspace():
|
1954 | 1883 |
#
|
1955 | 1884 |
# "Open" a workspace for this element
|
1 |
+kind: manual
|
|
2 |
+depends:
|
|
3 |
+- base.bst
|
|
4 |
+ |
|
5 |
+config:
|
|
6 |
+ install-commands:
|
|
7 |
+ - |
|
|
8 |
+ install -D -m775 /proc/self/fd/0 %{install-root}%{bindir}/bar <<\EOF
|
|
9 |
+ #!/bin/sh
|
|
10 |
+ echo bar
|
|
11 |
+ EOF
|
1 |
+kind: manual
|
|
2 |
+depends:
|
|
3 |
+- base.bst
|
|
4 |
+ |
|
5 |
+config:
|
|
6 |
+ install-commands:
|
|
7 |
+ - |
|
|
8 |
+ install -D -m775 /proc/self/fd/0 %{install-root}%{bindir}/foo <<\EOF
|
|
9 |
+ #!/bin/sh
|
|
10 |
+ echo foo
|
|
11 |
+ EOF
|
... | ... | @@ -28,11 +28,17 @@ DATA_DIR = os.path.join( |
28 | 28 |
# config (dict): A project.conf dictionary to composite over the default
|
29 | 29 |
# mount (tuple): A (host, target) tuple for the `--mount` option
|
30 | 30 |
# element (str): The element to build and run a shell with
|
31 |
+# elements (list): Other elements to build and run a shell with
|
|
31 | 32 |
# isolate (bool): Whether to pass --isolate to `bst shell`
|
32 | 33 |
#
|
33 |
-def execute_shell(cli, project, command, *, config=None, mount=None, element='base.bst', isolate=False):
|
|
34 |
+def execute_shell(cli, project, command, *, config=None, mount=None, elements=None, isolate=False):
|
|
34 | 35 |
# Ensure the element is built
|
35 |
- result = cli.run(project=project, project_config=config, args=['build', element])
|
|
36 |
+ if elements is None:
|
|
37 |
+ elements = ('base.bst',)
|
|
38 |
+ |
|
39 |
+ args = ['build', '--']
|
|
40 |
+ args.extend(elements)
|
|
41 |
+ result = cli.run(project=project, project_config=config, args=args)
|
|
36 | 42 |
assert result.exit_code == 0
|
37 | 43 |
|
38 | 44 |
args = ['shell']
|
... | ... | @@ -41,7 +47,9 @@ def execute_shell(cli, project, command, *, config=None, mount=None, element='ba |
41 | 47 |
if mount is not None:
|
42 | 48 |
host_path, target_path = mount
|
43 | 49 |
args += ['--mount', host_path, target_path]
|
44 |
- args += [element, '--'] + command
|
|
50 |
+ args.append('--')
|
|
51 |
+ args.extend(elements)
|
|
52 |
+ args += ['--'] + command
|
|
45 | 53 |
|
46 | 54 |
return cli.run(project=project, project_config=config, args=args)
|
47 | 55 |
|
... | ... | @@ -158,7 +166,7 @@ def test_no_shell(cli, tmpdir, datafiles): |
158 | 166 |
os.makedirs(os.path.dirname(os.path.join(element_path, element_name)), exist_ok=True)
|
159 | 167 |
_yaml.dump(element, os.path.join(element_path, element_name))
|
160 | 168 |
|
161 |
- result = execute_shell(cli, project, ['/bin/echo', 'Pegasissies!'], element=element_name)
|
|
169 |
+ result = execute_shell(cli, project, ['/bin/echo', 'Pegasissies!'], elements=(element_name,))
|
|
162 | 170 |
assert result.exit_code == 0
|
163 | 171 |
assert result.output == "Pegasissies!\n"
|
164 | 172 |
|
... | ... | @@ -345,11 +353,56 @@ def test_sysroot(cli, tmpdir, datafiles): |
345 | 353 |
|
346 | 354 |
|
347 | 355 |
# Test system integration commands can access devices in /dev
|
356 |
+@pytest.mark.integration
|
|
348 | 357 |
@pytest.mark.datafiles(DATA_DIR)
|
349 | 358 |
@pytest.mark.skipif(IS_LINUX and not HAVE_BWRAP, reason='Only available with bubblewrap on Linux')
|
350 | 359 |
def test_integration_devices(cli, tmpdir, datafiles):
|
351 | 360 |
project = os.path.join(datafiles.dirname, datafiles.basename)
|
352 |
- element_name = 'integration.bst'
|
|
361 |
+ element_name = 'shell/integration.bst'
|
|
362 |
+ |
|
363 |
+ result = execute_shell(cli, project, ["true"], elements=(element_name,))
|
|
364 |
+ assert result.exit_code == 0
|
|
365 |
+ |
|
366 |
+ |
|
367 |
+# Test multiple element shell
|
|
368 |
+@pytest.mark.integration
|
|
369 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
370 |
+@pytest.mark.skipif(IS_LINUX and not HAVE_BWRAP, reason='Only available with bubblewrap on Linux')
|
|
371 |
+def test_shell_multiple_elements(cli, tmpdir, datafiles):
|
|
372 |
+ project = os.path.join(datafiles.dirname, datafiles.basename)
|
|
373 |
+ |
|
374 |
+ result = execute_shell(cli, project, ["sh", "-c", "foo && bar"],
|
|
375 |
+ elements=["shell/adds-foo.bst", "shell/adds-bar.bst"])
|
|
376 |
+ assert result.exit_code == 0
|
|
377 |
+ |
|
378 |
+ |
|
379 |
+# Test multiple element build shell
|
|
380 |
+@pytest.mark.integration
|
|
381 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
382 |
+@pytest.mark.skipif(IS_LINUX and not HAVE_BWRAP, reason='Only available with bubblewrap on Linux')
|
|
383 |
+def test_shell_multiple_workspace(cli, tmpdir, datafiles):
|
|
384 |
+ project = os.path.join(datafiles.dirname, datafiles.basename)
|
|
385 |
+ elements = {'workspace/workspace-mount.bst': os.path.join(cli.directory, 'workspace-mount'),
|
|
386 |
+ 'make/makehello.bst': os.path.join(cli.directory, 'makehello')}
|
|
387 |
+ |
|
388 |
+ for element, workspace in elements.items():
|
|
389 |
+ res = cli.run(project=project, args=['workspace', 'open', element, '--directory', workspace])
|
|
390 |
+ assert res.exit_code == 0
|
|
391 |
+ |
|
392 |
+ for workspace in elements.values():
|
|
393 |
+ with open(os.path.join(workspace, "workspace-exists"), "w") as f:
|
|
394 |
+ pass
|
|
395 |
+ |
|
396 |
+ # Ensure the dependencies of our build failing element are built
|
|
397 |
+ result = cli.run(project=project, args=['build', 'base.bst', 'make/makehello.bst'])
|
|
398 |
+ assert result.exit_code == 0
|
|
353 | 399 |
|
354 |
- result = execute_shell(cli, project, ["true"], element=element_name)
|
|
400 |
+ # Test that only the first workspace exists, since only the first element may be staged for build,
|
|
401 |
+ # additional elements may only be staged as extra dependencies.
|
|
402 |
+ args = ['shell', '--build', '--'] + list(elements)
|
|
403 |
+ args += ['--', 'sh', '-c',
|
|
404 |
+ 'test -e /buildstream/test/workspace/workspace-mount.bst/workspace-exists && \
|
|
405 |
+ test ! -e /buildstream/test/make/makehello.bst/workspace-exists']
|
|
406 |
+ result = cli.run(project=project, args=args)
|
|
355 | 407 |
assert result.exit_code == 0
|
408 |
+ assert result.output == ''
|