NativeLink’s Local Remote Execution is a framework to build, distribute, and
rapidly iterate on custom toolchain setups that are transparent, fully hermetic,
and reproducible across machines of the same system architecture.
Local Remote Execution mirrors toolchains for remote execution in your local
development environment. This lets you reuse build artifacts with virtually
perfect cache hit rate across different repositories, developers, and CI.
Your project must have a Nix flake and use flake-parts.
Add the nativelink repository to your flake inputs and make the nixpkgs from
nativelink the global input for your project:
Then, in your flake-parts imports, add the nativelink flake module:
Finally, add the lre.bazelrc generator in your devShell’s shellHook:
Now, when you enter the nix flake (automated via direnv or manually via
nix develop) it will generate a file called lre.bazelrc which contains
information about the LRE toolchain and configures Bazel. With the default
configuration it looks like this:
In the snippet above you can see a warning that no local toolchain is
configured. LRE needs to know the remote toolchain configuration to make it
available locally. The local-remote-execution settings take an Env input and
an optional prefix input to configure the generated lre.bazelrc:
Exit and re-enter the flake. The lre.bazelrc should look now like this:
The commented paths created in lre.bazelrc technically have no effect on what
Bazel’s toolchain creates, but for nix to be able to generate the store paths
it needs to fetch the files to your local system. In other words, all paths
printed in lre.bazelrc will be available on your local system.
First, hook the generated lre.bazelrc into the main .bazelrc with a
try-import:
Recall that the lre.bazelrc file contains lines like this:
The @local-remote-execution Bazel module contains the Bazel-side configuration
of the LRE toolchain. If you view generated-cc/cc/BUILD
you should find the same /nix/store/... paths as in the lre.bazelrc file.
Now import the LRE toolchains in your MODULE.bazel to make the
@local-remote-execution Bazel module available to your build:
Let’s use NativeLink’s Kubernetes example to verify that the setup worked.
This is only relevant if you’re updating the base toolchains in the nativelink
repository itself. If you run nix flake update in the nativelink repository
you need to update the generated Bazel toolchains as well:
The original C++ and Java toolchain containers are never really instantiated.
Instead, their container environments are used and passed through transforming
functions that take a container schematic as input and generate some other
output.
The first transform is the createWorker wrapper
which converts an arbitrary OCI image to a NativeLink worker:
In the case of LRE the base image is built with Nix for perfect reproducibility.
However, you could use a more classic toolchain container like an Ubuntu base
image as well:
The second transform is some mechanism that generates toolchain configurations
for your respective RBE client:
In many classic setups the RBE client configurations are handwritten. In the
case of LRE we’re generating Bazel toolchains and platforms using a pipeline of
custom image generators and the rbe_configs_gen tool:
When you then invoke your RBE client with the configuration set up to use these
toolchains, the NativeLink scheduler matches actions to the worker they require.
In Bazel’s case this scheduler endpoint is set via the --remote_executor flag.
The general approach described works for arbitrary toolchain containers. You
might need to implement your own logic to get from the toolchain container to
some usable RBE configuration files (or write them manually), but this can be
considered an implementation detail specific to your requirements.
TODO(aaronmondal): Add an example of a custom toolchain extension around lre-cc.