Jürg Billeter pushed to branch juerg/command-batching at BuildStream / buildstream
Commits:
-
0a0a44a9
by Jürg Billeter at 2018-11-19T08:10:58Z
-
4a410481
by Jürg Billeter at 2018-11-19T08:10:58Z
-
32743857
by Jürg Billeter at 2018-11-19T08:10:58Z
-
7bce44f2
by Jürg Billeter at 2018-11-19T08:18:38Z
-
4a2aed3b
by Jürg Billeter at 2018-11-19T08:18:38Z
-
3f2fc9ef
by Jürg Billeter at 2018-11-19T08:18:38Z
-
69766dd8
by Jürg Billeter at 2018-11-19T08:18:38Z
-
b72aabe3
by Jürg Billeter at 2018-11-19T08:18:38Z
-
88e34468
by Jürg Billeter at 2018-11-19T08:18:38Z
-
905b9c3a
by Jürg Billeter at 2018-11-19T08:18:38Z
-
af9d3e25
by Jürg Billeter at 2018-11-19T08:18:38Z
-
9b96db38
by Jürg Billeter at 2018-11-19T08:18:38Z
10 changed files:
- NEWS
- buildstream/_exceptions.py
- buildstream/buildelement.py
- buildstream/element.py
- buildstream/plugins/elements/compose.py
- buildstream/sandbox/_sandboxremote.py
- buildstream/sandbox/sandbox.py
- buildstream/scriptelement.py
- buildstream/utils.py
- tests/integration/sandbox-bwrap.py
Changes:
| ... | ... | @@ -55,6 +55,9 @@ buildstream 1.3.1 |
| 55 | 55 |
with cached artifacts, only 'complete' elements can be pushed. If the element
|
| 56 | 56 |
is expected to have a populated build tree then it must be cached before pushing.
|
| 57 | 57 |
|
| 58 |
+ o Add sandbox API for command batching and use it for build, script, and
|
|
| 59 |
+ compose elements.
|
|
| 60 |
+ |
|
| 58 | 61 |
|
| 59 | 62 |
=================
|
| 60 | 63 |
buildstream 1.1.5
|
| ... | ... | @@ -262,9 +262,11 @@ class PlatformError(BstError): |
| 262 | 262 |
# Raised when errors are encountered by the sandbox implementation
|
| 263 | 263 |
#
|
| 264 | 264 |
class SandboxError(BstError):
|
| 265 |
- def __init__(self, message, reason=None):
|
|
| 265 |
+ def __init__(self, message, reason=None, collect=None):
|
|
| 266 | 266 |
super().__init__(message, domain=ErrorDomain.SANDBOX, reason=reason)
|
| 267 | 267 |
|
| 268 |
+ self.collect = collect
|
|
| 269 |
+ |
|
| 268 | 270 |
|
| 269 | 271 |
# ArtifactError
|
| 270 | 272 |
#
|
| ... | ... | @@ -127,8 +127,9 @@ artifact collection purposes. |
| 127 | 127 |
"""
|
| 128 | 128 |
|
| 129 | 129 |
import os
|
| 130 |
-from . import Element, Scope, ElementError
|
|
| 130 |
+from . import Element, Scope
|
|
| 131 | 131 |
from . import SandboxFlags
|
| 132 |
+from . import utils
|
|
| 132 | 133 |
|
| 133 | 134 |
|
| 134 | 135 |
# This list is preserved because of an unfortunate situation, we
|
| ... | ... | @@ -207,6 +208,9 @@ class BuildElement(Element): |
| 207 | 208 |
# Setup environment
|
| 208 | 209 |
sandbox.set_environment(self.get_environment())
|
| 209 | 210 |
|
| 211 |
+ # Active sandbox batch context manager
|
|
| 212 |
+ self.__sandbox_batch_cm = None # pylint: disable=attribute-defined-outside-init
|
|
| 213 |
+ |
|
| 210 | 214 |
def stage(self, sandbox):
|
| 211 | 215 |
|
| 212 | 216 |
# Stage deps in the sandbox root
|
| ... | ... | @@ -215,7 +219,7 @@ class BuildElement(Element): |
| 215 | 219 |
|
| 216 | 220 |
# Run any integration commands provided by the dependencies
|
| 217 | 221 |
# once they are all staged and ready
|
| 218 |
- with self.timed_activity("Integrating sandbox"):
|
|
| 222 |
+ with sandbox.batch(0, label="Integrating sandbox"):
|
|
| 219 | 223 |
for dep in self.dependencies(Scope.BUILD):
|
| 220 | 224 |
dep.integrate(sandbox)
|
| 221 | 225 |
|
| ... | ... | @@ -223,16 +227,24 @@ class BuildElement(Element): |
| 223 | 227 |
self.stage_sources(sandbox, self.get_variable('build-root'))
|
| 224 | 228 |
|
| 225 | 229 |
def assemble(self, sandbox):
|
| 230 |
+ # Use the batch context manager from prepare(), if available
|
|
| 231 |
+ batch_cm = self.__sandbox_batch_cm
|
|
| 232 |
+ self.__sandbox_batch_cm = None # pylint: disable=attribute-defined-outside-init
|
|
| 226 | 233 |
|
| 227 |
- # Run commands
|
|
| 228 |
- for command_name in _command_steps:
|
|
| 229 |
- commands = self.__commands[command_name]
|
|
| 230 |
- if not commands or command_name == 'configure-commands':
|
|
| 231 |
- continue
|
|
| 234 |
+ if not batch_cm:
|
|
| 235 |
+ batch_cm = sandbox.batch(SandboxFlags.ROOT_READ_ONLY,
|
|
| 236 |
+ collect=self.get_variable('install-root'))
|
|
| 237 |
+ |
|
| 238 |
+ with batch_cm:
|
|
| 239 |
+ # Run commands
|
|
| 240 |
+ for command_name in _command_steps:
|
|
| 241 |
+ commands = self.__commands[command_name]
|
|
| 242 |
+ if not commands or command_name == 'configure-commands':
|
|
| 243 |
+ continue
|
|
| 232 | 244 |
|
| 233 |
- with self.timed_activity("Running {}".format(command_name)):
|
|
| 234 |
- for cmd in commands:
|
|
| 235 |
- self.__run_command(sandbox, cmd, command_name)
|
|
| 245 |
+ with sandbox.batch(SandboxFlags.ROOT_READ_ONLY, label="Running {}".format(command_name)):
|
|
| 246 |
+ for cmd in commands:
|
|
| 247 |
+ self.__run_command(sandbox, cmd, command_name)
|
|
| 236 | 248 |
|
| 237 | 249 |
# %{install-root}/%{build-root} should normally not be written
|
| 238 | 250 |
# to - if an element later attempts to stage to a location
|
| ... | ... | @@ -252,11 +264,20 @@ class BuildElement(Element): |
| 252 | 264 |
return self.get_variable('install-root')
|
| 253 | 265 |
|
| 254 | 266 |
def prepare(self, sandbox):
|
| 255 |
- commands = self.__commands['configure-commands']
|
|
| 256 |
- if commands:
|
|
| 257 |
- with self.timed_activity("Running configure-commands"):
|
|
| 258 |
- for cmd in commands:
|
|
| 259 |
- self.__run_command(sandbox, cmd, 'configure-commands')
|
|
| 267 |
+ batch_cm = sandbox.batch(SandboxFlags.ROOT_READ_ONLY,
|
|
| 268 |
+ collect=self.get_variable('install-root'))
|
|
| 269 |
+ |
|
| 270 |
+ # Allow use of single batch context manager across prepare and assemble
|
|
| 271 |
+ batch_cm = utils._SplitContextManager(batch_cm)
|
|
| 272 |
+ |
|
| 273 |
+ with batch_cm:
|
|
| 274 |
+ commands = self.__commands['configure-commands']
|
|
| 275 |
+ if commands:
|
|
| 276 |
+ with sandbox.batch(SandboxFlags.ROOT_READ_ONLY, label="Running configure-commands"):
|
|
| 277 |
+ for cmd in commands:
|
|
| 278 |
+ self.__run_command(sandbox, cmd, 'configure-commands')
|
|
| 279 |
+ |
|
| 280 |
+ self.__sandbox_batch_cm = batch_cm # pylint: disable=attribute-defined-outside-init
|
|
| 260 | 281 |
|
| 261 | 282 |
def generate_script(self):
|
| 262 | 283 |
script = ""
|
| ... | ... | @@ -282,13 +303,9 @@ class BuildElement(Element): |
| 282 | 303 |
return commands
|
| 283 | 304 |
|
| 284 | 305 |
def __run_command(self, sandbox, cmd, cmd_name):
|
| 285 |
- self.status("Running {}".format(cmd_name), detail=cmd)
|
|
| 286 |
- |
|
| 287 | 306 |
# Note the -e switch to 'sh' means to exit with an error
|
| 288 | 307 |
# if any untested command fails.
|
| 289 | 308 |
#
|
| 290 |
- exitcode = sandbox.run(['sh', '-c', '-e', cmd + '\n'],
|
|
| 291 |
- SandboxFlags.ROOT_READ_ONLY)
|
|
| 292 |
- if exitcode != 0:
|
|
| 293 |
- raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode),
|
|
| 294 |
- collect=self.get_variable('install-root'))
|
|
| 309 |
+ sandbox.run(['sh', '-c', '-e', cmd + '\n'],
|
|
| 310 |
+ SandboxFlags.ROOT_READ_ONLY,
|
|
| 311 |
+ label=cmd)
|
| ... | ... | @@ -769,13 +769,13 @@ class Element(Plugin): |
| 769 | 769 |
environment = self.get_environment()
|
| 770 | 770 |
|
| 771 | 771 |
if bstdata is not None:
|
| 772 |
- commands = self.node_get_member(bstdata, list, 'integration-commands', [])
|
|
| 773 |
- for i in range(len(commands)):
|
|
| 774 |
- cmd = self.node_subst_list_element(bstdata, 'integration-commands', [i])
|
|
| 775 |
- self.status("Running integration command", detail=cmd)
|
|
| 776 |
- exitcode = sandbox.run(['sh', '-e', '-c', cmd], 0, env=environment, cwd='/')
|
|
| 777 |
- if exitcode != 0:
|
|
| 778 |
- raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode))
|
|
| 772 |
+ with sandbox.batch(0):
|
|
| 773 |
+ commands = self.node_get_member(bstdata, list, 'integration-commands', [])
|
|
| 774 |
+ for i in range(len(commands)):
|
|
| 775 |
+ cmd = self.node_subst_list_element(bstdata, 'integration-commands', [i])
|
|
| 776 |
+ |
|
| 777 |
+ sandbox.run(['sh', '-e', '-c', cmd], 0, env=environment, cwd='/',
|
|
| 778 |
+ label=cmd)
|
|
| 779 | 779 |
|
| 780 | 780 |
def stage_sources(self, sandbox, directory):
|
| 781 | 781 |
"""Stage this element's sources to a directory in the sandbox
|
| ... | ... | @@ -1586,8 +1586,7 @@ class Element(Plugin): |
| 1586 | 1586 |
self.warn("Failed to preserve workspace state for failed build sysroot: {}"
|
| 1587 | 1587 |
.format(e))
|
| 1588 | 1588 |
|
| 1589 |
- if isinstance(e, ElementError):
|
|
| 1590 |
- collect = e.collect # pylint: disable=no-member
|
|
| 1589 |
+ collect = getattr(e, 'collect', None)
|
|
| 1591 | 1590 |
|
| 1592 | 1591 |
self.__set_build_result(success=False, description=str(e), detail=e.detail)
|
| 1593 | 1592 |
raise
|
| ... | ... | @@ -2073,7 +2072,12 @@ class Element(Plugin): |
| 2073 | 2072 |
self.prepare(sandbox)
|
| 2074 | 2073 |
|
| 2075 | 2074 |
if workspace:
|
| 2076 |
- workspace.prepared = True
|
|
| 2075 |
+ def mark_workspace_prepared():
|
|
| 2076 |
+ workspace.prepared = True
|
|
| 2077 |
+ |
|
| 2078 |
+ # Defer workspace.prepared setting until pending batch commands
|
|
| 2079 |
+ # have been executed.
|
|
| 2080 |
+ sandbox._callback(mark_workspace_prepared)
|
|
| 2077 | 2081 |
|
| 2078 | 2082 |
def __is_cached(self, keystrength):
|
| 2079 | 2083 |
if keystrength is None:
|
| ... | ... | @@ -2156,6 +2160,7 @@ class Element(Plugin): |
| 2156 | 2160 |
|
| 2157 | 2161 |
sandbox = SandboxRemote(context, project,
|
| 2158 | 2162 |
directory,
|
| 2163 |
+ plugin=self,
|
|
| 2159 | 2164 |
stdout=stdout,
|
| 2160 | 2165 |
stderr=stderr,
|
| 2161 | 2166 |
config=config,
|
| ... | ... | @@ -2174,6 +2179,7 @@ class Element(Plugin): |
| 2174 | 2179 |
|
| 2175 | 2180 |
sandbox = platform.create_sandbox(context, project,
|
| 2176 | 2181 |
directory,
|
| 2182 |
+ plugin=self,
|
|
| 2177 | 2183 |
stdout=stdout,
|
| 2178 | 2184 |
stderr=stderr,
|
| 2179 | 2185 |
config=config,
|
| ... | ... | @@ -122,8 +122,9 @@ class ComposeElement(Element): |
| 122 | 122 |
snapshot = set(vbasedir.list_relative_paths())
|
| 123 | 123 |
vbasedir.mark_unmodified()
|
| 124 | 124 |
|
| 125 |
- for dep in self.dependencies(Scope.BUILD):
|
|
| 126 |
- dep.integrate(sandbox)
|
|
| 125 |
+ with sandbox.batch(0):
|
|
| 126 |
+ for dep in self.dependencies(Scope.BUILD):
|
|
| 127 |
+ dep.integrate(sandbox)
|
|
| 127 | 128 |
|
| 128 | 129 |
if require_split:
|
| 129 | 130 |
# Calculate added, modified and removed files
|
| ... | ... | @@ -19,11 +19,13 @@ |
| 19 | 19 |
# Jim MacArthur <jim macarthur codethink co uk>
|
| 20 | 20 |
|
| 21 | 21 |
import os
|
| 22 |
+import shlex
|
|
| 22 | 23 |
from urllib.parse import urlparse
|
| 23 | 24 |
|
| 24 | 25 |
import grpc
|
| 25 | 26 |
|
| 26 | 27 |
from . import Sandbox
|
| 28 |
+from .sandbox import _SandboxBatch
|
|
| 27 | 29 |
from ..storage._filebaseddirectory import FileBasedDirectory
|
| 28 | 30 |
from ..storage._casbaseddirectory import CasBasedDirectory
|
| 29 | 31 |
from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2, remote_execution_pb2_grpc
|
| ... | ... | @@ -238,3 +240,69 @@ class SandboxRemote(Sandbox): |
| 238 | 240 |
self.process_job_output(action_result.output_directories, action_result.output_files)
|
| 239 | 241 |
|
| 240 | 242 |
return 0
|
| 243 |
+ |
|
| 244 |
+ def _create_batch(self, main_group, flags, *, collect=None):
|
|
| 245 |
+ return _SandboxRemoteBatch(self, main_group, flags, collect=collect)
|
|
| 246 |
+ |
|
| 247 |
+ |
|
| 248 |
+# _SandboxRemoteBatch()
|
|
| 249 |
+#
|
|
| 250 |
+# Command batching by shell script generation.
|
|
| 251 |
+#
|
|
| 252 |
+class _SandboxRemoteBatch(_SandboxBatch):
|
|
| 253 |
+ |
|
| 254 |
+ def __init__(self, sandbox, main_group, flags, *, collect=None):
|
|
| 255 |
+ super().__init__(sandbox, main_group, flags, collect=collect)
|
|
| 256 |
+ |
|
| 257 |
+ self.script = None
|
|
| 258 |
+ self.first_command = None
|
|
| 259 |
+ self.cwd = None
|
|
| 260 |
+ self.env = None
|
|
| 261 |
+ |
|
| 262 |
+ def execute(self):
|
|
| 263 |
+ self.script = ""
|
|
| 264 |
+ |
|
| 265 |
+ self.main_group.execute(self)
|
|
| 266 |
+ |
|
| 267 |
+ first = self.first_command
|
|
| 268 |
+ if first and self.sandbox.run(['sh', '-c', '-e', self.script], self.flags, cwd=first.cwd, env=first.env) != 0:
|
|
| 269 |
+ raise SandboxError("Command execution failed", reason="command-failed", collect=self.collect)
|
|
| 270 |
+ |
|
| 271 |
+ def execute_group(self, group):
|
|
| 272 |
+ group.execute_children(self)
|
|
| 273 |
+ |
|
| 274 |
+ def execute_command(self, command):
|
|
| 275 |
+ if self.first_command is None:
|
|
| 276 |
+ # First command in batch
|
|
| 277 |
+ # Initial working directory and environment of script already matches
|
|
| 278 |
+ # the command configuration.
|
|
| 279 |
+ self.first_command = command
|
|
| 280 |
+ else:
|
|
| 281 |
+ # Change working directory for this command
|
|
| 282 |
+ if command.cwd != self.cwd:
|
|
| 283 |
+ self.script += "mkdir -p {}\n".format(command.cwd)
|
|
| 284 |
+ self.script += "cd {}\n".format(command.cwd)
|
|
| 285 |
+ |
|
| 286 |
+ # Update environment for this command
|
|
| 287 |
+ for key in self.env.keys():
|
|
| 288 |
+ if key not in command.env:
|
|
| 289 |
+ self.script += "unset {}\n".format(key)
|
|
| 290 |
+ for key, value in command.env.items():
|
|
| 291 |
+ if key not in self.env or self.env[key] != value:
|
|
| 292 |
+ self.script += "export {}={}\n".format(key, shlex.quote(value))
|
|
| 293 |
+ |
|
| 294 |
+ # Keep track of current working directory and environment
|
|
| 295 |
+ self.cwd = command.cwd
|
|
| 296 |
+ self.env = command.env
|
|
| 297 |
+ |
|
| 298 |
+ # Actual command execution
|
|
| 299 |
+ cmdline = ' '.join(shlex.quote(cmd) for cmd in command.command)
|
|
| 300 |
+ self.script += "(set -ex; {})".format(cmdline)
|
|
| 301 |
+ |
|
| 302 |
+ # Error handling
|
|
| 303 |
+ label = command.label or cmdline
|
|
| 304 |
+ quoted_label = shlex.quote("'{}'".format(label))
|
|
| 305 |
+ self.script += " || (echo Command {} failed with exitcode $? >&2 ; exit 1)\n".format(quoted_label)
|
|
| 306 |
+ |
|
| 307 |
+ def execute_call(self, call):
|
|
| 308 |
+ raise SandboxError("SandboxRemote does not support callbacks in command batches")
|
| 1 | 1 |
#
|
| 2 | 2 |
# Copyright (C) 2017 Codethink Limited
|
| 3 |
+# Copyright (C) 2018 Bloomberg Finance LP
|
|
| 3 | 4 |
#
|
| 4 | 5 |
# This program is free software; you can redistribute it and/or
|
| 5 | 6 |
# modify it under the terms of the GNU Lesser General Public
|
| ... | ... | @@ -29,7 +30,12 @@ See also: :ref:`sandboxing`. |
| 29 | 30 |
"""
|
| 30 | 31 |
|
| 31 | 32 |
import os
|
| 32 |
-from .._exceptions import ImplError, BstError
|
|
| 33 |
+import shlex
|
|
| 34 |
+import contextlib
|
|
| 35 |
+from contextlib import contextmanager
|
|
| 36 |
+ |
|
| 37 |
+from .._exceptions import ImplError, BstError, SandboxError
|
|
| 38 |
+from .._message import Message, MessageType
|
|
| 33 | 39 |
from ..storage._filebaseddirectory import FileBasedDirectory
|
| 34 | 40 |
from ..storage._casbaseddirectory import CasBasedDirectory
|
| 35 | 41 |
|
| ... | ... | @@ -94,6 +100,13 @@ class Sandbox(): |
| 94 | 100 |
self.__mount_sources = {}
|
| 95 | 101 |
self.__allow_real_directory = kwargs['allow_real_directory']
|
| 96 | 102 |
|
| 103 |
+ # Plugin ID for logging
|
|
| 104 |
+ plugin = kwargs.get('plugin', None)
|
|
| 105 |
+ if plugin:
|
|
| 106 |
+ self.__plugin_id = plugin._get_unique_id()
|
|
| 107 |
+ else:
|
|
| 108 |
+ self.__plugin_id = None
|
|
| 109 |
+ |
|
| 97 | 110 |
# Configuration from kwargs common to all subclasses
|
| 98 | 111 |
self.__config = kwargs['config']
|
| 99 | 112 |
self.__stdout = kwargs['stdout']
|
| ... | ... | @@ -121,6 +134,9 @@ class Sandbox(): |
| 121 | 134 |
# directory via get_directory.
|
| 122 | 135 |
self._never_cache_vdirs = False
|
| 123 | 136 |
|
| 137 |
+ # Pending command batch
|
|
| 138 |
+ self.__batch = None
|
|
| 139 |
+ |
|
| 124 | 140 |
def get_directory(self):
|
| 125 | 141 |
"""Fetches the sandbox root directory
|
| 126 | 142 |
|
| ... | ... | @@ -209,9 +225,16 @@ class Sandbox(): |
| 209 | 225 |
'artifact': artifact
|
| 210 | 226 |
})
|
| 211 | 227 |
|
| 212 |
- def run(self, command, flags, *, cwd=None, env=None):
|
|
| 228 |
+ def run(self, command, flags, *, cwd=None, env=None, label=None):
|
|
| 213 | 229 |
"""Run a command in the sandbox.
|
| 214 | 230 |
|
| 231 |
+ If this is called outside a batch context, the command is immediately
|
|
| 232 |
+ executed.
|
|
| 233 |
+ |
|
| 234 |
+ If this is called in a batch context, the command is added to the batch
|
|
| 235 |
+ for later execution. If the command fails, later commands will not be
|
|
| 236 |
+ executed. Command flags must match batch flags.
|
|
| 237 |
+ |
|
| 215 | 238 |
Args:
|
| 216 | 239 |
command (list): The command to run in the sandboxed environment, as a list
|
| 217 | 240 |
of strings starting with the binary to run.
|
| ... | ... | @@ -219,9 +242,10 @@ class Sandbox(): |
| 219 | 242 |
cwd (str): The sandbox relative working directory in which to run the command.
|
| 220 | 243 |
env (dict): A dictionary of string key, value pairs to set as environment
|
| 221 | 244 |
variables inside the sandbox environment.
|
| 245 |
+ label (str): An optional label for the command, used for logging. (*Since: 1.4*)
|
|
| 222 | 246 |
|
| 223 | 247 |
Returns:
|
| 224 |
- (int): The program exit code.
|
|
| 248 |
+ (int|None): The program exit code, or None if running in batch context.
|
|
| 225 | 249 |
|
| 226 | 250 |
Raises:
|
| 227 | 251 |
(:class:`.ProgramNotFoundError`): If a host tool which the given sandbox
|
| ... | ... | @@ -245,7 +269,66 @@ class Sandbox(): |
| 245 | 269 |
if isinstance(command, str):
|
| 246 | 270 |
command = [command]
|
| 247 | 271 |
|
| 248 |
- return self._run(command, flags, cwd=cwd, env=env)
|
|
| 272 |
+ if self.__batch:
|
|
| 273 |
+ if flags != self.__batch.flags:
|
|
| 274 |
+ raise SandboxError("Inconsistent sandbox flags in single command batch")
|
|
| 275 |
+ |
|
| 276 |
+ batch_command = _SandboxBatchCommand(command, cwd=cwd, env=env, label=label)
|
|
| 277 |
+ |
|
| 278 |
+ current_group = self.__batch.current_group
|
|
| 279 |
+ current_group.append(batch_command)
|
|
| 280 |
+ return None
|
|
| 281 |
+ else:
|
|
| 282 |
+ return self._run(command, flags, cwd=cwd, env=env)
|
|
| 283 |
+ |
|
| 284 |
+ @contextmanager
|
|
| 285 |
+ def batch(self, flags, *, label=None, collect=None):
|
|
| 286 |
+ """Context manager for command batching
|
|
| 287 |
+ |
|
| 288 |
+ This provides a batch context that defers execution of commands until
|
|
| 289 |
+ the end of the context. If a command fails, the batch will be aborted
|
|
| 290 |
+ and subsequent commands will not be executed.
|
|
| 291 |
+ |
|
| 292 |
+ Command batches may be nested. Execution will start only when the top
|
|
| 293 |
+ level batch context ends.
|
|
| 294 |
+ |
|
| 295 |
+ Args:
|
|
| 296 |
+ flags (:class:`.SandboxFlags`): The flags for this command batch.
|
|
| 297 |
+ label (str): An optional label for the batch group, used for logging.
|
|
| 298 |
+ collect (str): An optional directory containing partial install contents
|
|
| 299 |
+ on command failure.
|
|
| 300 |
+ |
|
| 301 |
+ Raises:
|
|
| 302 |
+ (:class:`.SandboxError`): If a command fails.
|
|
| 303 |
+ |
|
| 304 |
+ *Since: 1.4*
|
|
| 305 |
+ """
|
|
| 306 |
+ |
|
| 307 |
+ group = _SandboxBatchGroup(label=label)
|
|
| 308 |
+ |
|
| 309 |
+ if self.__batch:
|
|
| 310 |
+ # Nested batch
|
|
| 311 |
+ if flags != self.__batch.flags:
|
|
| 312 |
+ raise SandboxError("Inconsistent sandbox flags in single command batch")
|
|
| 313 |
+ |
|
| 314 |
+ parent_group = self.__batch.current_group
|
|
| 315 |
+ parent_group.append(group)
|
|
| 316 |
+ self.__batch.current_group = group
|
|
| 317 |
+ try:
|
|
| 318 |
+ yield
|
|
| 319 |
+ finally:
|
|
| 320 |
+ self.__batch.current_group = parent_group
|
|
| 321 |
+ else:
|
|
| 322 |
+ # Top-level batch
|
|
| 323 |
+ batch = self._create_batch(group, flags, collect=collect)
|
|
| 324 |
+ |
|
| 325 |
+ self.__batch = batch
|
|
| 326 |
+ try:
|
|
| 327 |
+ yield
|
|
| 328 |
+ finally:
|
|
| 329 |
+ self.__batch = None
|
|
| 330 |
+ |
|
| 331 |
+ batch.execute()
|
|
| 249 | 332 |
|
| 250 | 333 |
#####################################################
|
| 251 | 334 |
# Abstract Methods for Sandbox implementations #
|
| ... | ... | @@ -270,6 +353,20 @@ class Sandbox(): |
| 270 | 353 |
raise ImplError("Sandbox of type '{}' does not implement _run()"
|
| 271 | 354 |
.format(type(self).__name__))
|
| 272 | 355 |
|
| 356 |
+ # _create_batch()
|
|
| 357 |
+ #
|
|
| 358 |
+ # Abstract method for creating a batch object. Subclasses can override
|
|
| 359 |
+ # this method to instantiate a subclass of _SandboxBatch.
|
|
| 360 |
+ #
|
|
| 361 |
+ # Args:
|
|
| 362 |
+ # main_group (:class:`_SandboxBatchGroup`): The top level batch group.
|
|
| 363 |
+ # flags (:class:`.SandboxFlags`): The flags for commands in this batch.
|
|
| 364 |
+ # collect (str): An optional directory containing partial install contents
|
|
| 365 |
+ # on command failure.
|
|
| 366 |
+ #
|
|
| 367 |
+ def _create_batch(self, main_group, flags, *, collect=None):
|
|
| 368 |
+ return _SandboxBatch(self, main_group, flags, collect=collect)
|
|
| 369 |
+ |
|
| 273 | 370 |
################################################
|
| 274 | 371 |
# Private methods #
|
| 275 | 372 |
################################################
|
| ... | ... | @@ -418,3 +515,138 @@ class Sandbox(): |
| 418 | 515 |
return True
|
| 419 | 516 |
|
| 420 | 517 |
return False
|
| 518 |
+ |
|
| 519 |
+ # _get_plugin_id()
|
|
| 520 |
+ #
|
|
| 521 |
+ # Get the plugin's unique identifier
|
|
| 522 |
+ #
|
|
| 523 |
+ def _get_plugin_id(self):
|
|
| 524 |
+ return self.__plugin_id
|
|
| 525 |
+ |
|
| 526 |
+ # _callback()
|
|
| 527 |
+ #
|
|
| 528 |
+ # If this is called outside a batch context, the specified function is
|
|
| 529 |
+ # invoked immediately.
|
|
| 530 |
+ #
|
|
| 531 |
+ # If this is called in a batch context, the function is added to the batch
|
|
| 532 |
+ # for later invocation.
|
|
| 533 |
+ #
|
|
| 534 |
+ # Args:
|
|
| 535 |
+ # callback (callable): The function to invoke
|
|
| 536 |
+ #
|
|
| 537 |
+ def _callback(self, callback):
|
|
| 538 |
+ if self.__batch:
|
|
| 539 |
+ batch_call = _SandboxBatchCall(callback)
|
|
| 540 |
+ |
|
| 541 |
+ current_group = self.__batch.current_group
|
|
| 542 |
+ current_group.append(batch_call)
|
|
| 543 |
+ else:
|
|
| 544 |
+ callback()
|
|
| 545 |
+ |
|
| 546 |
+ |
|
| 547 |
+# _SandboxBatch()
|
|
| 548 |
+#
|
|
| 549 |
+# A batch of sandbox commands.
|
|
| 550 |
+#
|
|
| 551 |
+class _SandboxBatch():
|
|
| 552 |
+ |
|
| 553 |
+ def __init__(self, sandbox, main_group, flags, *, collect=None):
|
|
| 554 |
+ self.sandbox = sandbox
|
|
| 555 |
+ self.main_group = main_group
|
|
| 556 |
+ self.current_group = main_group
|
|
| 557 |
+ self.flags = flags
|
|
| 558 |
+ self.collect = collect
|
|
| 559 |
+ |
|
| 560 |
+ def execute(self):
|
|
| 561 |
+ self.main_group.execute(self)
|
|
| 562 |
+ |
|
| 563 |
+ def execute_group(self, group):
|
|
| 564 |
+ if group.label:
|
|
| 565 |
+ context = self.sandbox._get_context()
|
|
| 566 |
+ cm = context.timed_activity(group.label, unique_id=self.sandbox._get_plugin_id())
|
|
| 567 |
+ else:
|
|
| 568 |
+ cm = contextlib.suppress()
|
|
| 569 |
+ |
|
| 570 |
+ with cm:
|
|
| 571 |
+ group.execute_children(self)
|
|
| 572 |
+ |
|
| 573 |
+ def execute_command(self, command):
|
|
| 574 |
+ if command.label:
|
|
| 575 |
+ context = self.sandbox._get_context()
|
|
| 576 |
+ message = Message(self.sandbox._get_plugin_id(), MessageType.STATUS,
|
|
| 577 |
+ 'Running {}'.format(command.label))
|
|
| 578 |
+ context.message(message)
|
|
| 579 |
+ |
|
| 580 |
+ exitcode = self.sandbox._run(command.command, self.flags, cwd=command.cwd, env=command.env)
|
|
| 581 |
+ if exitcode != 0:
|
|
| 582 |
+ cmdline = ' '.join(shlex.quote(cmd) for cmd in command.command)
|
|
| 583 |
+ label = command.label or cmdline
|
|
| 584 |
+ raise SandboxError("Command '{}' failed with exitcode {}".format(label, exitcode),
|
|
| 585 |
+ reason="command-failed", collect=self.collect)
|
|
| 586 |
+ |
|
| 587 |
+ def execute_call(self, call):
|
|
| 588 |
+ call.callback()
|
|
| 589 |
+ |
|
| 590 |
+ |
|
| 591 |
+# _SandboxBatchItem()
|
|
| 592 |
+#
|
|
| 593 |
+# An item in a command batch.
|
|
| 594 |
+#
|
|
| 595 |
+class _SandboxBatchItem():
|
|
| 596 |
+ |
|
| 597 |
+ def __init__(self, *, label=None):
|
|
| 598 |
+ self.label = label
|
|
| 599 |
+ |
|
| 600 |
+ |
|
| 601 |
+# _SandboxBatchCommand()
|
|
| 602 |
+#
|
|
| 603 |
+# A command item in a command batch.
|
|
| 604 |
+#
|
|
| 605 |
+class _SandboxBatchCommand(_SandboxBatchItem):
|
|
| 606 |
+ |
|
| 607 |
+ def __init__(self, command, *, cwd, env, label=None):
|
|
| 608 |
+ super().__init__(label=label)
|
|
| 609 |
+ |
|
| 610 |
+ self.command = command
|
|
| 611 |
+ self.cwd = cwd
|
|
| 612 |
+ self.env = env
|
|
| 613 |
+ |
|
| 614 |
+ def execute(self, batch):
|
|
| 615 |
+ batch.execute_command(self)
|
|
| 616 |
+ |
|
| 617 |
+ |
|
| 618 |
+# _SandboxBatchGroup()
|
|
| 619 |
+#
|
|
| 620 |
+# A group in a command batch.
|
|
| 621 |
+#
|
|
| 622 |
+class _SandboxBatchGroup(_SandboxBatchItem):
|
|
| 623 |
+ |
|
| 624 |
+ def __init__(self, *, label=None):
|
|
| 625 |
+ super().__init__(label=label)
|
|
| 626 |
+ |
|
| 627 |
+ self.children = []
|
|
| 628 |
+ |
|
| 629 |
+ def append(self, item):
|
|
| 630 |
+ self.children.append(item)
|
|
| 631 |
+ |
|
| 632 |
+ def execute(self, batch):
|
|
| 633 |
+ batch.execute_group(self)
|
|
| 634 |
+ |
|
| 635 |
+ def execute_children(self, batch):
|
|
| 636 |
+ for item in self.children:
|
|
| 637 |
+ item.execute(batch)
|
|
| 638 |
+ |
|
| 639 |
+ |
|
| 640 |
+# _SandboxBatchCall()
|
|
| 641 |
+#
|
|
| 642 |
+# A call item in a command batch.
|
|
| 643 |
+#
|
|
| 644 |
+class _SandboxBatchCall(_SandboxBatchItem):
|
|
| 645 |
+ |
|
| 646 |
+ def __init__(self, callback):
|
|
| 647 |
+ super().__init__()
|
|
| 648 |
+ |
|
| 649 |
+ self.callback = callback
|
|
| 650 |
+ |
|
| 651 |
+ def execute(self, batch):
|
|
| 652 |
+ batch.execute_call(self)
|
| ... | ... | @@ -226,10 +226,11 @@ class ScriptElement(Element): |
| 226 | 226 |
.format(build_dep.name), silent_nested=True):
|
| 227 | 227 |
build_dep.stage_dependency_artifacts(sandbox, Scope.RUN, path="/")
|
| 228 | 228 |
|
| 229 |
- for build_dep in self.dependencies(Scope.BUILD, recurse=False):
|
|
| 230 |
- with self.timed_activity("Integrating {}".format(build_dep.name), silent_nested=True):
|
|
| 231 |
- for dep in build_dep.dependencies(Scope.RUN):
|
|
| 232 |
- dep.integrate(sandbox)
|
|
| 229 |
+ with sandbox.batch(0):
|
|
| 230 |
+ for build_dep in self.dependencies(Scope.BUILD, recurse=False):
|
|
| 231 |
+ with self.timed_activity("Integrating {}".format(build_dep.name), silent_nested=True):
|
|
| 232 |
+ for dep in build_dep.dependencies(Scope.RUN):
|
|
| 233 |
+ dep.integrate(sandbox)
|
|
| 233 | 234 |
else:
|
| 234 | 235 |
# If layout, follow its rules.
|
| 235 | 236 |
for item in self.__layout:
|
| ... | ... | @@ -251,37 +252,38 @@ class ScriptElement(Element): |
| 251 | 252 |
virtual_dstdir.descend(item['destination'].lstrip(os.sep).split(os.sep), create=True)
|
| 252 | 253 |
element.stage_dependency_artifacts(sandbox, Scope.RUN, path=item['destination'])
|
| 253 | 254 |
|
| 254 |
- for item in self.__layout:
|
|
| 255 |
+ with sandbox.batch(0):
|
|
| 256 |
+ for item in self.__layout:
|
|
| 255 | 257 |
|
| 256 |
- # Skip layout members which dont stage an element
|
|
| 257 |
- if not item['element']:
|
|
| 258 |
- continue
|
|
| 258 |
+ # Skip layout members which dont stage an element
|
|
| 259 |
+ if not item['element']:
|
|
| 260 |
+ continue
|
|
| 259 | 261 |
|
| 260 |
- element = self.search(Scope.BUILD, item['element'])
|
|
| 262 |
+ element = self.search(Scope.BUILD, item['element'])
|
|
| 261 | 263 |
|
| 262 |
- # Integration commands can only be run for elements staged to /
|
|
| 263 |
- if item['destination'] == '/':
|
|
| 264 |
- with self.timed_activity("Integrating {}".format(element.name),
|
|
| 265 |
- silent_nested=True):
|
|
| 266 |
- for dep in element.dependencies(Scope.RUN):
|
|
| 267 |
- dep.integrate(sandbox)
|
|
| 264 |
+ # Integration commands can only be run for elements staged to /
|
|
| 265 |
+ if item['destination'] == '/':
|
|
| 266 |
+ with self.timed_activity("Integrating {}".format(element.name),
|
|
| 267 |
+ silent_nested=True):
|
|
| 268 |
+ for dep in element.dependencies(Scope.RUN):
|
|
| 269 |
+ dep.integrate(sandbox)
|
|
| 268 | 270 |
|
| 269 | 271 |
install_root_path_components = self.__install_root.lstrip(os.sep).split(os.sep)
|
| 270 | 272 |
sandbox.get_virtual_directory().descend(install_root_path_components, create=True)
|
| 271 | 273 |
|
| 272 | 274 |
def assemble(self, sandbox):
|
| 273 | 275 |
|
| 274 |
- for groupname, commands in self.__commands.items():
|
|
| 275 |
- with self.timed_activity("Running '{}'".format(groupname)):
|
|
| 276 |
- for cmd in commands:
|
|
| 277 |
- self.status("Running command", detail=cmd)
|
|
| 278 |
- # Note the -e switch to 'sh' means to exit with an error
|
|
| 279 |
- # if any untested command fails.
|
|
| 280 |
- exitcode = sandbox.run(['sh', '-c', '-e', cmd + '\n'],
|
|
| 281 |
- SandboxFlags.ROOT_READ_ONLY if self.__root_read_only else 0)
|
|
| 282 |
- if exitcode != 0:
|
|
| 283 |
- raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode),
|
|
| 284 |
- collect=self.__install_root)
|
|
| 276 |
+ sandbox_flags = SandboxFlags.ROOT_READ_ONLY if self.__root_read_only else 0
|
|
| 277 |
+ |
|
| 278 |
+ with sandbox.batch(sandbox_flags, collect=self.__install_root):
|
|
| 279 |
+ for groupname, commands in self.__commands.items():
|
|
| 280 |
+ with sandbox.batch(sandbox_flags, label="Running '{}'".format(groupname)):
|
|
| 281 |
+ for cmd in commands:
|
|
| 282 |
+ # Note the -e switch to 'sh' means to exit with an error
|
|
| 283 |
+ # if any untested command fails.
|
|
| 284 |
+ sandbox.run(['sh', '-c', '-e', cmd + '\n'],
|
|
| 285 |
+ sandbox_flags,
|
|
| 286 |
+ label=cmd)
|
|
| 285 | 287 |
|
| 286 | 288 |
# Return where the result can be collected from
|
| 287 | 289 |
return self.__install_root
|
| ... | ... | @@ -1199,3 +1199,27 @@ def _deduplicate(iterable, key=None): |
| 1199 | 1199 |
def _get_link_mtime(path):
|
| 1200 | 1200 |
path_stat = os.lstat(path)
|
| 1201 | 1201 |
return path_stat.st_mtime
|
| 1202 |
+ |
|
| 1203 |
+ |
|
| 1204 |
+# _SplitContextManager():
|
|
| 1205 |
+#
|
|
| 1206 |
+# A context manager wrapper that allows using a context manager across two
|
|
| 1207 |
+# `with` statements.
|
|
| 1208 |
+#
|
|
| 1209 |
+class _SplitContextManager():
|
|
| 1210 |
+ def __init__(self, cm):
|
|
| 1211 |
+ self._cm = cm
|
|
| 1212 |
+ self._stage1_complete = False
|
|
| 1213 |
+ |
|
| 1214 |
+ def __enter__(self):
|
|
| 1215 |
+ if not self._stage1_complete:
|
|
| 1216 |
+ return self._cm.__enter__()
|
|
| 1217 |
+ else:
|
|
| 1218 |
+ return None
|
|
| 1219 |
+ |
|
| 1220 |
+ def __exit__(self, exception_type, exception_value, traceback):
|
|
| 1221 |
+ if not self._stage1_complete and exception_type is None:
|
|
| 1222 |
+ self._stage1_complete = True
|
|
| 1223 |
+ return False
|
|
| 1224 |
+ else:
|
|
| 1225 |
+ return self._cm.__exit__(exception_type, exception_value, traceback)
|
| ... | ... | @@ -58,5 +58,5 @@ def test_sandbox_bwrap_return_subprocess(cli, tmpdir, datafiles): |
| 58 | 58 |
})
|
| 59 | 59 |
|
| 60 | 60 |
result = cli.run(project=project, args=['build', element_name])
|
| 61 |
- result.assert_task_error(error_domain=ErrorDomain.ELEMENT, error_reason=None)
|
|
| 61 |
+ result.assert_task_error(error_domain=ErrorDomain.SANDBOX, error_reason="command-failed")
|
|
| 62 | 62 |
assert "sandbox-bwrap/command-exit-42.bst|Command 'exit 42' failed with exitcode 42" in result.stderr
|
