diff options
author | Richard Maw <richard.maw@codethink.co.uk> | 2018-10-29 13:42:52 +0000 |
---|---|---|
committer | Richard Maw <richard.maw@codethink.co.uk> | 2018-12-11 17:56:26 +0000 |
commit | 21b6ba36a7fdc7eb68dc0fede901f18b4b7fcff2 (patch) | |
tree | 8a4a25baf62cef996d9afeb420c07b4879909d67 | |
parent | 5e5792642ec043aacea3169402b7e10881a35c74 (diff) | |
download | buildstream-21b6ba36a7fdc7eb68dc0fede901f18b4b7fcff2.tar.gz |
_stream: Move shell logic from Element and support multiple elements
There's redundancy in pre-staging sandbox creation logic
with Element._sandbox doing something similar,
but hopefully not too much.
-rw-r--r-- | buildstream/_frontend/app.py | 2 | ||||
-rw-r--r-- | buildstream/_frontend/cli.py | 2 | ||||
-rw-r--r-- | buildstream/_stream.py | 127 |
3 files changed, 117 insertions, 14 deletions
diff --git a/buildstream/_frontend/app.py b/buildstream/_frontend/app.py index 4094eec17..671eefa37 100644 --- a/buildstream/_frontend/app.py +++ b/buildstream/_frontend/app.py @@ -599,7 +599,7 @@ class App(): click.echo("\nDropping into an interactive shell in the failed build sandbox\n", err=True) try: prompt = self.shell_prompt(element) - self.stream.shell(element, Scope.BUILD, prompt, isolate=True) + self.stream.shell([(element, Scope.BUILD)], prompt, isolate=True) except BstError as e: click.echo("Error while attempting to create interactive shell: {}".format(e), err=True) elif choice == 'log': diff --git a/buildstream/_frontend/cli.py b/buildstream/_frontend/cli.py index ef0dadb13..0b2ebc750 100644 --- a/buildstream/_frontend/cli.py +++ b/buildstream/_frontend/cli.py @@ -621,7 +621,7 @@ def shell(app, element, sysroot, mount, isolate, build_, command): for host_path, path in mount ] try: - exitcode = app.stream.shell(element, scope, prompt, + exitcode = app.stream.shell([(element, scope)], prompt, directory=sysroot, mounts=mounts, isolate=isolate, diff --git a/buildstream/_stream.py b/buildstream/_stream.py index 6a3d2c7fc..aeda13651 100644 --- a/buildstream/_stream.py +++ b/buildstream/_stream.py @@ -25,15 +25,17 @@ import stat import shlex import shutil import tarfile -from contextlib import contextmanager +from contextlib import contextmanager, ExitStack from tempfile import TemporaryDirectory from ._exceptions import StreamError, ImplError, BstError, set_last_task_error from ._message import Message, MessageType -from ._scheduler import Scheduler, SchedStatus, TrackQueue, FetchQueue, BuildQueue, PullQueue, PushQueue from ._pipeline import Pipeline, PipelineSelection +from ._platform import Platform +from .sandbox._config import SandboxConfig +from ._scheduler import Scheduler, SchedStatus, TrackQueue, FetchQueue, BuildQueue, PullQueue, PushQueue from . import utils, _yaml, _site -from . import Scope, Consistency +from . import SandboxFlags, Scope, Consistency # Stream() @@ -117,8 +119,7 @@ class Stream(): # Run a shell # # Args: - # element (Element): An Element object to run the shell for - # scope (Scope): The scope for the shell (Scope.BUILD or Scope.RUN) + # elements (List of (Element, Scope) tuples): Elements to run the shell for and their dependency scopes. # prompt (str): The prompt to display in the shell # directory (str): A directory where an existing prestaged sysroot is expected, or None # mounts (list of HostMount): Additional directories to mount into the sandbox @@ -128,7 +129,7 @@ class Stream(): # Returns: # (int): The exit code of the launched shell # - def shell(self, element, scope, prompt, *, + def shell(self, elements, prompt, *, directory=None, mounts=None, isolate=False, @@ -138,16 +139,118 @@ class Stream(): # in which case we just blindly trust the directory, using the element # definitions to control the execution environment only. if directory is None: - missing_deps = [ - dep._get_full_name() - for dep in self._pipeline.dependencies([element], scope) - if not dep._cached() - ] + missing_deps = [] + for element, scope in elements: + missing_deps.extend( + dep._get_full_name() + for dep in self._pipeline.dependencies((element,), scope) + if not dep._cached()) if missing_deps: raise StreamError("Elements need to be built or downloaded before staging a shell environment", detail="\n".join(missing_deps)) - return element._shell(scope, directory, mounts=mounts, isolate=isolate, prompt=prompt, command=command) + # Assert we're not mixing virtual directory compatible + # and non-virtual directory compatible elements + if (any(e.BST_VIRTUAL_DIRECTORY for (e, _) in elements) and + not all(e.BST_VIRTUAL_DIRECTORY for (e, _) in elements)): + raise StreamError( + "Elements do not support multiple-element staging", + detail=("Multi-element staging is not supported" + + " because elements {} support BST_VIRTUAL_DIRECTORY and {} do not.").format( + ', '.join(e.name for (e, _) in elements if e.BST_VIRTUAL_DIRECTORY), + ', '.join(e.name for (e, _) in elements if not e.BST_VIRTUAL_DIRECTORY))) + + with ExitStack() as stack: + # Creation logic duplicated from Element.__sandbox + # since most of it is creating the tmpdir + # and deciding whether to make a remote sandbox, + # which we don't want to. + if directory is None: + os.makedirs(self._context.builddir, exist_ok=True) + rootdir = stack.enter_context(TemporaryDirectory(dir=self._context.builddir)) + else: + rootdir = directory + + # SandboxConfig comes from project, element defaults and MetaElement sandbox config + # In the absence of it being exposed to other APIs and a merging strategy + # just make it from the project sandbox config. + sandbox_config = SandboxConfig(_yaml.node_get(self._project._sandbox, int, 'build-uid'), + _yaml.node_get(self._project._sandbox, int, 'build-gid')) + platform = Platform.get_platform() + sandbox = platform.create_sandbox(context=self._context, + project=self._project, + directory=rootdir, + stdout=None, stderr=None, config=sandbox_config, + bare_directory=directory is not None, + allow_real_directory=not any(e.BST_VIRTUAL_DIRECTORY + for (e, _) in elements)) + + # Configure the sandbox with the last element taking precedence for config. + for e, _ in elements: + e.configure_sandbox(sandbox) + + # Stage contents if not passed --sysroot + if not directory: + if any(e.BST_STAGE_INTEGRATES for (e, _) in elements): + if len(elements) > 1: + raise StreamError( + "Elements do not support multiple-element staging", + detail=("Elements {} do not support multi-element staging " + + " because element kinds {} set BST_STAGE_INTEGRATES").format( + ', '.join(e.name for (e, _) in elements if e.BST_STAGE_INTEGRATES), + ', '.join(set(e.get_kind() for (e, _) in elements)))) + elements[0][0].stage(sandbox) + else: + visited = {} + for e, scope in elements: + if scope is Scope.BUILD: + e.stage(sandbox, visited=visited) + else: + e.stage_dependency_artifacts(sandbox, scope, visited=visited) + + visited = {} + for e, scope in elements: + e.integrate_dependency_artifacts(sandbox, scope, visited=visited) + + environment = {} + for e, _ in elements: + environment.update(e.get_environment()) + flags = SandboxFlags.INTERACTIVE | SandboxFlags.ROOT_READ_ONLY + shell_command, shell_environment, shell_host_files = self._project.get_shell_config() + environment['PS1'] = prompt + # Special configurations for non-isolated sandboxes + if not isolate: + + # Open the network, and reuse calling uid/gid + # + flags |= SandboxFlags.NETWORK_ENABLED | SandboxFlags.INHERIT_UID + + # Apply project defined environment vars to set for a shell + for key, value in _yaml.node_items(shell_environment): + environment[key] = value + + # Setup any requested bind mounts + if mounts is None: + mounts = [] + + for mount in shell_host_files + mounts: + if not os.path.exists(mount.host_path): + if not mount.optional: + self._message(MessageType.WARN, + "Not mounting non-existing host file: {}".format(mount.host_path)) + else: + sandbox.mark_directory(mount.path) + sandbox._set_mount_source(mount.path, mount.host_path) + + if command: + argv = list(command) + else: + argv = shell_command + + self._message(MessageType.STATUS, "Running command", detail=" ".join(argv)) + + # Run shells with network enabled and readonly root. + return sandbox.run(argv, flags, env=environment) # build() # |