Jürg Billeter pushed to branch juerg/command-batching at BuildStream / buildstream
Commits:
-
516e990e
by ctolentino8 at 2018-10-31T11:36:46Z
-
b8a37a63
by Tristan Van Berkom at 2018-11-01T10:16:25Z
-
10d8d994
by Jürg Billeter at 2018-11-01T11:00:08Z
-
15d56e06
by Jürg Billeter at 2018-11-01T11:00:08Z
-
59b436df
by Jürg Billeter at 2018-11-01T11:00:08Z
-
ecd9d5e4
by Jürg Billeter at 2018-11-01T11:00:08Z
-
1f16d9fd
by Jürg Billeter at 2018-11-01T11:00:08Z
-
e1e4234d
by Jürg Billeter at 2018-11-01T11:00:08Z
-
ca6f10ce
by Jürg Billeter at 2018-11-01T11:00:08Z
-
2fbf0c6d
by Jürg Billeter at 2018-11-01T11:00:08Z
-
7e0a641c
by Jürg Billeter at 2018-11-01T11:00:08Z
-
9997c642
by Jürg Billeter at 2018-11-01T11:00:08Z
7 changed files:
- NEWS
- buildstream/_frontend/app.py
- buildstream/buildelement.py
- buildstream/element.py
- buildstream/sandbox/_sandboxremote.py
- buildstream/sandbox/sandbox.py
- tests/frontend/init.py
Changes:
| ... | ... | @@ -39,6 +39,8 @@ buildstream 1.3.1 |
| 39 | 39 |
instead of just a specially-formatted build-root with a `root` and `scratch`
|
| 40 | 40 |
subdirectory.
|
| 41 | 41 |
|
| 42 |
+ o Add sandbox API for command batching.
|
|
| 43 |
+ |
|
| 42 | 44 |
|
| 43 | 45 |
=================
|
| 44 | 46 |
buildstream 1.1.5
|
| ... | ... | @@ -305,7 +305,6 @@ class App(): |
| 305 | 305 |
directory = self._main_options['directory']
|
| 306 | 306 |
directory = os.path.abspath(directory)
|
| 307 | 307 |
project_path = os.path.join(directory, 'project.conf')
|
| 308 |
- elements_path = os.path.join(directory, element_path)
|
|
| 309 | 308 |
|
| 310 | 309 |
try:
|
| 311 | 310 |
# Abort if the project.conf already exists, unless `--force` was specified in `bst init`
|
| ... | ... | @@ -335,6 +334,7 @@ class App(): |
| 335 | 334 |
raise AppError("Error creating project directory {}: {}".format(directory, e)) from e
|
| 336 | 335 |
|
| 337 | 336 |
# Create the elements sub-directory if it doesnt exist
|
| 337 |
+ elements_path = os.path.join(directory, element_path)
|
|
| 338 | 338 |
try:
|
| 339 | 339 |
os.makedirs(elements_path, exist_ok=True)
|
| 340 | 340 |
except IOError as e:
|
| ... | ... | @@ -217,7 +217,8 @@ class BuildElement(Element): |
| 217 | 217 |
# once they are all staged and ready
|
| 218 | 218 |
with self.timed_activity("Integrating sandbox"):
|
| 219 | 219 |
for dep in self.dependencies(Scope.BUILD):
|
| 220 |
- dep.integrate(sandbox)
|
|
| 220 |
+ dep.integrate(sandbox, queue=True)
|
|
| 221 |
+ sandbox.run_queue(0)
|
|
| 221 | 222 |
|
| 222 | 223 |
# Stage sources in the build root
|
| 223 | 224 |
self.stage_sources(sandbox, self.get_variable('build-root'))
|
| ... | ... | @@ -230,9 +231,11 @@ class BuildElement(Element): |
| 230 | 231 |
if not commands or command_name == 'configure-commands':
|
| 231 | 232 |
continue
|
| 232 | 233 |
|
| 233 |
- with self.timed_activity("Running {}".format(command_name)):
|
|
| 234 |
+ with sandbox.queue_context_manager(self.timed_activity("Running {}".format(command_name))):
|
|
| 234 | 235 |
for cmd in commands:
|
| 235 |
- self.__run_command(sandbox, cmd, command_name)
|
|
| 236 |
+ self.__queue_command(sandbox, cmd, command_name)
|
|
| 237 |
+ |
|
| 238 |
+ sandbox.run_queue(SandboxFlags.ROOT_READ_ONLY)
|
|
| 236 | 239 |
|
| 237 | 240 |
# %{install-root}/%{build-root} should normally not be written
|
| 238 | 241 |
# to - if an element later attempts to stage to a location
|
| ... | ... | @@ -254,9 +257,9 @@ class BuildElement(Element): |
| 254 | 257 |
def prepare(self, sandbox):
|
| 255 | 258 |
commands = self.__commands['configure-commands']
|
| 256 | 259 |
if commands:
|
| 257 |
- with self.timed_activity("Running configure-commands"):
|
|
| 260 |
+ with sandbox.queue_context_manager(self.timed_activity("Running configure-commands")):
|
|
| 258 | 261 |
for cmd in commands:
|
| 259 |
- self.__run_command(sandbox, cmd, 'configure-commands')
|
|
| 262 |
+ self.__queue_command(sandbox, cmd, 'configure-commands')
|
|
| 260 | 263 |
|
| 261 | 264 |
def generate_script(self):
|
| 262 | 265 |
script = ""
|
| ... | ... | @@ -281,14 +284,18 @@ class BuildElement(Element): |
| 281 | 284 |
|
| 282 | 285 |
return commands
|
| 283 | 286 |
|
| 284 |
- def __run_command(self, sandbox, cmd, cmd_name):
|
|
| 285 |
- self.status("Running {}".format(cmd_name), detail=cmd)
|
|
| 287 |
+ def __queue_command(self, sandbox, cmd, cmd_name):
|
|
| 288 |
+ def start_cb():
|
|
| 289 |
+ self.status("Running {}".format(cmd_name), detail=cmd)
|
|
| 290 |
+ |
|
| 291 |
+ def complete_cb(exitcode):
|
|
| 292 |
+ if exitcode != 0:
|
|
| 293 |
+ raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode),
|
|
| 294 |
+ collect=self.get_variable('install-root'))
|
|
| 286 | 295 |
|
| 287 | 296 |
# Note the -e switch to 'sh' means to exit with an error
|
| 288 | 297 |
# if any untested command fails.
|
| 289 | 298 |
#
|
| 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'))
|
|
| 299 |
+ sandbox.queue(['sh', '-c', '-e', cmd + '\n'],
|
|
| 300 |
+ start_callback=start_cb,
|
|
| 301 |
+ complete_callback=complete_cb)
|
| ... | ... | @@ -79,6 +79,7 @@ import copy |
| 79 | 79 |
from collections import OrderedDict
|
| 80 | 80 |
from collections.abc import Mapping
|
| 81 | 81 |
from contextlib import contextmanager
|
| 82 |
+from functools import partial
|
|
| 82 | 83 |
import tempfile
|
| 83 | 84 |
import shutil
|
| 84 | 85 |
|
| ... | ... | @@ -753,11 +754,12 @@ class Element(Plugin): |
| 753 | 754 |
detail += " " + " ".join(["/" + f + "\n" for f in value])
|
| 754 | 755 |
self.warn("Ignored files", detail=detail)
|
| 755 | 756 |
|
| 756 |
- def integrate(self, sandbox):
|
|
| 757 |
+ def integrate(self, sandbox, *, queue=False):
|
|
| 757 | 758 |
"""Integrate currently staged filesystem against this artifact.
|
| 758 | 759 |
|
| 759 | 760 |
Args:
|
| 760 | 761 |
sandbox (:class:`.Sandbox`): The build sandbox
|
| 762 |
+ queue (bool): Whether to queue commands or immediately run them
|
|
| 761 | 763 |
|
| 762 | 764 |
This modifies the sysroot staged inside the sandbox so that
|
| 763 | 765 |
the sysroot is *integrated*. Only an *integrated* sandbox
|
| ... | ... | @@ -768,14 +770,33 @@ class Element(Plugin): |
| 768 | 770 |
bstdata = self.get_public_data('bst')
|
| 769 | 771 |
environment = self.get_environment()
|
| 770 | 772 |
|
| 773 |
+ if not queue:
|
|
| 774 |
+ # Element.integrate() must be called with queue=True if there are
|
|
| 775 |
+ # commands pending. Otherwise these commands are run implicitly
|
|
| 776 |
+ # by the call to run_queue() at the end of this method, which is
|
|
| 777 |
+ # likely not expected by the plugin.
|
|
| 778 |
+ assert not sandbox._has_commands_queued()
|
|
| 779 |
+ |
|
| 771 | 780 |
if bstdata is not None:
|
| 772 | 781 |
commands = self.node_get_member(bstdata, list, 'integration-commands', [])
|
| 773 | 782 |
for i in range(len(commands)):
|
| 774 | 783 |
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))
|
|
| 784 |
+ |
|
| 785 |
+ def start_cb(cmd):
|
|
| 786 |
+ self.status("Running integration command", detail=cmd)
|
|
| 787 |
+ |
|
| 788 |
+ def complete_cb(cmd, exitcode):
|
|
| 789 |
+ if exitcode != 0:
|
|
| 790 |
+ raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode))
|
|
| 791 |
+ |
|
| 792 |
+ sandbox.queue(['sh', '-e', '-c', cmd], env=environment, cwd='/',
|
|
| 793 |
+ start_callback=partial(start_cb, cmd),
|
|
| 794 |
+ complete_callback=partial(complete_cb, cmd))
|
|
| 795 |
+ |
|
| 796 |
+ if not queue:
|
|
| 797 |
+ # The caller does not allow keeping pending commands in the queue.
|
|
| 798 |
+ # Immediately run the pending commands.
|
|
| 799 |
+ sandbox.run_queue(0)
|
|
| 779 | 800 |
|
| 780 | 801 |
def stage_sources(self, sandbox, directory):
|
| 781 | 802 |
"""Stage this element's sources to a directory in the sandbox
|
| ... | ... | @@ -2093,7 +2114,15 @@ class Element(Plugin): |
| 2093 | 2114 |
self.prepare(sandbox)
|
| 2094 | 2115 |
|
| 2095 | 2116 |
if workspace:
|
| 2096 |
- workspace.prepared = True
|
|
| 2117 |
+ def mark_workspace_prepared():
|
|
| 2118 |
+ workspace.prepared = True
|
|
| 2119 |
+ |
|
| 2120 |
+ if sandbox._has_commands_queued():
|
|
| 2121 |
+ # Defer workspace.prepared setting until the queued
|
|
| 2122 |
+ # prepare commands have been executed.
|
|
| 2123 |
+ sandbox.queue(None, start_callback=mark_workspace_prepared)
|
|
| 2124 |
+ else:
|
|
| 2125 |
+ mark_workspace_prepared()
|
|
| 2097 | 2126 |
|
| 2098 | 2127 |
def __is_cached(self, keystrength):
|
| 2099 | 2128 |
if keystrength is None:
|
| ... | ... | @@ -19,11 +19,14 @@ |
| 19 | 19 |
# Jim MacArthur <jim macarthur codethink co uk>
|
| 20 | 20 |
|
| 21 | 21 |
import os
|
| 22 |
+import shlex
|
|
| 23 |
+from collections import deque
|
|
| 22 | 24 |
from urllib.parse import urlparse
|
| 23 | 25 |
|
| 24 | 26 |
import grpc
|
| 25 | 27 |
|
| 26 | 28 |
from . import Sandbox
|
| 29 |
+from .sandbox import _QueueEntryType
|
|
| 27 | 30 |
from ..storage._filebaseddirectory import FileBasedDirectory
|
| 28 | 31 |
from ..storage._casbaseddirectory import CasBasedDirectory
|
| 29 | 32 |
from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2, remote_execution_pb2_grpc
|
| ... | ... | @@ -189,6 +192,8 @@ class SandboxRemote(Sandbox): |
| 189 | 192 |
self._set_virtual_directory(new_dir)
|
| 190 | 193 |
|
| 191 | 194 |
def run(self, command, flags, *, cwd=None, env=None):
|
| 195 |
+ stdout, stderr = self._get_output()
|
|
| 196 |
+ |
|
| 192 | 197 |
# Upload sources
|
| 193 | 198 |
upload_vdir = self.get_virtual_directory()
|
| 194 | 199 |
|
| ... | ... | @@ -241,13 +246,90 @@ class SandboxRemote(Sandbox): |
| 241 | 246 |
|
| 242 | 247 |
action_result = execution_response.result
|
| 243 | 248 |
|
| 249 |
+ if stdout:
|
|
| 250 |
+ if action_result.stdout_raw:
|
|
| 251 |
+ stdout.write(str(action_result.stdout_raw, 'utf-8', errors='ignore'))
|
|
| 252 |
+ if stderr:
|
|
| 253 |
+ if action_result.stderr_raw:
|
|
| 254 |
+ stderr.write(str(action_result.stderr_raw, 'utf-8', errors='ignore'))
|
|
| 255 |
+ |
|
| 244 | 256 |
if action_result.exit_code != 0:
|
| 245 | 257 |
# A normal error during the build: the remote execution system
|
| 246 | 258 |
# has worked correctly but the command failed.
|
| 247 |
- # action_result.stdout and action_result.stderr also contains
|
|
| 248 |
- # build command outputs which we ignore at the moment.
|
|
| 249 | 259 |
return action_result.exit_code
|
| 250 | 260 |
|
| 251 | 261 |
self.process_job_output(action_result.output_directories, action_result.output_files)
|
| 252 | 262 |
|
| 253 | 263 |
return 0
|
| 264 |
+ |
|
| 265 |
+ def run_queue(self, flags):
|
|
| 266 |
+ # If a command fails, all remaining commands in the queue must be discarded.
|
|
| 267 |
+ queue = self._queue
|
|
| 268 |
+ self._queue = []
|
|
| 269 |
+ |
|
| 270 |
+ script = ""
|
|
| 271 |
+ i = 0
|
|
| 272 |
+ first = None
|
|
| 273 |
+ for entry in queue:
|
|
| 274 |
+ if entry.type == _QueueEntryType.COMMAND and entry.command:
|
|
| 275 |
+ if first is None:
|
|
| 276 |
+ # First command in queue
|
|
| 277 |
+ first = entry
|
|
| 278 |
+ else:
|
|
| 279 |
+ if entry.cwd != cwd:
|
|
| 280 |
+ script += "mkdir -p {}\n".format(entry.cwd)
|
|
| 281 |
+ script += "cd {}\n".format(entry.cwd)
|
|
| 282 |
+ for key in env.keys():
|
|
| 283 |
+ if key not in entry.env:
|
|
| 284 |
+ script += "unset {}\n".format(key)
|
|
| 285 |
+ for key, value in entry.env.items():
|
|
| 286 |
+ if key not in env or env[key] != value:
|
|
| 287 |
+ script += "export {}={}\n".format(key, value)
|
|
| 288 |
+ cwd = entry.cwd
|
|
| 289 |
+ env = entry.env
|
|
| 290 |
+ |
|
| 291 |
+ cmdline = ' '.join(shlex.quote(cmd) for cmd in entry.command)
|
|
| 292 |
+ script += "({})\n".format(cmdline)
|
|
| 293 |
+ script += "RETVAL=$?\n"
|
|
| 294 |
+ script += "if [ $RETVAL -ne 0 ] ; then\n"
|
|
| 295 |
+ # Report failing command and exit code to stderr (and then back to client)
|
|
| 296 |
+ script += " echo -e '\nbst-command-failure:' {} $RETVAL >&2\n".format(i)
|
|
| 297 |
+ script += " exit 1\n"
|
|
| 298 |
+ script += "fi\n"
|
|
| 299 |
+ i += 1
|
|
| 300 |
+ |
|
| 301 |
+ if first:
|
|
| 302 |
+ exit_code = self.run(['sh', '-c', script], flags, cwd=first.cwd, env=first.env)
|
|
| 303 |
+ else:
|
|
| 304 |
+ exit_code = 0
|
|
| 305 |
+ |
|
| 306 |
+ if exit_code != 0:
|
|
| 307 |
+ # TODO get failed command and exit code from stderr
|
|
| 308 |
+ failed_command = 0
|
|
| 309 |
+ command_exit_code = 1
|
|
| 310 |
+ |
|
| 311 |
+ i = 0
|
|
| 312 |
+ # Stack of pending context manager exit methods
|
|
| 313 |
+ exit_stack = deque()
|
|
| 314 |
+ try:
|
|
| 315 |
+ for entry in queue:
|
|
| 316 |
+ if entry.type == _QueueEntryType.COMMAND:
|
|
| 317 |
+ entry.start_callback()
|
|
| 318 |
+ if exit_code == 0 or i < failed_command:
|
|
| 319 |
+ # Command succeeded
|
|
| 320 |
+ entry.complete_callback(0)
|
|
| 321 |
+ else:
|
|
| 322 |
+ # Command failed
|
|
| 323 |
+ entry.complete_callback(command_exit_code)
|
|
| 324 |
+ break
|
|
| 325 |
+ elif entry.type == _QueueEntryType.CONTEXT_MANAGER_ENTER:
|
|
| 326 |
+ entry.cm.__enter__()
|
|
| 327 |
+ exit_stack.append(entry.cm.__exit__)
|
|
| 328 |
+ elif entry.type == _QueueEntryType.CONTEXT_MANAGER_EXIT:
|
|
| 329 |
+ exit_cb = exit_stack.pop()
|
|
| 330 |
+ exit_cb(None, None, None)
|
|
| 331 |
+ i += 1
|
|
| 332 |
+ finally:
|
|
| 333 |
+ while exit_stack:
|
|
| 334 |
+ exit_cb = exit_stack.pop()
|
|
| 335 |
+ exit_cb(None, None, None)
|
| ... | ... | @@ -29,6 +29,9 @@ See also: :ref:`sandboxing`. |
| 29 | 29 |
"""
|
| 30 | 30 |
|
| 31 | 31 |
import os
|
| 32 |
+from collections import deque, namedtuple
|
|
| 33 |
+from contextlib import contextmanager
|
|
| 34 |
+ |
|
| 32 | 35 |
from .._exceptions import ImplError, BstError
|
| 33 | 36 |
from ..storage._filebaseddirectory import FileBasedDirectory
|
| 34 | 37 |
from ..storage._casbaseddirectory import CasBasedDirectory
|
| ... | ... | @@ -71,6 +74,22 @@ class SandboxFlags(): |
| 71 | 74 |
"""
|
| 72 | 75 |
|
| 73 | 76 |
|
| 77 |
+# _QueueEntryType()
|
|
| 78 |
+#
|
|
| 79 |
+# A type for sandbox queue entries.
|
|
| 80 |
+#
|
|
| 81 |
+class _QueueEntryType():
|
|
| 82 |
+ |
|
| 83 |
+ # A queued command.
|
|
| 84 |
+ COMMAND = 1
|
|
| 85 |
+ |
|
| 86 |
+ # A queued context manager enter.
|
|
| 87 |
+ CONTEXT_MANAGER_ENTER = 2
|
|
| 88 |
+ |
|
| 89 |
+ # A queued context manager exit.
|
|
| 90 |
+ CONTEXT_MANAGER_EXIT = 3
|
|
| 91 |
+ |
|
| 92 |
+ |
|
| 74 | 93 |
class Sandbox():
|
| 75 | 94 |
"""Sandbox()
|
| 76 | 95 |
|
| ... | ... | @@ -121,6 +140,9 @@ class Sandbox(): |
| 121 | 140 |
# directory via get_directory.
|
| 122 | 141 |
self._never_cache_vdirs = False
|
| 123 | 142 |
|
| 143 |
+ # Queued commands
|
|
| 144 |
+ self._queue = []
|
|
| 145 |
+ |
|
| 124 | 146 |
def get_directory(self):
|
| 125 | 147 |
"""Fetches the sandbox root directory
|
| 126 | 148 |
|
| ... | ... | @@ -237,6 +259,97 @@ class Sandbox(): |
| 237 | 259 |
raise ImplError("Sandbox of type '{}' does not implement run()"
|
| 238 | 260 |
.format(type(self).__name__))
|
| 239 | 261 |
|
| 262 |
+ def queue(self, command, *, cwd=None, env=None, start_callback=None, complete_callback=None):
|
|
| 263 |
+ """Queue a command to be run in the sandbox.
|
|
| 264 |
+ |
|
| 265 |
+ If the command fails, commands queued later will not be executed.
|
|
| 266 |
+ The callbacks are not guaranteed to be invoked in real time.
|
|
| 267 |
+ |
|
| 268 |
+ Args:
|
|
| 269 |
+ command (list): The command to run in the sandboxed environment, as a list
|
|
| 270 |
+ of strings starting with the binary to run.
|
|
| 271 |
+ cwd (str): The sandbox relative working directory in which to run the command.
|
|
| 272 |
+ env (dict): A dictionary of string key, value pairs to set as environment
|
|
| 273 |
+ variables inside the sandbox environment.
|
|
| 274 |
+ start_callback (callable): Called when the command starts.
|
|
| 275 |
+ complete_callback (callble): Called when the command completes
|
|
| 276 |
+ with the exit code as argument.
|
|
| 277 |
+ |
|
| 278 |
+ .. note::
|
|
| 279 |
+ |
|
| 280 |
+ The optional *cwd* argument will default to the value set with
|
|
| 281 |
+ :func:`~buildstream.sandbox.Sandbox.set_work_directory`
|
|
| 282 |
+ |
|
| 283 |
+ *Since: 1.4*
|
|
| 284 |
+ """
|
|
| 285 |
+ entry = namedtuple('QueueEntry', ['type', 'command', 'cwd', 'env', 'start_callback', 'complete_callback'])
|
|
| 286 |
+ entry.type = _QueueEntryType.COMMAND
|
|
| 287 |
+ entry.command = command
|
|
| 288 |
+ entry.cwd = self._get_work_directory(cwd=cwd)
|
|
| 289 |
+ entry.env = self._get_environment(cwd=cwd, env=env)
|
|
| 290 |
+ entry.start_callback = start_callback
|
|
| 291 |
+ entry.complete_callback = complete_callback
|
|
| 292 |
+ self._queue.append(entry)
|
|
| 293 |
+ |
|
| 294 |
+ @contextmanager
|
|
| 295 |
+ def queue_context_manager(self, cm):
|
|
| 296 |
+ """Queue entering and exiting a context manager
|
|
| 297 |
+ |
|
| 298 |
+ Args:
|
|
| 299 |
+ cm: A context manager
|
|
| 300 |
+ |
|
| 301 |
+ *Since: 1.4*
|
|
| 302 |
+ """
|
|
| 303 |
+ entry = namedtuple('QueueEntry', ['type', 'cm'])
|
|
| 304 |
+ entry.type = _QueueEntryType.CONTEXT_MANAGER_ENTER
|
|
| 305 |
+ entry.cm = cm
|
|
| 306 |
+ self._queue.append(entry)
|
|
| 307 |
+ yield
|
|
| 308 |
+ entry = namedtuple('QueueEntry', ['type'])
|
|
| 309 |
+ entry.type = _QueueEntryType.CONTEXT_MANAGER_EXIT
|
|
| 310 |
+ self._queue.append(entry)
|
|
| 311 |
+ |
|
| 312 |
+ def run_queue(self, flags):
|
|
| 313 |
+ """Run a command in the sandbox.
|
|
| 314 |
+ |
|
| 315 |
+ Args:
|
|
| 316 |
+ flags (:class:`.SandboxFlags`): The flags for running this command.
|
|
| 317 |
+ |
|
| 318 |
+ Raises:
|
|
| 319 |
+ (:class:`.ProgramNotFoundError`): If a host tool which the given sandbox
|
|
| 320 |
+ implementation requires is not found.
|
|
| 321 |
+ |
|
| 322 |
+ *Since: 1.4*
|
|
| 323 |
+ """
|
|
| 324 |
+ # If a command fails, all remaining commands in the queue must be discarded.
|
|
| 325 |
+ queue = self._queue
|
|
| 326 |
+ self._queue = []
|
|
| 327 |
+ |
|
| 328 |
+ # Stack of pending context manager exit methods
|
|
| 329 |
+ exit_stack = deque()
|
|
| 330 |
+ try:
|
|
| 331 |
+ for entry in queue:
|
|
| 332 |
+ if entry.type == _QueueEntryType.COMMAND:
|
|
| 333 |
+ if entry.start_callback:
|
|
| 334 |
+ entry.start_callback()
|
|
| 335 |
+ if entry.command:
|
|
| 336 |
+ # pylint: disable=assignment-from-no-return
|
|
| 337 |
+ exit_code = self.run(entry.command, flags, cwd=entry.cwd, env=entry.env)
|
|
| 338 |
+ if entry.complete_callback:
|
|
| 339 |
+ entry.complete_callback(exit_code)
|
|
| 340 |
+ if exit_code != 0:
|
|
| 341 |
+ break
|
|
| 342 |
+ elif entry.type == _QueueEntryType.CONTEXT_MANAGER_ENTER:
|
|
| 343 |
+ entry.cm.__enter__()
|
|
| 344 |
+ exit_stack.append(entry.cm.__exit__)
|
|
| 345 |
+ elif entry.type == _QueueEntryType.CONTEXT_MANAGER_EXIT:
|
|
| 346 |
+ exit_cb = exit_stack.pop()
|
|
| 347 |
+ exit_cb(None, None, None)
|
|
| 348 |
+ finally:
|
|
| 349 |
+ while exit_stack:
|
|
| 350 |
+ exit_cb = exit_stack.pop()
|
|
| 351 |
+ exit_cb(None, None, None)
|
|
| 352 |
+ |
|
| 240 | 353 |
################################################
|
| 241 | 354 |
# Private methods #
|
| 242 | 355 |
################################################
|
| ... | ... | @@ -385,3 +498,12 @@ class Sandbox(): |
| 385 | 498 |
return True
|
| 386 | 499 |
|
| 387 | 500 |
return False
|
| 501 |
+ |
|
| 502 |
+ # _has_commands_queued()
|
|
| 503 |
+ #
|
|
| 504 |
+ # Returns whether the sandbox has a non-empty queue of pending commands.
|
|
| 505 |
+ #
|
|
| 506 |
+ # Returns:
|
|
| 507 |
+ # (bool): Whether the command queue is non-empty.
|
|
| 508 |
+ def _has_commands_queued(self):
|
|
| 509 |
+ return len(self._queue) > 0
|
| ... | ... | @@ -3,6 +3,7 @@ import pytest |
| 3 | 3 |
from tests.testutils import cli
|
| 4 | 4 |
|
| 5 | 5 |
from buildstream import _yaml
|
| 6 |
+from buildstream._frontend.app import App
|
|
| 6 | 7 |
from buildstream._exceptions import ErrorDomain, LoadErrorReason
|
| 7 | 8 |
from buildstream._versions import BST_FORMAT_VERSION
|
| 8 | 9 |
|
| ... | ... | @@ -98,3 +99,34 @@ def test_bad_element_path(cli, tmpdir, element_path): |
| 98 | 99 |
'init', '--project-name', 'foo', '--element-path', element_path
|
| 99 | 100 |
])
|
| 100 | 101 |
result.assert_main_error(ErrorDomain.APP, 'invalid-element-path')
|
| 102 |
+ |
|
| 103 |
+ |
|
| 104 |
+@pytest.mark.parametrize("element_path", [('foo'), ('foo/bar')])
|
|
| 105 |
+def test_element_path_interactive(cli, tmp_path, monkeypatch, element_path):
|
|
| 106 |
+ project = tmp_path
|
|
| 107 |
+ project_conf_path = project.joinpath('project.conf')
|
|
| 108 |
+ |
|
| 109 |
+ class DummyInteractiveApp(App):
|
|
| 110 |
+ def __init__(self, *args, **kwargs):
|
|
| 111 |
+ super().__init__(*args, **kwargs)
|
|
| 112 |
+ self.interactive = True
|
|
| 113 |
+ |
|
| 114 |
+ @classmethod
|
|
| 115 |
+ def create(cls, *args, **kwargs):
|
|
| 116 |
+ return DummyInteractiveApp(*args, **kwargs)
|
|
| 117 |
+ |
|
| 118 |
+ def _init_project_interactive(self, *args, **kwargs):
|
|
| 119 |
+ return ('project_name', '0', element_path)
|
|
| 120 |
+ |
|
| 121 |
+ monkeypatch.setattr(App, 'create', DummyInteractiveApp.create)
|
|
| 122 |
+ |
|
| 123 |
+ result = cli.run(project=str(project), args=['init'])
|
|
| 124 |
+ result.assert_success()
|
|
| 125 |
+ |
|
| 126 |
+ full_element_path = project.joinpath(element_path)
|
|
| 127 |
+ assert full_element_path.exists()
|
|
| 128 |
+ |
|
| 129 |
+ project_conf = _yaml.load(str(project_conf_path))
|
|
| 130 |
+ assert project_conf['name'] == 'project_name'
|
|
| 131 |
+ assert project_conf['format-version'] == '0'
|
|
| 132 |
+ assert project_conf['element-path'] == element_path
|
