Get started with pw_toolchain_bazel#

A modular toolkit for declaring C/C++ toolchains in Bazel

Quick start#

The fastest way to get started using pw_toolchain_bazel is to use Pigweed’s upstream toolchains.

  1. Enable required features in your project’s //.bazelrc file:

    # Required for new toolchain resolution API.
    build --incompatible_enable_cc_toolchain_resolution
    
    # Do not attempt to configure an autodetected (local) toolchain.
    common --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
    
  2. Configure pw_toolchain_bazel in your project’s //WORKSPACE file:

    # Add Pigweed itself, as a submodule from `//third_party/pigweed`.
    #
    # TODO: b/300695111 - Support depending on Pigweed as a git_repository,
    # even if you use pw_toolchain.
    local_repository(
        name = "pigweed",
        path = "third_party/pigweed",
    )
    local_repository(
        name = "pw_toolchain",
        path = "third_party/pigweed/pw_toolchain_bazel",
    )
    
    # Set up CIPD.
    load(
        "@pigweed//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl",
        "cipd_client_repository",
        "cipd_repository",
    )
    
    cipd_client_repository()
    
    # Set up and register Pigweed's toolchains.
    load(
        "@pigweed//pw_toolchain:register_toolchains.bzl",
        "register_pigweed_cxx_toolchains"
    )
    
    register_pigweed_cxx_toolchains()
    

And you’re done! You should now be able to compile for macOS, Linux, and ARM Cortex-M devices.

Overview#

This guide shows you how to use pw_toolchain_bazel to assemble a fully working toolchain. There are three core elements in a C/C++ toolchain in Bazel:

  1. The underlying tools used to perform compile and link actions.

  2. Flag declarations that may or may not apply to a given toolchain configuration.

  3. The final toolchain definition that binds tools and flag declarations together to produce working C/C++ compile and link commands.

This guide assumes you have a good grasp on writing Bazel build files, and also assumes you have a working understanding of what flags are typically passed to various compile and link tool invocations.

Adding Pigweed to your WORKSPACE#

Before you can use Pigweed and pw_toolchain_bazel in your project, you must register Pigweed in your //WORKSPACE file:

# Add Pigweed itself, as a submodule from `//third_party/pigweed`.
#
# TODO: b/300695111 - Support depending on Pigweed as a git_repository,
# even if you use pw_toolchain.
local_repository(
    name = "pigweed",
    path = "third_party/pigweed",
)
local_repository(
    name = "pw_toolchain",
    path = "third_party/pigweed/pw_toolchain_bazel",
)

Note

b/300695111: You must add Pigweed as a submodule to use Pigweed in a Bazel project. Pigweed does not yet work when added as a http_repository.

Configure .bazelrc#

To use this module’s toolchain rules, you must first add a couple flags that tell Bazel how to find toolchain definitions. Bazel’s .bazelrc lives at the root of your project, and is the source of truth for your project-specific build flags that control Bazel’s behavior.

# Required for new toolchain resolution API.
build --incompatible_enable_cc_toolchain_resolution

# Do not attempt to configure an autodetected (local) toolchain. We vendor
# all our toolchains, and CI VMs may not have any local toolchain to detect.
common --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1

Assemble a tool suite#

The fastest way to get started is using a toolchain tool repository template. pw_toolchain_bazel provides pre-assembled templates for clang and arm-none-eabi-gcc toolchains in the @pw_toolchain//build_external package. These build files can be attached to an external repository in your WORKSPACE file using the build_file attribute of http_archive, git_repository, or cipd_repository.

# Declare a toolchain tool suite for Linux.
http_archive(
    name = "linux_clang_toolchain",
    build_file = "@pw_toolchain//build_external:llvm_clang.BUILD",
    sha256 = "884ee67d647d77e58740c1e645649e29ae9e8a6fe87c1376be0f3a30f3cc9ab3",
    strip_prefix = "clang+llvm-17.0.6-x86_64-linux-gnu-ubuntu-22.04",
    url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-17.0.6/clang+llvm-17.0.6-x86_64-linux-gnu-ubuntu-22.04.tar.xz",
)

