Writing self configuring C/C++ Toolchains

Notice: This article is under construction, some sections may not be complete. This notice will be removed as it exits its initial draft.


The purpose of this article is to detail how to properly write Bazel C/C++ toolchains.

Many resources online will teach you how to write Bazel toolchains by hardcoding paths in a completely non-portable way. While this method is more beginner friendly, it is completely impractical for use by others who may not have your exact version of compiler, and who may have installed their tools in different locations.

Rather, this guide presents a method of flexible toolchain configuration which mimics the way that Bazel natively configures it's built in toolchain.

By the end of this guide, you should be able to define your own gcc or clang toolchain, allowing complete control over the parameters Bazel applies during builds. The framework presented in this article can also be applied to create toolchains for cross compilation.

Setting up a Bazel repository

First, we will set up a very simple bazel repo. The following commands can be copy-pasted to accomplish this:

mkdir repo touch repo/WORKSPACE

That's it! The presense of a WORKSPACE turns a folder, into a bazel repository.

Additionally, lets add some code to test compilation. Create the directory repo/hello_world with the following contents:

  • repo/hello_world/BUILD:

cc_binary( name = "hello_world", srcs = ["hello_world.c"], )
  • repo/hello_world/hello_world.c:

#include <stdio.h> int main() { printf("Hello, World!"); return 0; }

At this point you should be able to build and run our test target with bazel run //hello_world

Toolchain Selection

NOTE: This section describes a depcrecating method of toolchain selction, the cc_toolchain_suite. Until this section is revised, it is recommended to use the modern toolchain resolution system. The toolchain design presented in this article will work just the same with either method.

Before getting in to the design of the toolchain, we must first understand how bazel chooses target toolchains for a build.

  1. Bazel keys it's selection off of two command line parameters:

--cpu (default: "k8" (Linux), "darwin" (OSX), "windows" (Windows) --compiler (default: "")

Either of these parameters can be overriden on the command line, but for the sake of this example we will leave them alone as we are not concerned with cross-compiling.

  1. An applicable toolchain is selected from the cc_toolchain_suite.

By default, bazel will generate a toolchain suite for your system in bazel-repo/external/local_config_cc/BUILD that looks something like this:

# This suite compares 'cpu|compiler' parameters to find an applicable toolchain cc_toolchain_suite( name = "toolchain", toolchains = { "k8|compiler": ":cc-compiler-k8", "k8": ":cc-compiler-k8", "armeabi-v7a|compiler": ":cc-compiler-armeabi-v7a", "armeabi-v7a": ":cc-compiler-armeabi-v7a", }, )

This rule is essentially a dictionary, where cpu|compiler act as a key, and the label of the toolchain, :cc-compiler-k8, would be the selected value.

If we wanted to use a target system other than x86, our bazel build command could have specified bazel build //... --cpu=armeabi-v7a, and Bazel would have selected :cc-compiler-armeabi-v7a

Getting Bazel to select our own toolchains

The next step is to implement our own toolchain suite, and get Bazel to use it. First we will define our toolchain suite:

  • repo/toolchains/BUILD:

cc_toolchain_suite( name = "toolchain_suite", toolchains = { ### X86 Toolchains ### "k8": "@custom_toolchain//:cc-compiler-auto", "darwin": "@custom_toolchain//:cc-compiler-auto", "x64_windows": "@custom_toolchain//:cc-compiler-auto", }, visibility = ["//visibility:public"], )

And next, to give bazel knowledge of this toolchain suite, we order bazel to use it by placing these flags in the .bazelrc:

  • repo/.bazelrc:

# For HOST compilation, just use the default bazel toolchain. We do not care how host tools are built. build --host_crosstool_top='@bazel_tools//tools/cpp:toolchain' # Override the default x86 target toolchain to enable our custom toolchains build --crosstool_top='//toolchains:toolchain_suite'

At this point, we should be able to issue the same bazel run //hello_world command and receive the following error:

ERROR: /home/rdeushane/repo/toolchains/BUILD:1:1: no such package '@custom_toolchain//': The repository '@custom_toolchain' could not be resolved and referenced by '//toolchains:tool chain_suite' ERROR: Analysis of target '//hello_world:hello_world' failed; build aborted: Analysis failed INFO: Elapsed time: 0.083s INFO: 0 processes. FAILED: Build did NOT complete successfully (1 packages loaded, 6 targets configured) FAILED: Build did NOT complete successfully (1 packages loaded, 6 targets configured)

This error is good, this means Bazel has used our toolchain suite, but is failing because we have not yet defined "@custom_toolchain//:cc-compiler-auto"

Toolchain Design

Putting all the pieces together