Jürg Billeter pushed to branch juerg/command-batching at BuildStream / buildstream
Commits:
-
770f061f
by Jürg Billeter at 2018-11-19T07:08:58Z
-
226434fe
by Jürg Billeter at 2018-11-19T07:08:58Z
-
5d572d28
by Jürg Billeter at 2018-11-19T07:08:58Z
-
ddc76a38
by Jürg Billeter at 2018-11-19T07:08:58Z
-
9ed7dfc2
by Jürg Billeter at 2018-11-19T07:08:58Z
-
165986ba
by Jürg Billeter at 2018-11-19T07:08:58Z
-
6b21aae9
by Jürg Billeter at 2018-11-19T07:08:58Z
-
03b191d8
by Jürg Billeter at 2018-11-19T07:08:58Z
-
f94bc8a3
by Jürg Billeter at 2018-11-19T07:11:36Z
9 changed files:
- NEWS
- 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
|
... | ... | @@ -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,23 @@ 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 |
+ |
|
237 |
+ with batch_cm:
|
|
238 |
+ # Run commands
|
|
239 |
+ for command_name in _command_steps:
|
|
240 |
+ commands = self.__commands[command_name]
|
|
241 |
+ if not commands or command_name == 'configure-commands':
|
|
242 |
+ continue
|
|
232 | 243 |
|
233 |
- with self.timed_activity("Running {}".format(command_name)):
|
|
234 |
- for cmd in commands:
|
|
235 |
- self.__run_command(sandbox, cmd, command_name)
|
|
244 |
+ with sandbox.batch(SandboxFlags.ROOT_READ_ONLY, label="Running {}".format(command_name)):
|
|
245 |
+ for cmd in commands:
|
|
246 |
+ self.__run_command(sandbox, cmd, command_name)
|
|
236 | 247 |
|
237 | 248 |
# %{install-root}/%{build-root} should normally not be written
|
238 | 249 |
# to - if an element later attempts to stage to a location
|
... | ... | @@ -252,11 +263,17 @@ class BuildElement(Element): |
252 | 263 |
return self.get_variable('install-root')
|
253 | 264 |
|
254 | 265 |
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')
|
|
266 |
+ # Allow use of single batch context manager across prepare and assemble
|
|
267 |
+ batch_cm = utils._SplitContextManager(sandbox.batch(SandboxFlags.ROOT_READ_ONLY))
|
|
268 |
+ |
|
269 |
+ with batch_cm:
|
|
270 |
+ commands = self.__commands['configure-commands']
|
|
271 |
+ if commands:
|
|
272 |
+ with sandbox.batch(SandboxFlags.ROOT_READ_ONLY, label="Running configure-commands"):
|
|
273 |
+ for cmd in commands:
|
|
274 |
+ self.__run_command(sandbox, cmd, 'configure-commands')
|
|
275 |
+ |
|
276 |
+ self.__sandbox_batch_cm = batch_cm # pylint: disable=attribute-defined-outside-init
|
|
260 | 277 |
|
261 | 278 |
def generate_script(self):
|
262 | 279 |
script = ""
|
... | ... | @@ -282,13 +299,9 @@ class BuildElement(Element): |
282 | 299 |
return commands
|
283 | 300 |
|
284 | 301 |
def __run_command(self, sandbox, cmd, cmd_name):
|
285 |
- self.status("Running {}".format(cmd_name), detail=cmd)
|
|
286 |
- |
|
287 | 302 |
# Note the -e switch to 'sh' means to exit with an error
|
288 | 303 |
# if any untested command fails.
|
289 | 304 |
#
|
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'))
|
|
305 |
+ sandbox.run(['sh', '-c', '-e', cmd + '\n'],
|
|
306 |
+ SandboxFlags.ROOT_READ_ONLY,
|
|
307 |
+ 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
|
... | ... | @@ -2073,7 +2073,12 @@ class Element(Plugin): |
2073 | 2073 |
self.prepare(sandbox)
|
2074 | 2074 |
|
2075 | 2075 |
if workspace:
|
2076 |
- workspace.prepared = True
|
|
2076 |
+ def mark_workspace_prepared():
|
|
2077 |
+ workspace.prepared = True
|
|
2078 |
+ |
|
2079 |
+ # Defer workspace.prepared setting until pending batch commands
|
|
2080 |
+ # have been executed.
|
|
2081 |
+ sandbox._callback(mark_workspace_prepared)
|
|
2077 | 2082 |
|
2078 | 2083 |
def __is_cached(self, keystrength):
|
2079 | 2084 |
if keystrength is None:
|
... | ... | @@ -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):
|
|
245 |
+ return _SandboxRemoteBatch(self, main_group, flags)
|
|
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):
|
|
255 |
+ super().__init__(sandbox, main_group, flags)
|
|
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")
|
|
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.
|
|
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,62 @@ 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):
|
|
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 |
+ |
|
299 |
+ Raises:
|
|
300 |
+ (:class:`.SandboxError`): If a command fails.
|
|
301 |
+ """
|
|
302 |
+ |
|
303 |
+ group = _SandboxBatchGroup(label=label)
|
|
304 |
+ |
|
305 |
+ if self.__batch:
|
|
306 |
+ # Nested batch
|
|
307 |
+ if flags != self.__batch.flags:
|
|
308 |
+ raise SandboxError("Inconsistent sandbox flags in single command batch")
|
|
309 |
+ |
|
310 |
+ parent_group = self.__batch.current_group
|
|
311 |
+ parent_group.append(group)
|
|
312 |
+ self.__batch.current_group = group
|
|
313 |
+ try:
|
|
314 |
+ yield
|
|
315 |
+ finally:
|
|
316 |
+ self.__batch.current_group = parent_group
|
|
317 |
+ else:
|
|
318 |
+ # Top-level batch
|
|
319 |
+ batch = self._create_batch(group, flags)
|
|
320 |
+ |
|
321 |
+ self.__batch = batch
|
|
322 |
+ try:
|
|
323 |
+ yield
|
|
324 |
+ finally:
|
|
325 |
+ self.__batch = None
|
|
326 |
+ |
|
327 |
+ batch.execute()
|
|
249 | 328 |
|
250 | 329 |
#####################################################
|
251 | 330 |
# Abstract Methods for Sandbox implementations #
|
... | ... | @@ -270,6 +349,18 @@ class Sandbox(): |
270 | 349 |
raise ImplError("Sandbox of type '{}' does not implement _run()"
|
271 | 350 |
.format(type(self).__name__))
|
272 | 351 |
|
352 |
+ # _create_batch()
|
|
353 |
+ #
|
|
354 |
+ # Abstract method for creating a batch object. Subclasses can override
|
|
355 |
+ # this method to instantiate a subclass of _SandboxBatch.
|
|
356 |
+ #
|
|
357 |
+ # Args:
|
|
358 |
+ # main_group (:class:`_SandboxBatchGroup`): The top level batch group.
|
|
359 |
+ # flags (:class:`.SandboxFlags`): The flags for commands in this batch.
|
|
360 |
+ #
|
|
361 |
+ def _create_batch(self, main_group, flags):
|
|
362 |
+ return _SandboxBatch(self, main_group, flags)
|
|
363 |
+ |
|
273 | 364 |
################################################
|
274 | 365 |
# Private methods #
|
275 | 366 |
################################################
|
... | ... | @@ -418,3 +509,137 @@ class Sandbox(): |
418 | 509 |
return True
|
419 | 510 |
|
420 | 511 |
return False
|
512 |
+ |
|
513 |
+ # _get_plugin_id()
|
|
514 |
+ #
|
|
515 |
+ # Get the plugin's unique identifier
|
|
516 |
+ #
|
|
517 |
+ def _get_plugin_id(self):
|
|
518 |
+ return self.__plugin_id
|
|
519 |
+ |
|
520 |
+ # _callback()
|
|
521 |
+ #
|
|
522 |
+ # If this is called outside a batch context, the specified function is
|
|
523 |
+ # invoked immediately.
|
|
524 |
+ #
|
|
525 |
+ # If this is called in a batch context, the function is added to the batch
|
|
526 |
+ # for later invocation.
|
|
527 |
+ #
|
|
528 |
+ # Args:
|
|
529 |
+ # callback (callable): The function to invoke
|
|
530 |
+ #
|
|
531 |
+ def _callback(self, callback):
|
|
532 |
+ if self.__batch:
|
|
533 |
+ batch_call = _SandboxBatchCall(callback)
|
|
534 |
+ |
|
535 |
+ current_group = self.__batch.current_group
|
|
536 |
+ current_group.append(batch_call)
|
|
537 |
+ else:
|
|
538 |
+ callback()
|
|
539 |
+ |
|
540 |
+ |
|
541 |
+# _SandboxBatch()
|
|
542 |
+#
|
|
543 |
+# A batch of sandbox commands.
|
|
544 |
+#
|
|
545 |
+class _SandboxBatch():
|
|
546 |
+ |
|
547 |
+ def __init__(self, sandbox, main_group, flags):
|
|
548 |
+ self.sandbox = sandbox
|
|
549 |
+ self.main_group = main_group
|
|
550 |
+ self.current_group = main_group
|
|
551 |
+ self.flags = flags
|
|
552 |
+ |
|
553 |
+ def execute(self):
|
|
554 |
+ self.main_group.execute(self)
|
|
555 |
+ |
|
556 |
+ def execute_group(self, group):
|
|
557 |
+ if group.label:
|
|
558 |
+ context = self.sandbox._get_context()
|
|
559 |
+ cm = context.timed_activity(group.label, unique_id=self.sandbox._get_plugin_id())
|
|
560 |
+ else:
|
|
561 |
+ cm = contextlib.suppress()
|
|
562 |
+ |
|
563 |
+ with cm:
|
|
564 |
+ group.execute_children(self)
|
|
565 |
+ |
|
566 |
+ def execute_command(self, command):
|
|
567 |
+ if command.label:
|
|
568 |
+ context = self.sandbox._get_context()
|
|
569 |
+ message = Message(self.sandbox._get_plugin_id(), MessageType.STATUS,
|
|
570 |
+ 'Running {}'.format(command.label))
|
|
571 |
+ context.message(message)
|
|
572 |
+ |
|
573 |
+ exitcode = self.sandbox._run(command.command, self.flags, cwd=command.cwd, env=command.env)
|
|
574 |
+ if exitcode != 0:
|
|
575 |
+ cmdline = ' '.join(shlex.quote(cmd) for cmd in command.command)
|
|
576 |
+ label = command.label or cmdline
|
|
577 |
+ raise SandboxError("Command '{}' failed with exitcode {}".format(label, exitcode),
|
|
578 |
+ reason="command-failed")
|
|
579 |
+ |
|
580 |
+ def execute_call(self, call):
|
|
581 |
+ call.callback()
|
|
582 |
+ |
|
583 |
+ |
|
584 |
+# _SandboxBatchItem()
|
|
585 |
+#
|
|
586 |
+# An item in a command batch.
|
|
587 |
+#
|
|
588 |
+class _SandboxBatchItem():
|
|
589 |
+ |
|
590 |
+ def __init__(self, *, label=None):
|
|
591 |
+ self.label = label
|
|
592 |
+ |
|
593 |
+ |
|
594 |
+# _SandboxBatchCommand()
|
|
595 |
+#
|
|
596 |
+# A command item in a command batch.
|
|
597 |
+#
|
|
598 |
+class _SandboxBatchCommand(_SandboxBatchItem):
|
|
599 |
+ |
|
600 |
+ def __init__(self, command, *, cwd, env, label=None):
|
|
601 |
+ super().__init__(label=label)
|
|
602 |
+ |
|
603 |
+ self.command = command
|
|
604 |
+ self.cwd = cwd
|
|
605 |
+ self.env = env
|
|
606 |
+ |
|
607 |
+ def execute(self, batch):
|
|
608 |
+ batch.execute_command(self)
|
|
609 |
+ |
|
610 |
+ |
|
611 |
+# _SandboxBatchGroup()
|
|
612 |
+#
|
|
613 |
+# A group in a command batch.
|
|
614 |
+#
|
|
615 |
+class _SandboxBatchGroup(_SandboxBatchItem):
|
|
616 |
+ |
|
617 |
+ def __init__(self, *, label=None):
|
|
618 |
+ super().__init__(label=label)
|
|
619 |
+ |
|
620 |
+ self.children = []
|
|
621 |
+ |
|
622 |
+ def append(self, item):
|
|
623 |
+ self.children.append(item)
|
|
624 |
+ |
|
625 |
+ def execute(self, batch):
|
|
626 |
+ batch.execute_group(self)
|
|
627 |
+ |
|
628 |
+ def execute_children(self, batch):
|
|
629 |
+ for item in self.children:
|
|
630 |
+ item.execute(batch)
|
|
631 |
+ |
|
632 |
+ |
|
633 |
+# _SandboxBatchCall()
|
|
634 |
+#
|
|
635 |
+# A call item in a command batch.
|
|
636 |
+#
|
|
637 |
+class _SandboxBatchCall(_SandboxBatchItem):
|
|
638 |
+ |
|
639 |
+ def __init__(self, callback):
|
|
640 |
+ super().__init__()
|
|
641 |
+ |
|
642 |
+ self.callback = callback
|
|
643 |
+ |
|
644 |
+ def execute(self, batch):
|
|
645 |
+ 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):
|
|
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
|