scriptelement.py 11.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#
#  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>

20
"""
21 22
ScriptElement - Abstract class for scripting elements
=====================================================
23
The ScriptElement class is a convenience class one can derive for
24 25 26 27 28 29 30 31 32 33 34 35 36 37
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

38 39 40
from .element import Element, ElementError
from .sandbox import SandboxFlags
from .types import Scope
41 42 43 44 45 46 47


class ScriptElement(Element):
    __install_root = "/"
    __cwd = "/"
    __root_read_only = False
    __commands = None
48
    __layout = []
49

50
    # The compose element's output is its dependencies, so
51 52 53 54
    # we must rebuild if the dependencies change even when
    # not in strict build plans.
    #
    BST_STRICT_REBUILD = True
55

56 57 58 59 60 61 62 63
    # 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

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
    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:
116
          element (str): The name of the element to stage, or None. This may be any
117 118
                         element found in the dependencies, whether it is a direct
                         or indirect dependency.
119
          destination (str): The path inside the staging area for where to
120 121
                             stage this element. If it is not "/", then integration
                             commands will not be run.
122 123 124 125 126

        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.
127 128 129 130 131 132 133 134 135 136 137

        .. 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.
138
        """
139
        #
140
        # Even if this is an empty list by default, make sure that its
141 142
        # instance data instead of appending stuff directly onto class data.
        #
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
        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:
180 181 182 183
                if item['element']:
                    if not self.search(Scope.BUILD, item['element']):
                        raise ElementError("{}: '{}' in layout not found in dependencies"
                                           .format(self, item['element']))
184 185 186 187 188 189 190 191 192 193 194 195 196 197

    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
        }

198 199 200 201 202 203 204 205
    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())

206
        # Tell the sandbox to mount the install root
207
        directories = {self.__install_root: False}
208

209 210
        # Mark the artifact directories in the layout
        for item in self.__layout:
211 212 213
            destination = item['destination']
            was_artifact = directories.get(destination, False)
            directories[destination] = item['element'] or was_artifact
214

215
        for directory, artifact in directories.items():
216 217 218 219
            # 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)
220

221 222
    def stage(self, sandbox):

223 224 225 226 227
        # 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 /"
228
                                         .format(build_dep.name), silent_nested=True):
229
                    build_dep.stage_dependency_artifacts(sandbox, Scope.RUN, path="/")
230

231 232 233 234 235
            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)
236 237 238
        else:
            # If layout, follow its rules.
            for item in self.__layout:
239 240 241 242 243 244

                # Skip layout members which dont stage an element
                if not item['element']:
                    continue

                element = self.search(Scope.BUILD, item['element'])
245
                if item['destination'] == '/':
246
                    with self.timed_activity("Staging {} at /".format(element.name),
247
                                             silent_nested=True):
248
                        element.stage_dependency_artifacts(sandbox, Scope.RUN)
249 250
                else:
                    with self.timed_activity("Staging {} at {}"
251
                                             .format(element.name, item['destination']),
252
                                             silent_nested=True):
253
                        virtual_dstdir = sandbox.get_virtual_directory()
254
                        virtual_dstdir.descend(*item['destination'].lstrip(os.sep).split(os.sep), create=True)
255
                        element.stage_dependency_artifacts(sandbox, Scope.RUN, path=item['destination'])
256

257 258
            with sandbox.batch(SandboxFlags.NONE):
                for item in self.__layout:
259

260 261 262
                    # Skip layout members which dont stage an element
                    if not item['element']:
                        continue
263

264
                    element = self.search(Scope.BUILD, item['element'])
265

266 267 268 269 270 271
                    # 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)
272

273
        install_root_path_components = self.__install_root.lstrip(os.sep).split(os.sep)
274
        sandbox.get_virtual_directory().descend(*install_root_path_components, create=True)
275

276 277
    def assemble(self, sandbox):

278 279 280 281
        flags = SandboxFlags.NONE
        if self.__root_read_only:
            flags |= SandboxFlags.ROOT_READ_ONLY

282 283 284 285 286 287 288 289 290
        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)
291 292 293 294 295 296 297

        # Return where the result can be collected from
        return self.__install_root


def setup():
    return ScriptElement