Bazel can build and test code on a variety of platforms, in a few different configurations. Some of configurations are:
To describe these different scenarios, Bazel has introduced the concept of “platforms”. A platform is a device that can run a program, whether that program is Bazel itself, a compiler or other tool, or the final built output.
The terminology for these types of platforms are:
Typically, the host and execution platform are the same (except in the case of remote execution), since build actions are executed locally by Bazel. If you are building an executable that will run on the same computer where you are using Bazel, then the target platform is the same as the host platform. When you are cross-compiling (say, building an Android application), then the target platform is different from the host platform.
Conceptually in Bazel, a platform is a named collection of constraints. Constraints are just names and values used to define the features of a platform. Some examples of constraints are the CPU architecture, the operating system, the presence of a GPU, or the specific version of a compiler or other tool.
Platforms are defined in Bazel BUILD files, using three new rules:
Bazel automatically defines some constraints and values for you:
TODO(katre): fix table formatting
Here are some examples of defining additional constraints:
constraint_value( name = 'aarm64', constraint_setting = '@bazel_tools//platforms:cpu') constraint_value( name = 'openbsd', constraint_setting = '@bazel_tools//platforms:os') constraint_setting(name = 'glibc_version') constraint_value( name = 'glibc_2_25', constraint_setting = ':glibc_version') constraint_value( name = 'glibc_2_26', constraint_setting = ':glibc_version')
Constraint values can be defined in any package, although constraint settings and values are subject to the usual rules on visibility.
Here are some platforms defined using these constraints:
platform( name = 'linux_x86', constraint_values = [ '@bazel_tools//platforms:linux', '@bazel_tools//platforms:x86_64', ':glibc_2_25', ]) platform( name = 'openbsd_arm', constraint_values = [ ':openbsd', '@bazel_tools//platforms:arm', ])
You can mix constraint values from multiple packages in a single platform. Be careful, however, because you cannot use two constraint values that share a constraint setting! This will be reported as an error.
Being able to define platforms is nice, but what can you do with them? Currently, the main use of platforms and constraints is to have Bazel automatically select the right toolchain for your build.
Note: not all sets of rules support toolchains, notably the native C++ and Java rules do not work with this yet.
The host and target platform can be set via command-line flags:
TODO(katre): fix table formatting
There is two special predefined platforms: @bazel_tools//platforms:host_platform and @bazel_tools//platforms:target_platform. These platforms automatically detect the current host and target CPU and OS (based on the “--host_cpu” and “--cpu” flags) and sets those two constraint values. All other platforms that you want to use will need to be added to your BUILD files.
A toolchain is a configuration provider that allows you to tell a rule what compiler, compiler flags, etc to use. The set of available configuration parameters is up to the rule author.
There are three important definitions for every type of toolchain:
During a build, Bazel will use the current execution and target platforms to find an appropriate toolchain instance for each rule, based on the toolchain's declared constraints. This process is called Toolchain Resolution.
Every set of rules that uses a toolchain needs to define a unique label, the toolchain type, that is used by the rules when requesting a valid toolchain. The toolchain type label is given when the toolchain is defined in the toolchain() rule, so that Bazel can use it as an input to Toolchain Resolution.
To work with toolchains, a rule author needs to add a rule-specific toolchain rule, which defines the values of all configuration parameters the rule uses which change for different execution and target platforms. This rule should return a ToolchainInfo provider.
Instances of the toolchain rule will be lazily instantiated by Bazel only when needed. Because of this, a toolchain rule's dependencies can be as complex as needed, and even reply on remote repositories, and not affect builds where they are not used.
To define a new toolchain in Skylark, first you need to determine what information your rules need. Let‘s consider the case of a new programming language: the rules will need the path to the compiler, the path to the system libraries, and a flag that controls the generated binary’s CPU architecture.
Toolchain rules are ordinary Skylark rules that create and return providers. This example toolchain rule would look like this in Skylark:
def _my_toolchain_impl(ctx): toolchain = platform.ToolchainInfo( compiler = ctx.attr.compiler, system_lib = ctx.attr.system_lib, arch_flag = ctx.attr.arch_flag, ) return [toolchain] my_toolchain = rule( _my_toolchain_impl, attrs = { 'compiler': attr.string(), 'system_lib': attr.string(), 'arch_flags': attr.string_list(), })
And a sample usage would look like:
my_toolchain(name = 'linux_toolchain_impl', compiler = '@remote_linux_repo//compiler:compiler_binary', system_lib = '@remote_linux_repo//library:system_library', arch_flags = [ '--arch=Linux', '--debug_everything', ] ) my_toolchain(name = 'darwin_toolchain_impl', compiler = '@remote_darwin_repo//compiler:compiler_binary', system_lib = '@remote_darwin_repo//library:system_library', arch_flags = [ '--arch=Darwin', #'--debug_everything', # --debug_everything currently broken on Darwin ] )
To accomplish the lazy loading of toolchains, a separate rule called toolchain() is used. This rule tells Bazel the toolchain type, the execution and target constraints, and the label of the actual rule-specific toolchain to be used.
Example toolchain definitions:
toolchain(name = 'linux_toolchain', toolchain_type = '//path/to:my_toolchain_type', exec_compatible_with = [ '@bazel_tools//platforms:linux', '@bazel_tools//platforms:x86_64'], target_compatible_with = [ '@bazel_tools//platforms:linux', '@bazel_tools//platforms:x86_64'], toolchain = ':linux_toolchain_impl', ) toolchain(name = 'darwin_toolchain', toolchain_type = '//path/to:my_toolchain_type', exec_compatible_with = [ '@bazel_tools//platforms:darwin', '@bazel_tools//platforms:x86_64'], target_compatible_with = [ '@bazel_tools//platforms:darwin', '@bazel_tools//platforms:x86_64'], toolchain = ':darwin_toolchain_impl', )
Finally, Bazel needs to know about the toolchain definition, in order to use it. Registering a toolchain can be done in either the WORKSPACE, or via the “--experimental_extra_toolchains” flag.
Example in the WORKSPACE:
register_toolchains( '//path/to:linux_toolchain', '//path/to:darwin_toolchain', )
When a target requires a toolchain, Bazel checks the list of registered toolchains to find the first one that is valid, and creates a dependency from the target to the specific toolchain that was selected. The process looks like this:
Please note that this will always choose the first toolchain that is valid, so toolchains should be ordered by preference if it is possible that multiple could match.
When adding toolchain support to existing rules, it can be difficult to determine where errors are found and what causes problems with toolchain resolution. To help aid development, the “--toolchain_resolution_debug” flag has been added. This flag causes the Toolchain Resolution process to be very verbose about what toolchains are being considered, and why they are being skipped.
In order for rules to use the toolchain, the rule author needs to add the toolchain type to the rule definition:
my_library = rule( ... toolchains = ['//path/to:my_toolchain_type'] ...)
When the rule is used, Bazel will check the execution and target platforms, and choose an appropriate toolchain that matches (or fail with an error message explaining the lack of toolchains). The rule implementation can then access the toolchain as:
def _my_library_impl(ctx): toolchain = ctx.toolchains['//path/to:my_toolchain_type'] command = '%s -l %s %s' % (toolchain.compiler, toolchain.system_lib, toolchain.arch_flag) ...
Platforms can also be used with the config_setting rule to define configurable attributes. See config_setting for more details.