blob: 9abdba7d966a2a3d339b60d3249bf4e36d151a1a [file] [view] [edit]
# Design: Zephyr Sysbuild Support in Zephyr-Bazel
## Goal
Implement support for Zephyr's **Sysbuild (System build)** feature in
`zephyr-bazel`. This will allow developers to define multi-image, multi-core,
and multi-SoC targets in Bazel, coordinating their configuration, compilation,
and flashing while maintaining Kconfig/DTS correctness and Bazel's lazy
evaluation.
---
## Background
### Current Multi-Image Limitation in zephyr-bazel
Currently, `zephyr-bazel` excels at building single-image Zephyr applications
(e.g., `bazel build //apps/blinky`). However, modern embedded systems often
require building and packaging **multiple related images** for a single target
board. Examples include:
* An application running alongside a bootloader (e.g., MCUboot).
* A multi-core SoC (like nRF5340) where the main application runs on the
application core and a companion image (network or audio coprocessor) runs
on a secondary core.
Currently, `zephyr-bazel` lacks a native way to define and coordinate these
multi-image builds. Developers are forced to build each image separately and
manually stitch them together, breaking the unified build graph and caching
guarantees of Bazel.
---
### Conflict with Zephyr CMake Behavior (Sysbuild)
In standard Zephyr, multi-image builds are handled by **Sysbuild**. Sysbuild is
a meta-build system that:
1. Takes a **main application** as the entry point.
2. Dynamically adds **helper applications** (domains) based on Kconfig (e.g.,
`SB_CONFIG_BOOTLOADER_MCUBOOT=y`) or CMake (`ExternalZephyrProject_Add`).
3. Builds all images for the **same board** or related cores/SoCs on the same
physical board.
4. Allows the main app to provide **configuration overlays** for helpers (e.g.,
applying `sysbuild/mcuboot.conf` in the main app's directory as an
overlay to the MCUboot helper).
5. Can generate a **merged binary** (e.g., `merged.hex`) for production.
6. Coordinates flashing and debugging.
To maintain compatibility with standard Zephyr projects and support realistic
production hardware workloads, `zephyr-bazel` must implement a comparable
meta-build capability.
---
## Alternatives Considered
We evaluated several approaches to support multi-image builds in Bazel,
balancing Starlark complexity, lockfile performance, and developer usability.
---
### Alternative A: Automatic CMake/Kconfig Parsing (Auto-Discovery)
In this approach, the Bzlmod module extension would eagerly parse Zephyr's
`sysbuild.conf` or `sysbuild.cmake` files to automatically discover what helper
images are enabled and generate the Bazel build graph dynamically.
* **Pros**:
- **Zero Duplication**: Mirrors Zephyr's configuration files directly
without requiring the user to write Bazel-specific graphs.
* **Cons**:
- **High Complexity & Fragility**: CMake is a Turing-complete language.
Parsing `sysbuild.cmake` in Python/Starlark is extremely fragile and
error-prone.
- **Bzlmod Lockfile Bloat**: Auto-discovering hundreds of potential
combinations would eagerly declare repositories in the loading phase,
causing severe lockfile bloat and analysis slowdowns.
---
### Alternative B: Union Registration + select() in BUILD.bazel
Here, the user explicitly registers the union of all possible helper images in
`MODULE.bazel`, and then uses standard Bazel `select()` statements in
`BUILD.bazel` to conditionally wire the build graph based on target board
constraints.
* **Pros**:
- **Standard Bazel Idioms**: Relies on native `select()` statements.
- **Correct Pruning**: Only the selected helpers are built.
* **Cons**:
- **High Boilerplate**: Users must write complex, verbose `select()`
statements in every `BUILD.bazel` for multi-core/multi-image targets,
creating a high barrier to entry and risking dual-source-of-truth
discrepancies.
---
### Alternative C: Declarative JSON Files
Similar to the proposed solution, but using JSON files (`sysbuild.json`) next
to the source code to define the hardware graph.
* **Pros**:
- **No AST/Regex Parsing**: Python has native JSON support.
* **Cons**:
- **Inconsistency**: Zephyr configures everything via YAML (`module.yml`,
`board.yml`, `testcase.yaml`). Using JSON would introduce a different
syntax style into the codebase.
---
## Proposed Solution: Declarative Hierarchical YAML Sysbuild
We propose adopting a **Declarative YAML-based Hierarchical** design.
Instead of writing complex CMake or verbose Bazel `select()` code, developers
describe the hardware graph declaratively in simple YAML files (`sysbuild.yml`)
next to their source code. Bazel's module extension recursively parses these
files during the loading phase to generate the complete multi-image build graph
automatically.
```mermaid
graph TD
Platform["User Platform<br>(//boards/my_board)"]
Sysbuild["zephyr_sysbuild Target<br>(my_firmware)"]
Main["Main App<br>(my_app on cpuapp)"]
Mcuboot["Mcuboot Helper<br>(on cpuapp)"]
NetApp["Net App Helper<br>(on cpunet)"]
Netboot["Netboot Nested Helper<br>(on cpunet)"]
Platform --> Sysbuild
Sysbuild -->|Transition| Main
Sysbuild -->|Transition| Mcuboot
Sysbuild -->|Transition| NetApp
NetApp -->|Recursive Transition| Netboot
```
### How It Works
1. **YAML Graph Definition**: The user defines helpers and platform
overrides in `sysbuild.yml` and `boards/<board>.sysbuild.yml`.
2. **Recursive Discovery**: The module extension scans these files recursively
to resolve nested helper chains.
3. **Path-Based Namespacing with Smart Sharing**: To support standard Zephyr
behavior where a top-level app can configure nested helpers, helper
repositories are resolved by their full dependency path. To prevent
lockfile bloat, if no configuration overrides are detected along the path,
the helper shares a single "default" repository for that board.
4. **Zero-Boilerplate Target**: The user declares a simple `zephyr_sysbuild`
target in `BUILD.bazel` that automatically resolves the entire multi-core,
multi-SoC graph at build time using transitions.
---
## Detailed Design
This section details the specific implementation details.
---
### 1. Declarative YAML Files (`sysbuild.yml`)
Instead of writing complex Bazel code, the hardware graph is described
declaratively in YAML files next to the source code, matching Zephyr's
configuration style.
#### Global Helpers (`sysbuild.yml`)
Defined in the app's root directory. These helpers are built for *all* boards,
inheriting the active platform.
```yaml
# //apps/my_app/sysbuild.yml
helpers:
mcuboot:
target: "@mcuboot//boot/zephyr:zephyr_project"
```
#### Board-Specific, Multi-Core & External Helpers
Defined in the app's `boards/<board>.sysbuild.yml` file. This allows
adding companion images running on different cores/SoCs, or board-specific
external (non-Zephyr) helper targets.
```yaml
# //apps/my_app/boards/nrf5340dk_nrf5340_cpuapp.sysbuild.yml
helpers:
net_app:
target: "//apps/net_companion"
# Explicitly transition this helper to the companion network core SoC/board:
platform: "//boards/nordic/nrf5340dk:nrf5340_cpunet"
custom_coprocessor_firmware:
target: "//third_party/custom_firmware"
# Mark as external to bypass Zephyr contextual repository parsing:
type: "external"
```
* **Multi-Core / Multi-SoC Companion Support**: Handled via the `platform`
attribute, which explicitly redirects the helper to compile for a companion
core or SoC using its specific platform target.
* **Non-Zephyr External Projects**: Declared using `type: "external"`. This
allows including custom bootloaders or companion firmware built with other
rules, which will only be compiled when building for boards that list them
in their `<board>.sysbuild.yml`.
---
### 2. Recursive Discovery & Path-Based Namespacing with Smart Sharing
During the loading phase, the `zephyr_setup` module extension recursively scans
these YAML files starting from the main application:
1. **Main App on `nrf5340_cpuapp`**: Resolves `mcuboot` (inherits
`nrf5340_cpuapp`), `net_app` (targets `nrf5340_cpunet`), and
`custom_coprocessor_firmware` (external target).
2. **Recursive Scan**: Since `net_companion` is transitioned to
`nrf5340_cpunet`, the extension scans `//apps/net_companion/sysbuild.yml`
in the context of `nrf5340_cpunet`, resolving `netboot` (inherits
`nrf5340_cpunet`).
3. **External Target Exemption**: Helpers declared with `type: "external"`
bypass the generation of namespaced contextual repositories (`@zc_...`).
Their dependency wiring is managed directly in the transition.
#### Optimization: Smart Sharing of Default Configurations
In standard Zephyr, a top-level application can override configurations for any
helper in the tree (e.g., `my_app` providing `sysbuild/netboot.conf`). This
means a helper's configuration is technically unique to its full dependency
path (e.g., `my_app -> net_companion -> netboot`).
To prevent lockfile Cartesian product explosion, the module extension applies a
**Smart Sharing** optimization during the loading phase:
1. **Trace Overlays**: For every helper in the resolved graph, the extension
scans all parent directories in its path for corresponding configuration
overlays.
* For `netboot` at `my_app -> net_companion -> netboot`, it checks for
`my_app/sysbuild/netboot.conf` and
`net_companion/sysbuild/netboot.conf`.
2. **Case A: No Overlays Detected (Default Config)**:
* If no overlay files are found anywhere in the path, the helper is
considered to be using the **default configuration** for that board.
* We **do not** declare a unique repository.
* The transition index (`@zephyr_index//:sysbuild_index.bzl`) is
configured to point this helper to the shared default repository:
`@zc_<helper_hash>_<board>`.
3. **Case B: Overlays Detected (Custom Config)**:
* If one or more overlay files are found, this helper requires a **custom
configuration**.
* We declare a unique path-specific repository using a hash of the full
dependency path: `@zc_<path_hash>_<board>`.
* The extension registers this repository eagerly, ensuring Bazel can
resolve its unique Kconfig/DTS targets.
This ensures 100% compatibility with Zephyr's hierarchical configuration while
keeping the lockfile size minimal for the common case where nested helpers are
not customized.
The complete resolved graph mapping is stored in
`@zephyr_index//:sysbuild_index.bzl`.
---
### 3. Sysbuild Configuration Parsing & Namespaced Kconfig (`sysbuild.conf`)
While `sysbuild.yml` defines the hardware graph (which images and platforms are
built), standard Zephyr projects use `sysbuild.conf` (a Kconfig file) to define
sysbuild-specific configurations and pass namespaced variables to specific
helper images.
To support this, the module extension executes a Python translation helper
during the discovery phase to parse `sysbuild.conf` and propagate settings to
the respective images, replicating Zephyr's CMake namespacing and translation
logic.
#### Namespaced Variables Support
Zephyr Sysbuild allows directing Kconfig settings to specific helper images
using the domain name as a prefix:
`-D<domain_name>_CONFIG_<OPTION>=<value>` (on command line) or
`<domain_name>_CONFIG_<OPTION>=<value>` in `sysbuild.conf`.
For example, to configure the MCUboot helper specifically, a user writes in
`sysbuild.conf`:
```kconfig
mcuboot_CONFIG_BOOT_SIGNATURE_TYPE_RSA=y
```
#### Translation and Parsing Mechanism
The Python translation helper script parses `sysbuild.conf` (and any
board-specific `sysbuild_<board>.conf` if present) during the loading phase:
1. **Extract Namespaced Settings**: It scans the file for lines matching the
namespaced pattern:
`^([a-zA-Z0-9_]+)_(CONFIG_[a-zA-Z0-9_]+)=(.*)$`
* If a match is found (e.g., `mcuboot_CONFIG_BOOT_SIGNATURE_TYPE_RSA=y`),
it extracts the target domain (`mcuboot`) and the Kconfig setting
(`CONFIG_BOOT_SIGNATURE_TYPE_RSA=y`).
* It validates that the target domain is a registered helper in the
graph.
2. **Translate Global `SB_CONFIG_` Symbols**: It parses standard `SB_CONFIG_`
symbols and translates them to target `CONFIG_` values based on Zephyr's
standard mapping rules (defined in Zephyr's
`share/sysbuild/image_configurations/`).
* **Main App Translations**: Maps `SB_CONFIG_BOOTLOADER_MCUBOOT` to
`CONFIG_BOOTLOADER_MCUBOOT`, `SB_CONFIG_MCUBOOT_MODE_*` to
`CONFIG_MCUBOOT_BOOTLOADER_MODE_*`, etc.
* **Bootloader (MCUboot) Translations**: Maps `SB_CONFIG_MCUBOOT_MODE_*`
to `CONFIG_BOOT_UPGRADE_ONLY` or similar, `SB_CONFIG_BOOT_SIGNATURE_*`
to `CONFIG_BOOT_SIGNATURE_*`, etc.
3. **Generate Kconfig Fragments**: It groups all extracted and translated
configurations by target image and writes them to temporary Kconfig
fragments (e.g., `sysbuild_generated.conf`) for each affected image.
#### Propagation to Contextual Repositories
The generated Kconfig fragments are passed to the respective contextual
repositories (`@zc_...`) via the `zephyr_state` or as explicit attributes in
`gen_zephyr_config`.
During repository rule execution, `gen_zephyr_config` includes these generated
fragments in the Kconfig merge list (as an extra overlay), ensuring that the
final compiled binaries are correctly configured according to the
`sysbuild.conf` settings.
This maintains 100% compatibility with standard Zephyr Kconfig propagation and
namespacing without requiring the execution of CMake.
---
### 4. Contextual Repository Generation (`gen_zephyr_config`)
For custom helper configurations (Case B above), the `gen_zephyr_config`
repository rule is instantiated with the full list of resolved configuration
overlays propagated by the module extension.
The repository rule uses these explicit paths to resolve Kconfig and Devicetree
overlays during execution, ensuring that:
* All parent overlays (`owner_app/sysbuild/helper.conf`) are correctly
applied.
* Top-level overlays (`top_app/sysbuild/helper.conf`) take precedence.
This replicates Zephyr's standard hierarchical configuration overlay behavior
hermetically inside the Bazel repository rule execution phase.
---
### 5. The `zephyr_sysbuild` Starlark Target (Macro + Rule)
To provide a zero-boilerplate user experience, the public API `zephyr_sysbuild`
is implemented as a **Starlark Macro** that wraps an underlying private rule
`_zephyr_sysbuild_rule`.
```python
# In //apps/my_app/BUILD.bazel
load("@zephyr-bazel//bazel:sysbuild.bzl", "zephyr_sysbuild")
zephyr_sysbuild(
name = "my_firmware",
main = "//apps/my_app",
)
```
#### The Macro Dependency Injection
Because Bazel rules cannot dynamically discover dependencies during the Analysis
Phase, the macro resolves them during the Loading Phase by loading the index
generated by the module extension:
```python
# //bazel:sysbuild.bzl (Starlark Macro API)
load("@zephyr_index//:sysbuild_index.bzl", "SYSBUILD_GRAPHS")
def zephyr_sysbuild(name, main, **kwargs):
# 1. Resolve helpers at loading time using the generated index
graph = SYSBUILD_GRAPHS.get(main, {})
helpers = graph.get("helpers", [])
# 2. Instantiate the underlying rule with resolved dependencies
_zephyr_sysbuild_rule(
name = name,
main = main,
helpers = helpers,
**kwargs
)
```
#### Platform Transition Rules (The Core Mechanism)
To support heterogeneous multi-core and multi-SoC setups where different
helpers require different target platforms, we use intermediate **Platform
Transition Targets** generated dynamically by the `zephyr_sysbuild` macro.
This avoids the Bazel limitation where a single transition on a `label_list`
attribute must transition all targets to the same platform.
##### 1. The Transition Rule
A generic Starlark rule `transitioned_dep` is defined to transition a single
dependency to a dynamically specified platform:
```python
# //bazel:private/transition_rule.bzl
def _transition_impl(settings, attr):
return {"//command_line_option:platforms": [attr.platform]}
platform_transition = transition(
implementation = _transition_impl,
inputs = [],
outputs = ["//command_line_option:platforms"],
)
def _transitioned_dep_impl(ctx):
# Forward providers from the actual target
return [ctx.attr.dep[DefaultInfo]]
transitioned_dep = rule(
implementation = _transitioned_dep_impl,
attrs = {
"dep": attr.label(cfg = platform_transition),
"platform": attr.string(mandatory = True),
},
)
```
##### 2. Macro-Level Target Generation
The `zephyr_sysbuild` macro loads the resolved graph from the index and
generates `transitioned_dep` targets for the main application and each helper.
It resolves the correct platform (shared default or custom path-specific) at
loading time:
```python
# //bazel:sysbuild.bzl (Starlark Macro API)
load("@zephyr_index//:sysbuild_index.bzl", "SYSBUILD_GRAPHS")
load("//bazel:private/transition_rule.bzl", "transitioned_dep")
def zephyr_sysbuild(name, main, **kwargs):
graph = SYSBUILD_GRAPHS.get(main, {})
helpers = graph.get("helpers", [])
main_platform = graph.get("main_platform")
transitioned_helpers = []
for i, helper in enumerate(helpers):
helper_target = helper["target"]
helper_platform = helper["resolved_platform"]
helper_name = "%s_helper_%d" % (name, i)
transitioned_dep(
name = helper_name,
dep = helper_target,
platform = helper_platform,
)
transitioned_helpers.append(":" + helper_name)
# Transition the main application
main_name = "%s_main" % name
transitioned_dep(
name = main_name,
dep = main,
platform = main_platform,
)
# Instantiate the underlying sysbuild rule with transitioned targets
_zephyr_sysbuild_rule(
name = name,
main = ":" + main_name,
helpers = transitioned_helpers,
**kwargs
)
```
This keeps the transition "pure" (only changing `--platforms`), preventing
configuration fan-out, while supporting heterogeneous multi-SoC and multi-core
hardware.
---
### 6. Invalidation & File Watching
To ensure build correctness while maintaining loading-phase performance, the
system must implement a precise invalidation strategy.
#### Loading-Phase Invalidation (Module Extension)
The `zephyr_setup` module extension must watch all configuration files that
define the sysbuild graph. To avoid redundant watches and performance
degradation:
* **Local Assets**: If the main app or a helper resides within a directory
already watched by the global `apps_dirs` or `boards_dirs` discovery (which
uses `mctx.watch_tree`), the extension **should not** add duplicate watches
for files inside these directories.
* **External Assets**: If a helper resides in an external repository (e.g.,
`@mcuboot//boot/zephyr`), the extension **must explicitly watch** its
`sysbuild.yml` and any candidate overlay files using `mctx.watch()`. This
ensures that changes to external bootloaders or companion firmware
configurations correctly invalidate the Bazel lockfile and re-generate the
graph.
* **Overlay Detection**: For the "Smart Sharing" optimization, the
extension must watch the candidate overlay paths (e.g.,
`sysbuild/<helper>.conf`). If the file does not exist, watching the path
ensures that *creating* the file triggers invalidation to transition the
helper to a custom configuration.
#### Analysis/Execution-Phase Invalidation (Repository Rules)
* The generated `@zc_...` repository rules must watch the specific resolved
overlay files passed to them by the extension using `rctx.watch()`.
* They must also watch the app's `sysbuild/` directory to detect configuration
changes.
---
## User Documentation Required
The user documentation must be updated to add a user facing description of the
supported sysbuild features and how they are used. It should enumerate the
differences between the bazel and cmake sysbuild. It must also include this
warning about toolchains:
> [!IMPORTANT]
> **Heterogeneous Toolchains**: Building for multi-SoC setups (e.g., ARM main
> core + RISC-V companion) requires that the workspace contains registered
> toolchains for both architectures. Bazel will automatically select the
> correct toolchain for each core based on the platform constraints, but both
> toolchains must be configured in the workspace.
---
## Verification Plan
### Automated Tests
We will verify this implementation by adding integration tests in the
`examples/` folder:
* **Example App**: Add a new example app named `sysbuild_app` in the
`examples/` directory.
* **Multi-Board Configuration**: Configure `sysbuild_app` with two different
boards (e.g., `board_single_core` and `board_multi_core`) that define
different child helper configurations in their `<board>.sysbuild.yml`
files.
* `board_single_core` will only enable `mcuboot`.
* `board_multi_core` will enable `mcuboot` + a companion `net_app` (which
recursively enables `netboot` on the companion core).
* **CI/CD Integration**: Add the build commands for both board configurations
to `examples/workflows.json` under the `"builds"` block. This ensures that
running `./pw default` will build both multi-image graphs automatically
during local testing and CI.
* **Output Verification**: Verify that:
* All target binaries (`sysbuild_app`, `mcuboot`, `net_app`, `netboot`)
are compiled successfully for their respective platforms.
* Kconfig overlays from the owner apps are correctly applied.
* A merged binary (`merged.hex`) is generated for both board
configurations.
* **Invalidation & Watching Tests**: We will add automated unit tests to
verify the invalidation logic:
1. **Local Change Invalidation**: Verify that modifying a local
`sysbuild.yml` or adding a `sysbuild/<helper>.conf` file triggers a
re-run of the module extension and updates the build graph.
2. **External Change Invalidation**: Verify that modifying a
`sysbuild.yml` in an external repository (mocked in tests) correctly
invalidates the extension.
3. **No-Op Stability**: Verify that building when no files have changed
does *not* trigger re-running of the extension or repository rules
(zero-operational overhead).
---
## Implementation Plan
This plan breaks down the implementation into progressive phases, each with
clear verification steps to ensure correctness before proceeding.
### Phase 1: YAML Discovery & Graph Resolution (Loading Phase)
* **Goal**: Implement `sysbuild.yml` recursive scanning in Bzlmod extension.
* **Tasks**:
* Update `setup.bzl` (`zephyr_setup` extension) to scan `sysbuild.yml`
and `boards/<board>.sysbuild.yml` using `PyYAML`.
* Implement topological sorting of helpers.
* Implement the "Smart Sharing" optimization logic (sharing default
repos vs. declaring custom path-specific repos).
* Generate `SYSBUILD_GRAPHS` index in `sysbuild_index.bzl`.
* **Verification / Tests**:
* **Unit Tests**: Add Python unit tests for YAML parsing and graph
resolution logic (mocking the filesystem).
* **Integration Test**: Verify that running `bazel query` on a test app
with nested and companion helpers generates the expected graph in
`sysbuild_index.bzl`.
### Phase 2: `sysbuild.conf` Parsing & Kconfig Translation
* **Goal**: Parse `sysbuild.conf` and translate `SB_CONFIG_` to `CONFIG_`.
* **Tasks**:
* Implement `sysbuild.conf` parsing using `kconfiglib` in the extension.
* Implement Python translation script that maps `SB_CONFIG_` to `CONFIG_`
for the main app and `mcuboot` (replicating Zephyr's CMake logic).
* Generate translated Kconfig fragments (`sysbuild_generated.conf`).
* Update `gen_zephyr_config` repository rule to accept these fragments
and append them as Kconfig overlays.
* **Verification / Tests**:
* **Unit Tests**: Add Python unit tests for the translation logic
(verify `SB_CONFIG_BOOTLOADER_MCUBOOT=y` translates correctly for both
main app and bootloader).
* **Integration Test**: Verify that the generated `autoconf.h` in the
`@zc_` repository contains the translated `CONFIG_` symbols.
### Phase 3: Starlark Rules and Transitions
* **Goal**: Implement transitioned dependencies and `zephyr_sysbuild` target.
* **Tasks**:
* Implement `transitioned_dep` rule in
`bazel/private/transition_rule.bzl`.
* Implement `zephyr_sysbuild` macro in `bazel/sysbuild.bzl` to generate
`transitioned_dep` targets and instantiate `_zephyr_sysbuild_rule`.
* Implement `_zephyr_sysbuild_rule` to depend on transitioned targets
and coordinate packaging (generating merged binaries if needed).
* **Verification / Tests**:
* **Integration Test**: Create a test `zephyr_sysbuild` target and verify
that building it triggers compilation of both main app and helpers
for their respective platforms. Use `bazel query` to inspect the
analyzed configuration.
### Phase 4: Invalidation and Watching
* **Goal**: Implement robust invalidation for configuration changes.
* **Tasks**:
* Implement directory-level watches (`mctx.watch_tree` / `mctx.watch`)
on the `sysbuild/` directory in the Bzlmod extension.
* **Verification / Tests**:
* **Manual / Automated Invalidation Test**:
1. Build a sysbuild target.
2. Modify `sysbuild.yml` (e.g., add a helper) -> Verify Bazel detects
the change and compiles the new helper.
3. Modify `sysbuild.conf` (e.g., change MCUboot mode) -> Verify Bazel
recompiles the helper with the new configuration.
### Phase 5: User Documentation Update
* **Goal**: Update the User Guide to document Sysbuild support.
* **Tasks**:
* Update [user_guide.md](user_guide.md) to document Sysbuild features.
* Explain `sysbuild.yml` and `sysbuild.conf` usage.
* Enumerate differences between Bazel Sysbuild and CMake Sysbuild.
* Include the **Heterogeneous Toolchains** warning.
* **Verification / Tests**:
* Verify that the document renders correctly and adheres to the 80-char
line limit.