Create toolchain definition#

To set up a complete toolchain definition, you’ll need toolchain and pw_cc_toolchain rules that serve as the core of your toolchain. A simplified example is provided below.

load("@pw_toolchain//cc_toolchain:defs.bzl", "pw_cc_toolchain")

pw_cc_toolchain(
    name = "host_toolchain",
    action_configs = [
        "@linux_clang_toolchain//:ar",
        "@linux_clang_toolchain//:clang",
        "@linux_clang_toolchain//:clang++",
        "@linux_clang_toolchain//:lld",
        "@linux_clang_toolchain//:llvm-cov",
        "@linux_clang_toolchain//:llvm-objcopy",
        "@linux_clang_toolchain//:llvm-objdump",
        "@linux_clang_toolchain//:llvm-strip",
    ],
    cxx_builtin_include_directories = [
        "%package(@linux_clang_toolchain//)%/include/x86_64-unknown-linux-gnu/c++/v1",
        "%package(@linux_clang_toolchain//)%/include/c++/v1",
        "%package(@linux_clang_toolchain//)%/lib/clang/17/include",
    ],
    toolchain_identifier = "host-linux-toolchain",
    flag_sets = [
        "@pw_toolchain//flag_sets:c++17",
        "@pw_toolchain//flag_sets:debugging",
        "@pw_toolchain//flag_sets:no_canonical_prefixes",
    ],
)

toolchain(
    name = "host_cc_toolchain_linux",
    # This is the list of constraints that must be satisfied for the suite of
    # toolchain tools to be determined as runnable on the current machine.
    exec_compatible_with = [
        "@platforms//os:linux",
    ],
    # This is the list of constraints that dictates compatibility of the final
    # artifacts produced by this toolchain.
    target_compatible_with = [
        "@platforms//os:linux",
    ],
    toolchain = ":host_toolchain",
    toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
)

The toolchain rule#

The toolchain rule communicates to Bazel what kind of toolchains are available, what environments the tools can run on, and what environment the artifacts are intended for. A quick overview of the critical parts of this rule are outlined below.

  • name: The name of the toolchain rule. This is the label that you reference when registering a toolchain so Bazel knows it may use this toolchain.

  • toolchain_type: The language this toolchain is designed for. Today, pw_toolchain_bazel only supports C/C++ toolchains via the @bazel_tools//tools/cpp:toolchain_type type.

  • exec_compatible_with: What constraints must be satisfied for this toolchain to be compatible with the execution environment. In simpler terms, if the machine that is currently running the build is a Linux x86_64 machine, it can only use toolchains designed to run on that OS and architecture. exec_compatible_with is what prevents a Linux machine from trying to compile using tools designed for a Windows machine and vice versa.

  • target_compatible_with: What constraints must be satisfied for this toolchain to be compatible with the targeted environment. Rather than specifying whether the tools are compatible, this specifies the compatibility of the final artifacts produced by this toolchain. For example, target_compatible_with is what tells Bazel that a toolchain is building firmware for a Cortex-M4.

  • toolchain: The rule that implements the toolchain behavior. When using pw_toolchain_bazel, this points to a pw_cc_toolchain rule. Multiple toolchain rules can point to the same pw_cc_toolchain, which can be useful for creating parameterized toolchains that have a lot in common.

The pw_cc_toolchain rule#

This is the heart of your C/C++ toolchain configuration, and has two main configuration surfaces of interest.

While the other attributes of a pw_cc_toolchain are still required, their behaviors are less interesting from a configuration perspective and are required for correctness and completeness reasons. See the full API reference for pw_cc_toolchain for more information.

Register your toolchain#

Once you’ve declared a complete toolchain to your liking, you’ll need to register it in your project’s WORKSPACE file so Bazel knows it can use the new toolchain. An example for a toolchain with the name host_cc_toolchain_linux living in //toolchains/BUILD is illustrated below.

register_toolchains(
     "//toolchains:host_cc_toolchain_linux",
)

