blob: 40355f5cfb5c99d01a5b103c3106a77edf24daa2 [file] [log] [blame]
#
# 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:
# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
"""
BuildElement - Abstract class for build elements
================================================
The BuildElement class is a convenience element one can derive from for
implementing the most common case of element.
.. _core_buildelement_builtins:
Built-in functionality
----------------------
The BuildElement base class provides built in functionality that could be
overridden by the individual plugins.
This section will give a brief summary of how some of the common features work,
some of them or the variables they use will be further detailed in the following
sections.
The `strip-binaries` variable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `strip-binaries` variable is by default **empty**. You need to use the
appropiate commands depending of the system you are building.
If you are targetting Linux, ones known to work are the ones used by the
`freedesktop-sdk <https://freedesktop-sdk.io/>`_, you can take a look to them in their
`project.conf <https://gitlab.com/freedesktop-sdk/freedesktop-sdk/blob/freedesktop-sdk-18.08.21/project.conf#L74>`_
Location for staging dependencies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The BuildElement supports the "location" :term:`dependency configuration <Dependency configuration>`,
which means you can use this configuration for any BuildElement class.
The "location" configuration defines where the dependency will be staged in the
build sandbox.
**Example:**
Here is an example of how one might stage some dependencies into
an alternative location while staging some elements in the sandbox root.
.. code:: yaml
# Stage these build dependencies in /opt
#
build-depends:
- baseproject.bst:opt-dependencies.bst
config:
location: /opt
# Stage these tools in "/" and require them as
# runtime dependencies.
depends:
- baseproject.bst:base-tools.bst
.. note::
The order of dependencies specified is not significant.
The staging locations will be sorted so that elements are staged in parent
directories before subdirectories.
Location for running commands
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``command-subdir`` variable sets where commands will be executed,
and the directory will be created automatically if it does not exist.
The ``command-subdir`` is a relative path from ``%{build-root}``, and
cannot be a parent or adjacent directory, it must expand to a subdirectory
of ``${build-root}``.
Location for configuring the project
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``conf-root`` is the location that specific build elements can use to look for build configuration files.
This is used by elements such as `autotools <https://apache.github.io/buildstream-plugins/elements/autotools.html>`_,
`cmake <https://apache.github.io/buildstream-plugins/elements/cmake.html>`_,
`meson <https://apache.github.io/buildstream-plugins/elements/meson.html>`_,
`setuptools <https://apache.github.io/buildstream-plugins/elements/setuptools.html>`_ and
`pip <https://apache.github.io/buildstream-plugins/elements/pip.html>`_.
The default value of ``conf-root`` is defined by default as ``.``. This means that if
the ``conf-root`` is not explicitly set to another directory, the configuration
files are expected to be found in ``command-subdir``.
Separating source and build directories
'''''''''''''''''''''''''''''''''''''''
A typical example of using ``conf-root`` is when performing
`autotools <https://apache.github.io/buildstream-plugins/elements/autotools.html>`_ builds
where your source directory is separate from your build directory.
This can be achieved in build elements which use ``conf-root`` as follows:
.. code:: yaml
variables:
# Specify that build configuration scripts are found in %{build-root}
conf-root: "%{build-root}"
# The build will run in the `_build` subdirectory
command-subdir: _build
Install Location
~~~~~~~~~~~~~~~~
Build elements must install the build output to the directory defined by ``install-root``.
You need not set or change the ``install-root`` variable as it will be defined
automatically on your behalf, and it is used to collect build output when creating
the resulting artifacts.
It is important to know about ``install-root`` in order to write your own
custom install instructions, for example the
`cmake <https://apache.github.io/buildstream-plugins/elements/cmake.html>`_
element will use it to specify the ``DESTDIR``.
Abstract method implementations
-------------------------------
Element.configure_sandbox()
~~~~~~~~~~~~~~~~~~~~~~~~~~~
In :func:`Element.configure_sandbox() <buildstream.element.Element.configure_sandbox>`,
the BuildElement will ensure that the sandbox locations described by the ``%{build-root}``
and ``%{install-root}`` variables are marked and will be mounted read-write for the
:func:`assemble phase<buildstream.element.Element.configure_sandbox>`.
The working directory for the sandbox will be configured to be the ``%{build-root}``,
unless the ``%{command-subdir}`` variable is specified for the element in question,
in which case the working directory will be configured as ``%{build-root}/%{command-subdir}``.
Element.stage()
~~~~~~~~~~~~~~~
In :func:`Element.stage() <buildstream.element.Element.stage>`, the BuildElement
will do the following operations:
* Stage all of the build dependencies into the sandbox root.
* Run the integration commands for all staged dependencies using
:func:`Element.integrate() <buildstream.element.Element.integrate>`
* Stage any Source on the given element to the ``%{build-root}`` location
inside the sandbox, using
:func:`Element.stage_sources() <buildstream.element.Element.integrate>`
Element.assemble()
~~~~~~~~~~~~~~~~~~
In :func:`Element.assemble() <buildstream.element.Element.assemble>`, the
BuildElement will proceed to run sandboxed commands which are expected to be
found in the element configuration.
Commands are run in the following order:
* ``configure-commands``: Commands to configure the build scripts
* ``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
The result of the build is expected to end up in ``%{install-root}``, and
as such; Element.assemble() method will return the ``%{install-root}`` for
artifact collection purposes.
.. note::
In the case that the element is currently workspaced, the ``configure-commands``
will only be run in subsequent builds until they succeed at least once, unless
:ref:`bst workspace reset --soft <invoking_workspace_reset>` is called on the
workspace to explicitly avoid an incremental build.
"""
import os
from .element import Element
_command_steps = ["configure-commands", "build-commands", "install-commands", "strip-commands"]
class BuildElement(Element):
#############################################################
# Abstract Method Implementations #
#############################################################
def configure(self, node):
self.__commands = {} # pylint: disable=attribute-defined-outside-init
# FIXME: Currently this forcefully validates configurations
# for all BuildElement subclasses so they are unable to
# extend the configuration
node.validate_keys(_command_steps)
self._command_subdir = self.get_variable("command-subdir") # pylint: disable=attribute-defined-outside-init
for command_name in _command_steps:
self.__commands[command_name] = node.get_str_list(command_name, [])
def configure_dependencies(self, dependencies):
self.__layout = {} # pylint: disable=attribute-defined-outside-init
# FIXME: Currently this forcefully validates configurations
# for all BuildElement subclasses so they are unable to
# extend the configuration
for dep in dependencies:
# Determine the location to stage each element, default is "/"
location = "/"
if dep.config:
dep.config.validate_keys(["location"])
location = dep.config.get_str("location")
try:
element_list = self.__layout[location]
except KeyError:
element_list = []
self.__layout[location] = element_list
element_list.append((dep.element, dep.path))
def preflight(self):
pass
def get_unique_key(self):
dictionary = {}
for command_name, command_list in self.__commands.items():
dictionary[command_name] = command_list
if self._command_subdir:
dictionary["command-subdir"] = self._command_subdir
# 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 affect
# the cache key.
if self.get_variable("notparallel"):
dictionary["notparallel"] = True
# Specify the layout in the key, if any of the elements are not going to
# be staged in "/"
#
if any(location for location in self.__layout if location != "/"):
sorted_locations = sorted(self.__layout)
layout_key = {
location: [dependency_path for _, dependency_path in self.__layout[location]]
for location in sorted_locations
}
dictionary["layout"] = layout_key
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
if self._command_subdir:
command_dir = os.path.join(build_root, self._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):
# 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 self.timed_activity("Integrating sandbox", silent_nested=True), sandbox.batch():
for dep in self.dependencies(element_list):
dep.integrate(sandbox)
# Stage sources in the build root
self.stage_sources(sandbox, self.get_variable("build-root"))
def assemble(self, sandbox):
with sandbox.batch(root_read_only=True, label="Running commands"):
# We need to ensure that configure-commands are only called
# once in workspaces, because the changes will persist across
# incremental builds - not desirable, for example, in the case
# of autotools, we don't want to run `./configure` a second time
# in an incremental build if it has succeeded at least once.
#
# Here we use an empty file `.bst-prepared` as a marker of whether
# configure-commands have already completed successfully in a previous build.
#
needs_configure = True
marker_filename = ".bst-prepared"
commands = self.__commands["configure-commands"]
if commands:
if self._get_workspace():
vdir = sandbox.get_virtual_directory()
buildroot = self.get_variable("build-root")
buildroot_vdir = vdir.open_directory(buildroot.lstrip(os.sep))
# Marker found, no need to configure
if buildroot_vdir.exists(marker_filename):
needs_configure = False
if needs_configure:
for cmd in commands:
self.__run_command(sandbox, cmd)
# This will serialize a command to create the marker file
# in the sandbox batch after running configure
if self._get_workspace():
sandbox._create_empty_file(marker_filename)
# Run commands
for command_name in _command_steps:
commands = self.__commands[command_name]
if not commands or command_name == "configure-commands":
continue
for cmd in commands:
self.__run_command(sandbox, cmd)
# Empty the build directory after a successful build to avoid the
# overhead of capturing the build directory.
self.run_cleanup_commands(sandbox)
# Return the payload, this is configurable but is generally
# always the /buildstream-install directory
return self.get_variable("install-root")
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
#############################################################
# Private Local Methods #
#############################################################
def __run_command(self, sandbox, cmd):
# 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=True, label=cmd)