summaryrefslogtreecommitdiff
path: root/src/buildstream/scriptelement.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/buildstream/scriptelement.py')
-rw-r--r--src/buildstream/scriptelement.py297
1 files changed, 297 insertions, 0 deletions
diff --git a/src/buildstream/scriptelement.py b/src/buildstream/scriptelement.py
new file mode 100644
index 000000000..dfdbb45c0
--- /dev/null
+++ b/src/buildstream/scriptelement.py
@@ -0,0 +1,297 @@
+#
+# Copyright (C) 2017 Codethink Limited
+#
+# 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/>.
+#
+# Authors:
+# Jonathan Maw <jonathan.maw@codethink.co.uk>
+
+"""
+ScriptElement - Abstract class for scripting elements
+=====================================================
+The ScriptElement class is a convenience class one can derive for
+implementing elements that stage elements and run command-lines on them.
+
+Any derived classes must write their own configure() implementation, using
+the public APIs exposed in this class.
+
+Derived classes must also chain up to the parent method in their preflight()
+implementations.
+
+
+"""
+
+import os
+from collections import OrderedDict
+
+from .element import Element, ElementError
+from .sandbox import SandboxFlags
+from .types import Scope
+
+
+class ScriptElement(Element):
+ __install_root = "/"
+ __cwd = "/"
+ __root_read_only = False
+ __commands = None
+ __layout = []
+
+ # The compose element's output is its dependencies, so
+ # we must rebuild if the dependencies change even when
+ # not in strict build plans.
+ #
+ BST_STRICT_REBUILD = True
+
+ # Script artifacts must never have indirect dependencies,
+ # so runtime dependencies are forbidden.
+ BST_FORBID_RDEPENDS = True
+
+ # This element ignores sources, so we should forbid them from being
+ # added, to reduce the potential for confusion
+ BST_FORBID_SOURCES = True
+
+ def set_work_dir(self, work_dir=None):
+ """Sets the working dir
+
+ The working dir (a.k.a. cwd) is the directory which commands will be
+ called from.
+
+ Args:
+ work_dir (str): The working directory. If called without this argument
+ set, it'll default to the value of the variable ``cwd``.
+ """
+ if work_dir is None:
+ self.__cwd = self.get_variable("cwd") or "/"
+ else:
+ self.__cwd = work_dir
+
+ def set_install_root(self, install_root=None):
+ """Sets the install root
+
+ The install root is the directory which output will be collected from
+ once the commands have been run.
+
+ Args:
+ install_root(str): The install root. If called without this argument
+ set, it'll default to the value of the variable ``install-root``.
+ """
+ if install_root is None:
+ self.__install_root = self.get_variable("install-root") or "/"
+ else:
+ self.__install_root = install_root
+
+ def set_root_read_only(self, root_read_only):
+ """Sets root read-only
+
+ When commands are run, if root_read_only is true, then the root of the
+ filesystem will be protected. This is strongly recommended whenever
+ possible.
+
+ If this variable is not set, the default permission is read-write.
+
+ Args:
+ root_read_only (bool): Whether to mark the root filesystem as
+ read-only.
+ """
+ self.__root_read_only = root_read_only
+
+ def layout_add(self, element, destination):
+ """Adds an element-destination pair to the layout.
+
+ Layout is a way of defining how dependencies should be added to the
+ staging area for running commands.
+
+ Args:
+ element (str): The name of the element to stage, or None. This may be any
+ element found in the dependencies, whether it is a direct
+ or indirect dependency.
+ destination (str): The path inside the staging area for where to
+ stage this element. If it is not "/", then integration
+ commands will not be run.
+
+ If this function is never called, then the default behavior is to just
+ stage the Scope.BUILD dependencies of the element in question at the
+ sandbox root. Otherwise, the Scope.RUN dependencies of each specified
+ element will be staged in their specified destination directories.
+
+ .. note::
+
+ The order of directories in the layout is significant as they
+ will be mounted into the sandbox. It is an error to specify a parent
+ directory which will shadow a directory already present in the layout.
+
+ .. note::
+
+ In the case that no element is specified, a read-write directory will
+ be made available at the specified location.
+ """
+ #
+ # Even if this is an empty list by default, make sure that its
+ # instance data instead of appending stuff directly onto class data.
+ #
+ if not self.__layout:
+ self.__layout = []
+ self.__layout.append({"element": element,
+ "destination": destination})
+
+ def add_commands(self, group_name, command_list):
+ """Adds a list of commands under the group-name.
+
+ .. note::
+
+ Command groups will be run in the order they were added.
+
+ .. note::
+
+ This does not perform substitutions automatically. They must
+ be performed beforehand (see
+ :func:`~buildstream.element.Element.node_subst_list`)
+
+ Args:
+ group_name (str): The name of the group of commands.
+ command_list (list): The list of commands to be run.
+ """
+ if not self.__commands:
+ self.__commands = OrderedDict()
+ self.__commands[group_name] = command_list
+
+ def __validate_layout(self):
+ if self.__layout:
+ # Cannot proceeed if layout is used, but none are for "/"
+ root_defined = any([(entry['destination'] == '/') for entry in self.__layout])
+ if not root_defined:
+ raise ElementError("{}: Using layout, but none are staged as '/'"
+ .format(self))
+
+ # Cannot proceed if layout specifies an element that isn't part
+ # of the dependencies.
+ for item in self.__layout:
+ if item['element']:
+ if not self.search(Scope.BUILD, item['element']):
+ raise ElementError("{}: '{}' in layout not found in dependencies"
+ .format(self, item['element']))
+
+ def preflight(self):
+ # The layout, if set, must make sense.
+ self.__validate_layout()
+
+ def get_unique_key(self):
+ return {
+ 'commands': self.__commands,
+ 'cwd': self.__cwd,
+ 'install-root': self.__install_root,
+ 'layout': self.__layout,
+ 'root-read-only': self.__root_read_only
+ }
+
+ def configure_sandbox(self, sandbox):
+
+ # Setup the environment and work directory
+ sandbox.set_work_directory(self.__cwd)
+
+ # Setup environment
+ sandbox.set_environment(self.get_environment())
+
+ # Tell the sandbox to mount the install root
+ directories = {self.__install_root: False}
+
+ # Mark the artifact directories in the layout
+ for item in self.__layout:
+ destination = item['destination']
+ was_artifact = directories.get(destination, False)
+ directories[destination] = item['element'] or was_artifact
+
+ for directory, artifact in directories.items():
+ # Root does not need to be marked as it is always mounted
+ # with artifact (unless explicitly marked non-artifact)
+ if directory != '/':
+ sandbox.mark_directory(directory, artifact=artifact)
+
+ def stage(self, sandbox):
+
+ # Stage the elements, and run integration commands where appropriate.
+ if not self.__layout:
+ # if no layout set, stage all dependencies into /
+ for build_dep in self.dependencies(Scope.BUILD, recurse=False):
+ with self.timed_activity("Staging {} at /"
+ .format(build_dep.name), silent_nested=True):
+ build_dep.stage_dependency_artifacts(sandbox, Scope.RUN, path="/")
+
+ with sandbox.batch(SandboxFlags.NONE):
+ for build_dep in self.dependencies(Scope.BUILD, recurse=False):
+ with self.timed_activity("Integrating {}".format(build_dep.name), silent_nested=True):
+ for dep in build_dep.dependencies(Scope.RUN):
+ dep.integrate(sandbox)
+ else:
+ # If layout, follow its rules.
+ for item in self.__layout:
+
+ # Skip layout members which dont stage an element
+ if not item['element']:
+ continue
+
+ element = self.search(Scope.BUILD, item['element'])
+ if item['destination'] == '/':
+ with self.timed_activity("Staging {} at /".format(element.name),
+ silent_nested=True):
+ element.stage_dependency_artifacts(sandbox, Scope.RUN)
+ else:
+ with self.timed_activity("Staging {} at {}"
+ .format(element.name, item['destination']),
+ silent_nested=True):
+ virtual_dstdir = sandbox.get_virtual_directory()
+ virtual_dstdir.descend(*item['destination'].lstrip(os.sep).split(os.sep), create=True)
+ element.stage_dependency_artifacts(sandbox, Scope.RUN, path=item['destination'])
+
+ with sandbox.batch(SandboxFlags.NONE):
+ for item in self.__layout:
+
+ # Skip layout members which dont stage an element
+ if not item['element']:
+ continue
+
+ element = self.search(Scope.BUILD, item['element'])
+
+ # Integration commands can only be run for elements staged to /
+ if item['destination'] == '/':
+ with self.timed_activity("Integrating {}".format(element.name),
+ silent_nested=True):
+ for dep in element.dependencies(Scope.RUN):
+ dep.integrate(sandbox)
+
+ install_root_path_components = self.__install_root.lstrip(os.sep).split(os.sep)
+ sandbox.get_virtual_directory().descend(*install_root_path_components, create=True)
+
+ def assemble(self, sandbox):
+
+ flags = SandboxFlags.NONE
+ if self.__root_read_only:
+ flags |= SandboxFlags.ROOT_READ_ONLY
+
+ with sandbox.batch(flags, collect=self.__install_root):
+ for groupname, commands in self.__commands.items():
+ with sandbox.batch(flags, label="Running '{}'".format(groupname)):
+ for cmd in commands:
+ # Note the -e switch to 'sh' means to exit with an error
+ # if any untested command fails.
+ sandbox.run(['sh', '-c', '-e', cmd + '\n'],
+ flags,
+ label=cmd)
+
+ # Return where the result can be collected from
+ return self.__install_root
+
+
+def setup():
+ return ScriptElement