User Guide

Introduction

The zephyr-bazel project provides a Bazel overlay for the Zephyr RTOS. It allows you to build Zephyr applications using Bazel while maintaining the familiar concepts of applications, boards, Kconfig, and Devicetree. This guide assumes you are already familiar with these Zephyr concepts. If you are new to Zephyr, please refer to the official Zephyr documentation first.

In this setup, Zephyr applications are defined as Bazel targets, and boards are represented as Bazel platforms. Kconfig and Devicetree configurations are resolved contextually for each application and board combination, similar to how the standard Zephyr build system works, but fully integrated into the Bazel build graph.

Zephyr Apps

In Zephyr-Bazel, applications are defined as Bazel targets, but they maintain the standard Zephyr structure and configuration files you are familiar with.

Registering Applications in MODULE.bazel

To make the build system aware of your applications, you must register all directories containing application folders in your project's MODULE.bazel file. This includes custom application directories as well as standard Zephyr directories (like the Zephyr samples directory) if you wish to build them.

This is done using the zephyr_setup module extension. You provide a list of directory paths to the apps_dirs parameter:

zephyr_setup = use_extension("@zephyr-bazel//:setup.bzl", "zephyr_setup")
zephyr_setup.env(
    apps_dirs = [
        "//apps",
        "@zephyr//samples", # Required to build Zephyr samples
    ],
)

This tells Bazel to scan these directories for valid Zephyr applications.

For a directory to be passed as a label to apps_dirs, it must be a valid Bazel package. This means the directory must contain a BUILD or BUILD.bazel file. Within that directory, the discovery system looks for standard Zephyr application indicators like a prj.conf file.

Here is an example of a simple project structure:

my_project/
├── MODULE.bazel
├── apps/
   ├── BUILD.bazel
   ├── my_app1/
      ├── BUILD.bazel
      ├── prj.conf
      └── main.cc
   └── my_app2/
       ├── BUILD.bazel
       ├── prj.conf
       └── main.cc
└── src/
    └── (optional shared source files)

Defining a Zephyr Application

Inside your application's directory, you define the build target using a zephyr_app macro in a BUILD.bazel file (similar to a CMakeLists.txt file in standard Zephyr).

Here is what creating a Zephyr app in Bazel does for you automatically:

  • Header Injection: It ensures that the generated autoconf.h is automatically included in your source files, so Kconfig symbols are available.
  • Devicetree Integration: It handles mapping the generated Devicetree headers to your build targets.
  • Configuration Resolution: It sets up the necessary “transitions” (a Bazel concept) to allow building the same app for different boards.

Example BUILD.bazel

Here is an example of how to define a Zephyr application in a BUILD.bazel file:

load("@zephyr//:cc.bzl", "zephyr_app", "zephyr_cc_library")

zephyr_app(
    name = "hello_bazel",
    deps = [
        ":app_main",
    ],
)

zephyr_cc_library(
    name = "app_main",
    srcs = ["hello_bazel.cc"],
)

Multi-Board Support and Configuration

A key feature of this setup is that a single Zephyr application target can be built against multiple different boards (platforms). You do not need to create separate build targets for each board.

When you build an application, Bazel will look for configuration files just like standard Zephyr does:

  • It reads the base prj.conf file in the application root.
  • It looks for board-specific configurations in a boards/ subdirectory (e.g., boards/<board_name>.conf).
  • It applies Devicetree overlays found in the boards/ directory (e.g., boards/<board_name>.overlay).

The resolution of Kconfig symbols and Devicetree overlays works the same way as in the standard Zephyr build system, ensuring compatibility with existing Zephyr projects.

Zephyr Boards

In Zephyr-Bazel, hardware boards are represented as Bazel platforms. A platform defines the constraints of the target hardware (like CPU architecture) and tells Bazel which Zephyr board to use for building.

Mapping Platforms to Boards

When you build an application, you specify the target board using the --platforms flag:

bazelisk build //apps/my_app1 --platforms=//boards/my_custom_board

Bazel maps this platform label to a Zephyr board using a set of resolution rules based on the package path and target name.

Registering Boards in MODULE.bazel

By default, the build system automatically includes all in-tree boards provided by the Zephyr repository. If you have Out-of-Tree (OOT) boards, you must register their directories in your MODULE.bazel file using the boards_dirs parameter:

zephyr_setup.env(
    apps_dirs = ["//apps"],
    boards_dirs = ["//boards"], # Register OOT boards directory
)

Board-App Combinations (Discovery)

To prevent generating a massive matrix of build targets for every possible combination of application and board, the system uses heuristics to determine which combinations are valid:

  1. Out-of-Tree Exemption: If both the application and the board are Out-of-Tree, the combination is automatically enabled.
  2. Heuristics for In-Tree Assets: For combinations involving in-tree boards or apps, the system looks for specific configuration files in the application's boards/ directory (e.g., boards/<board_name>.conf or boards/<board_name>.overlay).
    • Adding support for a specific board: If you need to build an application for a specific in-tree board, you can simply add a corresponding .conf file for that board in the app's boards/ directory.
  3. Manual Fallback: If you want a board to be available for all applications regardless of whether specific configuration files exist, you can add it to the manual_boards list in MODULE.bazel:
