The goal of the Zephyr-Bazel integration is to provide a seamless developer experience that mirrors the flexibility of the native Zephyr build system while leveraging Bazel's correctness and speed. Ideally, a user should be able to add a new application or a new board to the workspace, and Bazel should automatically discover the new combinations without requiring manual registration in MODULE.bazel or other configuration files. Developers expect to build any app for any board using a simple command like bazel build //apps/blinky --platforms=//boards/my_board.
While the current design achieves this automatic discovery by eagerly declaring repositories for the Cartesian product of all apps and boards in a module extension, it hits a hard scaling limit due to Bazel's Bzlmod lockfile implementation:
MODULE.bazel.lock file.To overcome these limitations, we need a mechanism to prune the number of declared repositories during the loading phase while maintaining as much of the automatic discovery experience as possible.
We considered moving from a repository per board-app combination to a repository per app model. Two variants were explored:
select() behavior on Kconfig symbols. Losing select() behavior cannot be done since this is the core methodology for correctly generating the build for all the Zephyr kernel, driver, and subsystem code based on the configuration.This option is even worse than the current design. It would require a single repository to handle all applications and boards. This would either force eager evaluation of the entire Cartesian product of apps and boards (which is impossible at scale) or require complex custom rules that still cannot overcome Bazel's limitations regarding dynamic target generation.
Because we cannot sacrifice lazy evaluation of the matrix, and we cannot lose the ability to use Kconfig symbols in Bazel select() statements, the existing system of one repository per combination is required.
Since the repository-per-combination structure is required, the only way to reduce the lockfile size is to limit the number of combinations declared in the loading phase. We reviewed several methods for down-selecting boards:
MODULE.bazel.ZEPHYR_BOARDS) to filter boards.boards/ directory and only generates repositories for boards that have specific configuration files or overlays.The proposed solution adopts a stratified approach to reducing the number of declared repositories. It applies different heuristic rules based on the origin of the assets (In-Tree vs. Out-of-Tree) and the presence of Zephyr metadata files, while maintaining a fallback mechanism.
Instead of generating repositories for the full Cartesian product, the module extension applies the following priority-based rules for each (app, board) combination:
testcase.yaml or sample.yaml file, the extension parses it. To prevent lockfile Cartesian product explosion, it only generates repositories for boards that are explicitly listed in platform_allow. If a test relies on broad dynamic constraints (like arch_allow, filter, min_flash) or exclusion (platform_exclude), the extension ignores Rule 2 and drops down to Rule 3 (Filesystem heuristics).boards/ directory. It only generates repositories for boards that have specific config files (e.g., <board>.conf or <board>.overlay) or specific revision overrides in that folder.MODULE.bazel or an environment variable. Repositories are generated for these boards for all apps.testcase.yaml prevents generating thousands of useless test/board combinations.select() and Lazy Execution: Keeps the repository-per- combination model.The discovery pruning logic will be implemented inside _zephyr_setup_core_impl in setup.bzl. This function is responsible for scanning application directories, applying heuristics validation rules filtering, and setting up configurations Cartesian survival indexes.
(app, board) pair, if the app is located in an out-of-tree directory, and the board is also an OOT board, the extension will unconditionally generate the configuration repository. In mixed cases (where one of the two is an in-tree asset), the combination is subject to the heuristics metadata filtering rules.An app is considered Out-of-Tree if it originates from a directory explicitly listed in apps_dirs. A board is considered Out-of-Tree if it originates from a directory explicitly listed in boards_dirs. The in-tree boards folder inside zephyr_root is automatically scanned and considered in-tree.
testcase.yaml or sample.yaml in the app's root. To parse these files, it will invoke a small Python helper script. This script logic is run from inside the zephyr_setup module extension where the path to @zephyr has already been resolved and dynamically supplied in @zephyr_state//:state.json.To safely resolve the path to the helper script inside the module extension in setup.bzl, the extension should use script_path = mctx.path(Label("@zephyr- bazel//scripts/build:parse_test_metadata.py")).
[!IMPORTANT] > To prevent Cartesian product scaling and analysis loading phase performance degradation, the helper script must > process all applications at once. It receives a JSON mapping of application package names to their absolute paths > via a
--apps-jsonargument and returns a resolved JSON mapping of valid board combinations.
The Python helper script dynamically integrates @zephyr//scripts/pylib/twister into its sys.path runtime Python imports environment. If dynamic twister dependencies (like ruamel.yaml) are missing from the Python runtime environment, the script should fall back to a lightweight, internal YAML parsing logic for platform_allow statements to be robust during the Bazel loading phase.
The script evaluates the YAML file and returns the union of all boards explicitly allowed under platform_allow. If the file contains broad dynamic constraints (like arch_allow), the script returns an empty set, directing the extension to skip Rule 2 and drop down to Rule 3 (boards/ scan heuristics).
[!NOTE] > Returning a massive list of matching boards dynamically constraint scenarios (such as all
arch_allow: arm> boards) would trigger lockfile explosions again, which violates the optimization goal of the discovery > pruning structure. Dropping down to Rule 3 acts as validation fallback rules.
The output forms a JSON structure which Starlark decodes using json.decode().
The script must only write the final JSON array to stdout. All other logging, warnings, or debugging print statements must be routed to sys.stderr to ensure that Starlark's json.decode(res.stdout) behaves robustly.
scripts/build/discovery_utils.py to search for overrides. A repository is declared if <board>.conf, <board>.overlay, or specific revision overrides exist in the app's boards/ directory.If a board has qualified names (e.g. board/qualifiers), candidate files should follow the syntax format <board_name>_<qualifiers>.conf. This heuristics logic should be extracted and shared from kconfig_gen_values.py:268-274.
MODULE.bazel (e.g., manual_boards = ["nrf52840dk_nrf52840"]) will be unconditionally combined with any discovered lists, acting as a fallback.To support apps that rely on the default configuration without custom overlays, users can explicitly allow list boards using the manual_boards parameter in their MODULE.bazel.
MODULE.bazelzephyr_setup.env( apps_dirs = ["//apps"], boards_dirs = ["//boards"], manual_boards = [ "nrf52840dk_nrf52840", "same70q21b", ], )
setup.bzlmanual_boards = attr.string_list() to the _env tag class attributes in setup.bzl._zephyr_setup_core_impl, we iterate over tags and extend a workspace manual_boards list.manual_boards to the JSON written to @zephyr_state//:state.json._zephyr_setup_apps_impl, the extension will pull in manual_boards from the state and ensure repositories are always generated for combinations involving these boards for all apps in the discovery loop.The implementation will proceed in the following stages:
setup.bzlmanual_boards = attr.string_list() to the _env tag class attributes._zephyr_setup_core_impl, aggregate manual boards lists, add it to the state_data dictionary mapping, and assign it to @zephyr_state//:state.json.setup.bzl, scripts/build/parse_test_metadata.py [NEW], scripts/build/discovery_utils.py [NEW]--apps-json and --zephyr-root.sys.path.insert(0, args.zephyr_root + "/scripts/pylib/twister").kconfig_gen_values.py:268-274._zephyr_setup_core_impl: Inject combinations pruning heuristics rules setup after scanning apps and boards.setup.bzl_zephyr_setup_core_impl: Filter surviving combinations and store valid combinations in state_data as a dictionary mapping norm_app_pkg: [supported_board_names]._zephyr_setup_apps_impl: Ensure the extension relies on this dictionary inside @zephyr_state and only generates configuration repositories for combinations that survived pruning._zephyr_setup_core_impl:594: Update Cartesian index builder generator rules for @zephyr_index. Only generate indices mapping combinations that survived heuristics pruning logic validation set.