| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| # 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 typing import List, Optional, TYPE_CHECKING |
| |
| from .element import Element |
| |
| if TYPE_CHECKING: |
| from typing import Dict, Tuple |
| |
| |
| class ScriptElement(Element): |
| __install_root = "/" |
| __cwd = "/" |
| __root_read_only = False |
| __commands = None # type: OrderedDict[str, List[str]] |
| __layout = {} # type: Dict[str, List[Tuple[Element, str]]] |
| |
| # 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 |
| |
| ############################################################# |
| # Public Methods # |
| ############################################################# |
| |
| def set_work_dir(self, work_dir: Optional[str] = None) -> None: |
| """Sets the working dir |
| |
| The working dir (a.k.a. cwd) is the directory which commands will be |
| called from. |
| |
| Args: |
| work_dir: 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: Optional[str] = None) -> 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: 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: bool) -> None: |
| """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: Whether to mark the root filesystem as read-only. |
| """ |
| self.__root_read_only = root_read_only |
| |
| def layout_add(self, element: Element, dependency_path: str, location: str) -> None: |
| """Adds an element to the layout. |
| |
| Layout is a way of defining how dependencies should be added to the |
| staging area for running commands. |
| |
| Args: |
| element (Element): The element to stage. |
| dependency_path (str): The element relative path to the dependency, usually obtained via |
| :attr:`the dependency configuration <buildstream.element.DependencyConfiguration.path>` |
| location (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 build dependencies of the element in question at the |
| sandbox root. Otherwise, the specified elements including their |
| runtime dependencies will be staged in their respective locations. |
| |
| .. note:: |
| |
| The order of directories in the layout is not significant. |
| |
| The paths in the layout will be sorted so that elements are staged in parent |
| directories before subdirectories. |
| |
| The elements for each respective staging directory in the layout will be staged |
| in the predetermined deterministic staging order. |
| """ |
| # |
| # Even if this is an empty dict by default, make sure that it is |
| # instance data instead of appending stuff directly onto class data. |
| # |
| if not self.__layout: |
| self.__layout = {} |
| |
| # Get or create the element list |
| try: |
| element_list = self.__layout[location] |
| except KeyError: |
| element_list = [] |
| self.__layout[location] = element_list |
| |
| element_list.append((element, dependency_path)) |
| |
| def add_commands(self, group_name: str, command_list: List[str]) -> None: |
| """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 |
| |
| ############################################################# |
| # Abstract Method Implementations # |
| ############################################################# |
| def preflight(self): |
| pass |
| |
| def get_unique_key(self): |
| sorted_locations = sorted(self.__layout) |
| layout_key = { |
| location: [dependency_path for _, dependency_path in self.__layout[location]] |
| for location in sorted_locations |
| } |
| return { |
| "commands": self.__commands, |
| "cwd": self.__cwd, |
| "install-root": self.__install_root, |
| "layout": layout_key, |
| "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()) |
| |
| # Mark the install root |
| sandbox.mark_directory(self.__install_root) |
| |
| def stage(self, sandbox): |
| |
| # If self.layout_add() was never called, do the default staging of |
| # everything in "/" and run the integration commands |
| if not self.__layout: |
| |
| with self.timed_activity("Staging dependencies", silent_nested=True): |
| self.stage_dependency_artifacts(sandbox) |
| |
| with sandbox.batch(label="Integrating sandbox"): |
| for dep in self.dependencies(): |
| dep.integrate(sandbox) |
| |
| else: |
| # First stage it all |
| # |
| sorted_locations = sorted(self.__layout) |
| |
| for location in sorted_locations: |
| with self.timed_activity("Staging dependencies at: {}".format(location), silent_nested=True): |
| element_list = [element for element, _ in self.__layout[location]] |
| self.stage_dependency_artifacts(sandbox, element_list, path=location) |
| |
| # Now integrate any elements staged in the root |
| # |
| root_list = self.__layout.get("/", None) |
| if root_list: |
| element_list = [element for element, _ in root_list] |
| with sandbox.batch(), self.timed_activity("Integrating sandbox", silent_nested=True): |
| for dep in self.dependencies(element_list): |
| dep.integrate(sandbox) |
| |
| # Ensure the install root exists |
| # |
| sandbox.get_virtual_directory().open_directory(self.__install_root.lstrip(os.sep), create=True) |
| |
| def assemble(self, sandbox): |
| with sandbox.batch(root_read_only=self.__root_read_only, collect=self.__install_root): |
| for groupname, commands in self.__commands.items(): |
| with sandbox.batch(root_read_only=self.__root_read_only, 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"], root_read_only=self.__root_read_only, label=cmd) |
| |
| # Empty the build directory after a successful build to avoid the |
| # overhead of capturing the build directory. |
| self.run_cleanup_commands(sandbox) |
| |
| # Return where the result can be collected from |
| return self.__install_root |
| |
| |
| def setup(): |
| return ScriptElement |