summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJürg Billeter <j@bitron.ch>2018-08-08 14:35:25 +0200
committerWilliam Salmon <will.salmon@codethink.co.uk>2019-07-25 13:57:18 +0100
commit560cbb40f51a51eb9a4e0bcb701b4c64ba0f7a00 (patch)
tree034e6ba7098f2c786f9d03db0cd138412275a1a6
parentb54c8cb07ca257be79940ffa70853bf75d2c287c (diff)
downloadbuildstream-560cbb40f51a51eb9a4e0bcb701b4c64ba0f7a00.tar.gz
sandbox: Add initial SandboxBuildBox
-rw-r--r--src/buildstream/_platform/linux.py25
-rw-r--r--src/buildstream/sandbox/_sandboxbuildbox.py247
-rw-r--r--src/buildstream/sandbox/_sandboxbwrap.py2
-rw-r--r--src/buildstream/sandbox/sandbox.py16
4 files changed, 289 insertions, 1 deletions
diff --git a/src/buildstream/_platform/linux.py b/src/buildstream/_platform/linux.py
index b69dd456e..b400bfaac 100644
--- a/src/buildstream/_platform/linux.py
+++ b/src/buildstream/_platform/linux.py
@@ -24,6 +24,7 @@ from .. import utils
from ..sandbox import SandboxDummy
from .platform import Platform
+from .._exceptions import PlatformError
class Linux(Platform):
@@ -31,6 +32,7 @@ class Linux(Platform):
def _setup_sandbox(self, force_sandbox):
sandbox_setups = {
'bwrap': self._setup_bwrap_sandbox,
+ 'buildbox': self._setup_buildbox_sandbox,
'chroot': self._setup_chroot_sandbox,
'dummy': self._setup_dummy_sandbox,
}
@@ -67,6 +69,7 @@ class Linux(Platform):
# Private Methods #
################################################
+ # Dummy sandbox methods
@staticmethod
def _check_dummy_sandbox_config(config):
return True
@@ -81,6 +84,7 @@ class Linux(Platform):
self.create_sandbox = self._create_dummy_sandbox
return True
+ # Bubble-wrap sandbox methods
def _check_sandbox_config_bwrap(self, config):
from ..sandbox._sandboxbwrap import SandboxBwrap
return SandboxBwrap.check_sandbox_config(self, config)
@@ -103,6 +107,7 @@ class Linux(Platform):
self.create_sandbox = self._create_bwrap_sandbox
return True
+ # Chroot sandbox methods
def _check_sandbox_config_chroot(self, config):
from ..sandbox._sandboxchroot import SandboxChroot
return SandboxChroot.check_sandbox_config(self, config)
@@ -118,3 +123,23 @@ class Linux(Platform):
self.check_sandbox_config = self._check_sandbox_config_chroot
self.create_sandbox = Linux._create_chroot_sandbox
return True
+
+ # Buildbox sandbox methods
+ def _check_sandbox_config_buildbox(self, config):
+ from ..sandbox._sandboxbuildbox import SandboxBuildBox
+ return SandboxBuildBox.check_sandbox_config(self, config)
+
+ @staticmethod
+ def _create_buildbox_sandbox(*args, **kwargs):
+ from ..sandbox._sandboxbuildbox import SandboxBuildBox
+ if kwargs.get('allow_real_directory'):
+ raise PlatformError("The BuildBox Sandbox does not support real directories.",
+ reason="You are using BuildBox sandbox because BST_FORCE_SANBOX=buildbox")
+ return SandboxBuildBox(*args, **kwargs)
+
+ def _setup_buildbox_sandbox(self):
+ from ..sandbox._sandboxbuildbox import SandboxBuildBox
+ self._check_sandbox(SandboxBuildBox)
+ self.check_sandbox_config = self._check_sandbox_config_buildbox
+ self.create_sandbox = self._create_buildbox_sandbox
+ return True
diff --git a/src/buildstream/sandbox/_sandboxbuildbox.py b/src/buildstream/sandbox/_sandboxbuildbox.py
new file mode 100644
index 000000000..417d2224d
--- /dev/null
+++ b/src/buildstream/sandbox/_sandboxbuildbox.py
@@ -0,0 +1,247 @@
+#
+# Copyright (C) 2018 Bloomberg LP
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import signal
+import subprocess
+from contextlib import ExitStack
+
+import psutil
+
+from .. import utils, _signals, ProgramNotFoundError
+from . import Sandbox, SandboxFlags
+from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2
+from ..storage._casbaseddirectory import CasBasedDirectory
+from .._exceptions import SandboxError
+
+
+# SandboxBuidBox()
+#
+# BuildBox-based sandbox implementation.
+#
+class SandboxBuildBox(Sandbox):
+
+ def __init__(self, context, project, directory, **kwargs):
+ if kwargs.get('allow_real_directory'):
+ raise SandboxError("BuildBox does not support real directories")
+ else:
+ kwargs['allow_real_directory'] = False
+ super().__init__(context, project, directory, **kwargs)
+
+ @classmethod
+ def check_available(cls):
+ try:
+ utils.get_host_tool('buildbox')
+ except utils.ProgramNotFoundError as Error:
+ cls._dummy_reasons += ["buildbox not found"]
+ raise SandboxError(" and ".join(cls._dummy_reasons),
+ reason="unavailable-local-sandbox") from Error
+
+ @classmethod
+ def check_sandbox_config(cls, platform, config):
+ # Report error for elements requiring non-0 UID/GID
+ # TODO
+ if config.build_uid != 0 or config.build_gid != 0:
+ return False
+
+ # Check host os and architecture match
+ if config.build_os != platform.get_host_os():
+ raise SandboxError("Configured and host OS don't match.")
+ elif config.build_arch != platform.get_host_arch():
+ raise SandboxError("Configured and host architecture don't match.")
+
+ return True
+
+ def _run(self, command, flags, *, cwd, env):
+ stdout, stderr = self._get_output()
+
+ root_directory = self.get_virtual_directory()
+ scratch_directory = self._get_scratch_directory()
+
+ if not self._has_command(command[0], env):
+ raise SandboxError("Staged artifacts do not provide command "
+ "'{}'".format(command[0]),
+ reason='missing-command')
+
+ # Grab the full path of the buildbox binary
+ try:
+ buildbox_command = [utils.get_host_tool('buildbox')]
+ except ProgramNotFoundError as Err:
+ raise SandboxError(("BuildBox not on path, you are using the BuildBox sandbox because "
+ "BST_FORCE_SANDBOX=buildbox")) from Err
+
+ for mark in self._get_marked_directories():
+ path = mark['directory']
+ assert path.startswith('/') and len(path) > 1
+ root_directory.descend(*path[1:].split(os.path.sep), create=True)
+
+ digest = root_directory._get_digest()
+ with open(os.path.join(scratch_directory, 'in'), 'wb') as input_digest_file:
+ input_digest_file.write(digest.SerializeToString())
+
+ buildbox_command += ["--local=" + root_directory.cas_cache.casdir]
+ buildbox_command += ["--input-digest=in"]
+ buildbox_command += ["--output-digest=out"]
+
+ common_details = ("BuildBox is a experimental sandbox and does not support the requested feature.\n"
+ "You are using this feature because BST_FORCE_SANDBOX=buildbox.")
+
+ if not flags & SandboxFlags.NETWORK_ENABLED:
+ # TODO
+ self._issue_warning(
+ "BuildBox sandbox does not have Networking yet",
+ detail=common_details
+ )
+
+ if cwd is not None:
+ buildbox_command += ['--chdir=' + cwd]
+
+ # In interactive mode, we want a complete devpts inside
+ # the container, so there is a /dev/console and such. In
+ # the regular non-interactive sandbox, we want to hand pick
+ # a minimal set of devices to expose to the sandbox.
+ #
+ if flags & SandboxFlags.INTERACTIVE:
+ # TODO
+ self._issue_warning(
+ "BuildBox sandbox does not fully support BuildStream shells yet",
+ detail=common_details
+ )
+
+ if flags & SandboxFlags.ROOT_READ_ONLY:
+ # TODO
+ self._issue_warning(
+ "BuildBox sandbox does not fully support BuildStream `Read only Root`",
+ detail=common_details
+ )
+
+ # Set UID and GID
+ if not flags & SandboxFlags.INHERIT_UID:
+ # TODO
+ self._issue_warning(
+ "BuildBox sandbox does not fully support BuildStream Inherit UID",
+ detail=common_details
+ )
+
+ os.makedirs(os.path.join(scratch_directory, 'mnt'), exist_ok=True)
+ buildbox_command += ['mnt']
+
+ # Add the command
+ buildbox_command += command
+
+ # Use the MountMap context manager to ensure that any redirected
+ # mounts through fuse layers are in context and ready for buildbox
+ # to mount them from.
+ #
+ with ExitStack() as stack:
+ # Ensure the cwd exists
+ if cwd is not None and len(cwd) > 1:
+ assert cwd.startswith('/')
+ root_directory.descend(*cwd[1:].split(os.path.sep), create=True)
+
+ # If we're interactive, we want to inherit our stdin,
+ # otherwise redirect to /dev/null, ensuring process
+ # disconnected from terminal.
+ if flags & SandboxFlags.INTERACTIVE:
+ stdin = sys.stdin
+ else:
+ stdin = stack.enter_context(open(os.devnull, "r"))
+
+ # Run buildbox !
+ exit_code = self.run_buildbox(buildbox_command, stdin, stdout, stderr, env,
+ interactive=(flags & SandboxFlags.INTERACTIVE),
+ cwd=scratch_directory)
+
+ if exit_code == 0:
+ with open(os.path.join(scratch_directory, 'out'), 'rb') as output_digest_file:
+ output_digest = remote_execution_pb2.Digest()
+ output_digest.ParseFromString(output_digest_file.read())
+ self._vdir = CasBasedDirectory(root_directory.cas_cache, digest=output_digest)
+
+ return exit_code
+
+ def run_buildbox(self, argv, stdin, stdout, stderr, env, *, interactive, cwd):
+ def kill_proc():
+ if process:
+ # First attempt to gracefully terminate
+ proc = psutil.Process(process.pid)
+ proc.terminate()
+
+ try:
+ proc.wait(20)
+ except psutil.TimeoutExpired:
+ utils._kill_process_tree(process.pid)
+
+ def suspend_proc():
+ group_id = os.getpgid(process.pid)
+ os.killpg(group_id, signal.SIGSTOP)
+
+ def resume_proc():
+ group_id = os.getpgid(process.pid)
+ os.killpg(group_id, signal.SIGCONT)
+
+ with _signals.suspendable(suspend_proc, resume_proc), _signals.terminator(kill_proc):
+ process = subprocess.Popen(
+ argv,
+ close_fds=True,
+ env=env,
+ stdin=stdin,
+ stdout=stdout,
+ stderr=stderr,
+ cwd=cwd,
+ start_new_session=interactive
+ )
+
+ # Wait for the child process to finish, ensuring that
+ # a SIGINT has exactly the effect the user probably
+ # expects (i.e. let the child process handle it).
+ try:
+ while True:
+ try:
+ _, status = os.waitpid(process.pid, 0)
+ # If the process exits due to a signal, we
+ # brutally murder it to avoid zombies
+ if not os.WIFEXITED(status):
+ utils._kill_process_tree(process.pid)
+
+ # Unlike in the bwrap case, here only the main
+ # process seems to receive the SIGINT. We pass
+ # on the signal to the child and then continue
+ # to wait.
+ except KeyboardInterrupt:
+ process.send_signal(signal.SIGINT)
+ continue
+
+ break
+ # If we can't find the process, it has already died of
+ # its own accord, and therefore we don't need to check
+ # or kill anything.
+ except psutil.NoSuchProcess:
+ pass
+
+ # Return the exit code - see the documentation for
+ # os.WEXITSTATUS to see why this is required.
+ if os.WIFEXITED(status):
+ exit_code = os.WEXITSTATUS(status)
+ else:
+ exit_code = -1
+
+ return exit_code
+
+ def _use_cas_based_directory(self):
+ # Always use CasBasedDirectory for BuildBox
+ return True
diff --git a/src/buildstream/sandbox/_sandboxbwrap.py b/src/buildstream/sandbox/_sandboxbwrap.py
index 1155793c6..81e9f34de 100644
--- a/src/buildstream/sandbox/_sandboxbwrap.py
+++ b/src/buildstream/sandbox/_sandboxbwrap.py
@@ -336,7 +336,7 @@ class SandboxBwrap(Sandbox):
# The only message relevant to us now is the exit-code of the subprocess.
for line in json_status_file:
with suppress(json.decoder.JSONDecodeError):
- o = json.loads(line)
+ o = json.loads(line.decode())
if isinstance(o, collections.abc.Mapping) and 'exit-code' in o:
child_exit_code = o['exit-code']
break
diff --git a/src/buildstream/sandbox/sandbox.py b/src/buildstream/sandbox/sandbox.py
index 3229b2dc6..ece15c949 100644
--- a/src/buildstream/sandbox/sandbox.py
+++ b/src/buildstream/sandbox/sandbox.py
@@ -613,6 +613,22 @@ class Sandbox():
self._build_directory = directory
self._build_directory_always = always
+ # _issue_warning()
+ #
+ # Issue warning with __context that is not available with subclasses
+ #
+ # Args:
+ # message (str): A message to issue
+ # details (str): optional, more detatils
+ def _issue_warning(self, message, detail=None):
+ self.__context.messenger.message(
+ Message(None,
+ MessageType.WARN,
+ message,
+ detail=detail
+ )
+ )
+
# _SandboxBatch()
#