At this point, you should have a custom, working toolchain! For more extensive examples, consider taking a look at Pigweed’s fully instantiated and supported toolchains

Customize behavior with flag sets#

Now that your toolchain is working, you can customize it by introducing new flag sets.

Configure warnings#

Enabling compiler warnings and setting them to be treated as errors is a great way to prevent unintentional bugs that stem from dubious code.

load(
    "@pw_toolchain//cc_toolchain:defs.bzl",
    "pw_cc_flag_set",
)

pw_cc_flag_set(
    name = "warnings",
    actions = [
        "@pw_toolchain//actions:all_c_compiler_actions",
        "@pw_toolchain//actions:all_cpp_compiler_actions",
    ],
    flags = [
        "-Wall",
        "-Wextra",
        "-Werror",  # Make all warnings errors, except for the exemptions below.
        "-Wno-error=cpp",  # preprocessor #warning statement
        "-Wno-error=deprecated-declarations",  # [[deprecated]] attribute
    ],
)

Omit unreferenced symbols#

If a function, variable, or data section isn’t used anywhere in your binaries, it can be omitted with the following flag sets.

load(
    "@pw_toolchain//cc_toolchain:defs.bzl",
    "pw_cc_flag_set",
)

# Treats symbols representing functions and data as individual sections.
# This is mostly relevant when using `:omit_unused_sections`.
pw_cc_flag_set(
    name = "function_and_data_sections",
    actions = [
        "@pw_toolchain//actions:all_c_compiler_actions",
        "@pw_toolchain//actions:all_cpp_compiler_actions",
    ],
    flags = [
        "-ffunction-sections",
        "-fdata-sections",
    ],
)

pw_cc_flag_set(
    name = "omit_unused_sections",
    actions = ["@pw_toolchain//actions:all_link_actions"],
    # This flag is parameterized by operating system. macOS and iOS require
    # a different flag to express this concept.
    flags = select({
        "@platforms//os:macos": ["-Wl,-dead_strip"],
        "@platforms//os:ios": ["-Wl,-dead_strip"],
        "//conditions:default": ["-Wl,--gc-sections"],
    }),
)

Set global defines#

Toolchains may declare preprocessor defines that are available for all compile actions.

load(
    "["@pw_toolchain//cc_toolchain:defs.bzl"]",
    "pw_cc_flag_set",
)

# Specify global defines that should be available to all compile actions.
pw_cc_flag_set(
   name = "global_defines",
   actions = [
       "@pw_toolchain//actions:all_asm_compiler_actions",
       "@pw_toolchain//actions:all_c_compiler_actions",
       "@pw_toolchain//actions:all_cpp_compiler_actions",
   ],
   flags = [
      "-DPW_LOG_LEVEL=PW_LOG_LEVEL_INFO",  # Omit all debug logs.
   ],
)

Bind custom flags to a toolchain#

After you’ve assembled a selection of custom flag sets, you can bind them to your toolchain definition by listing them in pw_cc_toolchain.flag_sets:

pw_cc_toolchain(
    name = "host_toolchain",
    action_configs = [
        "@linux_clang_toolchain//:ar",
        "@linux_clang_toolchain//:clang",
        "@linux_clang_toolchain//:clang++",
    ...
    flag_sets = [
        "@pw_toolchain//flag_sets:c++17",
        "@pw_toolchain//flag_sets:debugging",
        "@pw_toolchain//flag_sets:no_canonical_prefixes",
        ":warnings",  # Newly added pw_cc_flag_set from above.
        ":function_and_data_sections",  # Newly added pw_cc_flag_set from above.
        ":omit_unused_sections",  # Newly added pw_cc_flag_set from above.
        ":global_defines",  # Newly added pw_cc_flag_set from above.
    ],
)

Note

Flags appear in the tool invocations in the order as they are listed in pw_cc_toolchain.flag_sets, so if you need a flag to appear earlier in the command-line invocation of the tool just move it to towards the beginning of the list.

You can use pw_cc_flag_set rules to add support for new architectures, enable/disable warnings, add preprocessor defines, enable LTO, and more.