| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2016 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: |
| # Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> |
| |
| """The BuildElement class is a convenience element one can derive from for |
| implementing the most common case of element. |
| |
| |
| Description of assemble activities |
| ---------------------------------- |
| This element will perform the following steps to assemble an element: |
| |
| Stage dependencies |
| ~~~~~~~~~~~~~~~~~~ |
| The dependencies in the :func:`Scope.BUILD <buildstream.element.Scope.BUILD>` |
| scope will be staged at the root of the sandbox |
| |
| Integrate dependencies |
| ~~~~~~~~~~~~~~~~~~~~~~ |
| The integration commands taken from the ``bst`` public domain of each dependency |
| will be run in the sandbox to create and update caches. Typically ``ldconfig`` |
| among other things is run in this step. |
| |
| Stage sources |
| ~~~~~~~~~~~~~ |
| :mod:`Sources <buildstream.source>` are now staged according to their configuration |
| into the ``%{build-root}`` directory (normally ``/buildstream/build``) inside the sandbox. |
| |
| Run commands |
| ~~~~~~~~~~~~ |
| Commands are now run in the sandbox. |
| |
| Commands are taken from the element configuration specified by the given |
| :mod:`BuildElement <buildstream.buildelement>` subclass, which can in turn be |
| overridden by the user in element declarations (``.bst`` files). |
| |
| Commands are run in the following order: |
| |
| * ``configure-commands``: Commands to configure how the element will build |
| * ``build-commands``: Commands to build the element |
| * ``install-commands``: Commands to install the results into ``%{install-root}`` |
| * ``strip-commands``: Commands to strip debugging symbols installed binaries |
| |
| Sometimes it is interesting to append or prepend commands to an existing |
| command list without replacing it entirely, for this; array composition |
| :ref:`prepend <format_directives_list_prepend>` and |
| :ref:`append <format_directives_list_append>` directives can be used. |
| |
| **Example** |
| |
| .. code:: yaml |
| |
| config: |
| configure-commands: |
| (<): |
| - echo "Do something before default configure-commands" |
| |
| **Working Directory** |
| |
| Note that by default the working directory is where the sources are staged in |
| ``%{build-root}``, but this can be overridden to build inside of a subdirectory |
| of the build directory using the ``command-subdir`` variable in an element |
| declaration. e.g.: |
| |
| .. code:: yaml |
| |
| variables: |
| command-subdir: src |
| |
| The above fragment will cause all commands to be run in the ``src/`` subdirectory |
| of the staged sources. |
| |
| |
| Result collection |
| ~~~~~~~~~~~~~~~~~ |
| Finally, the resulting build *artifact* is collected from the the ``%{install-root}`` |
| directory (which is normally configured as ``/buildstream/install``) inside the sandbox. |
| |
| All build elements must install into the ``%{install-root}`` using whatever |
| semantic the given build system provides to do this. E.g. for standard autotools |
| packages we simply do ``make DESTDIR=%{install-root} install``. |
| """ |
| |
| import os |
| from . import Element, Scope, ElementError |
| from . import SandboxFlags |
| |
| |
| # This list is preserved because of an unfortunate situation, we |
| # need to remove these older commands which were secret and never |
| # documented, but without breaking the cache keys. |
| _legacy_command_steps = ['bootstrap-commands', |
| 'configure-commands', |
| 'build-commands', |
| 'test-commands', |
| 'install-commands', |
| 'strip-commands'] |
| |
| _command_steps = ['configure-commands', |
| 'build-commands', |
| 'install-commands', |
| 'strip-commands'] |
| |
| |
| class BuildElement(Element): |
| |
| def configure(self, node): |
| |
| self.commands = {} |
| |
| # FIXME: Currently this forcefully validates configurations |
| # for all BuildElement subclasses so they are unable to |
| # extend the configuration |
| self.node_validate(node, _command_steps) |
| |
| for command_name in _legacy_command_steps: |
| if command_name in _command_steps: |
| self.commands[command_name] = self._get_commands(node, command_name) |
| else: |
| self.commands[command_name] = [] |
| |
| def preflight(self): |
| pass |
| |
| def get_unique_key(self): |
| dictionary = {} |
| |
| for command_name, command_list in self.commands.items(): |
| dictionary[command_name] = command_list |
| |
| # Specifying notparallel for a given element effects the |
| # cache key, while having the side effect of setting max-jobs to 1, |
| # which is normally automatically resolved and does not effect |
| # the cache key. |
| variables = self._get_variables() |
| if self.node_get_member(variables.variables, bool, 'notparallel', False): |
| dictionary['notparallel'] = True |
| |
| return dictionary |
| |
| def configure_sandbox(self, sandbox): |
| build_root = self.get_variable('build-root') |
| install_root = self.get_variable('install-root') |
| |
| # Tell the sandbox to mount the build root and install root |
| sandbox.mark_directory(build_root) |
| sandbox.mark_directory(install_root) |
| |
| # Allow running all commands in a specified subdirectory |
| command_subdir = self.get_variable('command-subdir') |
| if command_subdir: |
| command_dir = os.path.join(build_root, command_subdir) |
| else: |
| command_dir = build_root |
| sandbox.set_work_directory(command_dir) |
| |
| # Setup environment |
| sandbox.set_environment(self.get_environment()) |
| |
| def stage(self, sandbox): |
| |
| # Stage deps in the sandbox root |
| with self.timed_activity("Staging dependencies", silent_nested=True): |
| self.stage_dependency_artifacts(sandbox, Scope.BUILD) |
| |
| # Run any integration commands provided by the dependencies |
| # once they are all staged and ready |
| with self.timed_activity("Integrating sandbox"): |
| for dep in self.dependencies(Scope.BUILD): |
| dep.integrate(sandbox) |
| |
| # Stage sources in the build root |
| self.stage_sources(sandbox, self.get_variable('build-root')) |
| |
| def assemble(self, sandbox): |
| |
| # Run commands |
| for command_name in _command_steps: |
| commands = self.commands[command_name] |
| if not commands: |
| continue |
| |
| with self.timed_activity("Running {}".format(command_name)): |
| for cmd in commands: |
| self.status("Running {}".format(command_name), detail=cmd) |
| |
| # Note the -e switch to 'sh' means to exit with an error |
| # if any untested command fails. |
| # |
| exitcode = sandbox.run(['sh', '-c', '-e', cmd + '\n'], |
| SandboxFlags.ROOT_READ_ONLY) |
| if exitcode != 0: |
| raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode)) |
| |
| # %{install-root}/%{build-root} should normally not be written |
| # to - if an element later attempts to stage to a location |
| # that is not empty, we abort the build - in this case this |
| # will almost certainly happen. |
| staged_build = os.path.join(self.get_variable('install-root'), |
| self.get_variable('build-root')) |
| |
| if os.path.isdir(staged_build) and os.listdir(staged_build): |
| self.warn("Writing to %{install-root}/%{build-root}.", |
| detail="Writing to this directory will almost " + |
| "certainly cause an error, since later elements " + |
| "will not be allowed to stage to %{build-root}.") |
| |
| # Return the payload, this is configurable but is generally |
| # always the /buildstream/install directory |
| return self.get_variable('install-root') |
| |
| def _get_commands(self, node, name): |
| list_node = self.node_get_member(node, list, name, []) |
| commands = [] |
| |
| for i in range(len(list_node)): |
| command = self.node_subst_list_element(node, name, [i]) |
| commands.append(command) |
| |
| return commands |
| |
| def generate_script(self): |
| script = "" |
| for command_name in _command_steps: |
| commands = self.commands[command_name] |
| |
| for cmd in commands: |
| script += "(set -ex; {}\n) || exit 1\n".format(cmd) |
| |
| return script |