A “platform” is user-defined, and describes the operating system, CPU architecture, and the runtime ABI to dynamic-link against. Bazel includes some constraints in the @platforms
repo, for example:
platform(
name = "linux_aarch64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:aarch64"
],
)
Platforms can also include map of properties for executing actions on them, typically used with Remote Build Execution:
platform(
name = "x86_64_linux_remote",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
exec_properties = {
"OSFamily": "Linux",
"container-image": "docker://ghcr.io/catthehacker/ubuntu:act-22.04@sha256:5f9c35c25db1d51a8ddaae5c0ba8d3c163c5e9a4a6cc97acd409ac7eae239448",
},
)
There are several platforms involved in a build:
- Dev: machine where the code is cloned
- Host: machine where Bazel is running
- Exec: machine where tools like compilers run
- Target: machine where the built program will run
Cross-compiling produces binaries for a different target platform from the exec platform.
Requirements of cross compilation
- Compiler toolchain that supports the particular combination of exec → target platform. eg llvm/clang.
- A sysroot that matches the target platform
Why cross-compile?
- You can compile code for any target platform regardless of where Bazel runs
- Fully reproducible and repeatable, eg: any update the developer machine does not break the build - No glibc version skews that surface later during deployment
Sysroot
A sysroot contains all the linkable libraries and headers needed for compilation and linking.
Typical contents include /usr/lib[64]
and /usr/include
.
Here is a list of sysroot generators we know of:
- https://github.com/scasagrande/toolchains_llvm_sysroot
- https://github.com/keith/bazel-cc-sysroot-generator
- https://github.com/malt3/sysroots
- https://github.com/f0rmiga/gcc-toolchain
- https://github.com/lukasoyen/bazel_linux_packages
Example: Targeting GNU/Linux arm64
For the following example, we will not build our own sysroot, instead we will use an existing sysroot for Linux aarch64
CPU architecture. We’ll just use the first one in the list above, but don’t have a strong reason to prefer one over another.
We can run the llvm/clang compiler to compile a simple C binary for a target platform.
Add this to MODULE.bazel
# fetch a sysroot built for GNU/Linux 4.18.0
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "sysroot_linux_aarch64_2_28",
url = "https://github.com/scasagrande/toolchains_llvm_sysroot/releases/download/linux-sysroot-4_18-2_28-10_3_0/linux-sysroot-aarch64.tar.zst",
integrity = "sha256-hGKzwbu31ua17Ejjtk1ZZxFVsKUioo47yr72e87T4Ag=",
# Don’t forget to create an empty BUILD file in vendor directory
build_file = "//vendor:sysroot.BUILD"
)
# And instruct llvm toolchain to the sysroot we fetched above for any linux-aarch64
# builds.
llvm.sysroot(
name = "llvm_toolchain",
label = "@sysroot_linux_aarch64_2_28//:sysroot",
targets = ["linux-aarch64"],
)
filegroup(
name = "sysroot",
srcs = glob(["**"]),
visibility = ["//visibility:public"]
)
Now let’s define the target platform and a cc_binary in our BUILD
file
cc_binary(
name = "main",
srcs = ["main.cc"],
)
./main.cc
#include <stdio.h>
#include <gnu/libc-version.h>
int main() {
printf("hello\n");
printf("GNU libc version is: %s\n", gnu_get_libc_version());
return 0;
}
Now you can compile the binary above by running; bazel build :main --platforms=//tools/platforms:linux_aarch64
Now lets put this binary into a Docker container with a newer glibc version than the sysroot, which ought to work since glibc releases are backwards-compatible.
Append to MODULE.bazel
:
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
oci.pull(
name = "cc_debian12",
digest = "sha256:c53c9416a1acdbfd6e09abba720442444a3d1a6338b8db850e5e198b59af5570",
image = "gcr.io/distroless/cc-debian12",
platforms = [
"linux/arm64/v8",
],
)
use_repo(oci, "cc_debian12")
Append to BUILD.bazel
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load")
load("@tar.bzl", "tar")
tar(
name = "binary_layer",
mtree = ["./usr/bin/say_hi type=file contents=$(location :main)"],
srcs = [":main"]
)
oci_image(
name = "image",
base = "@cc_debian12",
tars = [":binary_layer"],
entrypoint = ["/usr/bin/say_hi"]
)
oci_load(
name = "load_into_docker",
image = ":image",
repo_tags = ["app:latest"]
)
Now run bazel run :load_into_docker --platforms=//tools/platforms:linux_aarch64
which will build your binary and put into a container and load onto the docker daemon running locally.
Once it loads, run docker run app:latest
, you should see the following output.
hello
GNU libc version is: 2.36
Transitions
By default Bazel will compile your code for the platform that you are running on and in order to tell it to compile for a specific target platform, we have to use the --platforms
flag.
However this doesn't work for more than one platform simultaneously, even though the flag signifies plurality. In addition to this limitation, its not always easy to remember what flags you are supposed to pass. Instead, we can use transitions.
Transitions define configuration changes between rules. For example, a request like "compile my dependency for a different CPU than its parent" is handled by a transition.
Lets fix the BUILD file so that we don’t need to specify —platforms flag anymore, to do that we’ll use `platform_transition_binary` to transition our binary to be built for a specific platform.
load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_binary")
platform_transition_binary(
name = "main_aarch64",
srcs = [":main"],
target_platform = "//tools/platforms:linux_aarch64"
)
Now we can build the binary using the following command: bazel build :main_aarch64
Targeting macOS
Cross compiling to macOS is almost identical to targeting GNU/Linux with a few considerations. the headers and libraries for targeting macOS is distributed by Apple and requires installation of Xcode, including accepting a Terms of Service. For instance Aspect builds its own macOS.sdk distribution for hermetic macos builds.
Targeting Windows
Independent of Bazel, cross-compiling to windows is not easy so we won’t be teaching it here.