zephyr_setup.env(
    apps_dirs = ["//apps"],
    manual_boards = ["nrf52840dk/nrf52840"], # Available for all apps
)

Boards, SoCs, and Modifiers

Zephyr's Hardware Model v2 (HWM v2) supports boards with multiple SoCs and qualifiers (modifiers). The Bazel build system handles these by matching the target name in the platform label to the Zephyr board ID or SoC name.

Here are the resolution rules used to map a Bazel platform to a Zephyr board:

  1. Explicit Match: The target name matches the Zephyr board ID.
  2. SoC/Variant Match: The target name matches a specific SoC variant of a board. For example, a target :nrf52840 in the //boards/nordic/nrf52840dk package matches the board/SoC combination nrf52840dk/nrf52840.
  3. Default Shortcut: If the target name is :default and the directory contains exactly one board, that board is selected.

Examples

Defining a Simple Board

Here is how you might define a platform for a simple board in a BUILD.bazel file inside your board's directory (e.g., //boards/my_board):

platform(
    name = "my_board",
    constraint_values = [
        "@platforms//cpu:armv7m",
        "@zephyr//constraints/arm:cortex-m4",
    ],
)

You would build for this board using: --platforms=//boards/my_board

Handling SoCs and Variants

For a multi-core board like nrf5340dk, you can define platforms for both the application and network cores by matching the target name to the Zephyr SoC name:

# In //boards/nordic/nrf5340dk/BUILD.bazel

# Application core platform
platform(
    name = "nrf5340_cpuapp",
    constraint_values = [
        "@platforms//cpu:armv8-m",
        "@zephyr//constraints/arm:cortex-m33",
    ],
)

# Network core platform
platform(
    name = "nrf5340_cpunet",
    constraint_values = [
        "@platforms//cpu:armv8-m",
        "@zephyr//constraints/arm:cortex-m33",
    ],
)

Referencing //boards/nordic/nrf5340dk:nrf5340_cpuapp will resolve to the nrf5340dk/nrf5340_cpuapp board in Zephyr, and similarly for the network core.

Zephyr Modules

Zephyr modules provide additional source code, drivers, and libraries that can be used by your applications.

Including Zephyr Modules

Zephyr modules are discovered by the build system similarly to apps and boards. You can include modules in two ways:

  1. Bzlmod Dependencies: If a module is available as a Bazel module (via bazel_dep), the zephyr_setup extension will automatically attempt to discover it if it contains a zephyr/module.yml file.
  2. Manual Modules: You can explicitly list modules in the modules attribute of zephyr_setup.env in MODULE.bazel:
zephyr_setup.env(
    apps_dirs = ["//apps"],
    modules = [
        "@my_module//:zephyr/module.yml",
    ],
)

Bazel Support Requirement

For a Zephyr module to work in this system, it must have Bazel support. This means it needs to contain BUILD.bazel files defining its libraries (either provided natively by the module or applied via a Bazel overlay).

Important: Exporting zephyr/module.yml

For the automatic module discovery from Bzlmod dependencies to work, the module's repository must export the zephyr/module.yml file. If the file is not visible to external repositories, the discovery scan will not be able to read it, and the module will not be detected.

In the module's BUILD.bazel file (at the root or where module.yml resides), you should ensure it is exported:

exports_files(["zephyr/module.yml"])

Sysbuild

zephyr-bazel supports Zephyr's Sysbuild feature for multi-image, multi-core, and multi-SoC targets.

Defining a Sysbuild Target

You define a sysbuild target using the zephyr_sysbuild macro in a BUILD.bazel file:

load("@zephyr-bazel//:bazel/sysbuild.bzl", "zephyr_sysbuild")

zephyr_sysbuild(
    name = "my_firmware",
    main = "//apps/my_app",
)

Configuration

  • Hardware Graph: Described in sysbuild.yml in the app directory and boards/<board>.sysbuild.yml for board-specific helpers.
  • Kconfig: Handled via sysbuild.conf in the app directory. Settings are translated to target images.

Unsupported Zephyr Features

While zephyr-bazel attempts to mirror the flexibility of the standard Zephyr build system, some features are unsupported or work differently due to Bazel's architecture:

  1. Full Twister Compatibility: Native Twister can evaluate complex dynamic constraints in testcase.yaml (like arch_allow: arm). In this system, complex filters are skipped during the loading phase to avoid lockfile explosions, falling back to filesystem heuristics (checking for <board>.conf).
  2. Interactive Kconfig (menuconfig): The system generates Kconfig configurations statically. Interactive tools like menuconfig or guiconfig are not directly supported for generating prj.conf updates within the Bazel workflow.
  3. Standard west Workspace Structure: Since Bazel fetches dependencies into its own cache, there is no standard west workspace directory structure. Tools that expect a specific relative path layout between Zephyr and modules may need adaptation.
  4. Implicit Default Combinations: You cannot build an app for an in-tree board that relies purely on defaults without adding at least an empty <board>.conf file or listing it in manual_boards, due to the discovery pruning logic.