blob: ea45e2c9e1550eaae2ae9a51599b9fde3181fab5 [file]
.. _blog-08-bazel-docgen:
===============================================
Pigweed Blog #8: Migrating pigweed.dev to Bazel
===============================================
*Published on 2025-02-03 by Kayce Basques*
``pigweed.dev`` is now built with Bazel! This blog post covers:
* :ref:`blog-08-bazel-docgen-why`
* :ref:`blog-08-bazel-docgen-compare`
* :ref:`blog-08-bazel-docgen-good`
* :ref:`blog-08-bazel-docgen-challenges`
* :ref:`blog-08-bazel-docgen-next`
.. important::
Pigweed still supports GN and will continue to do so for a long time. This
post merely discusses how we migrated the build for our own docs site
(``pigweed.dev``) from GN to Bazel.
.. _blog-08-bazel-docgen-why:
---------------------------------
Why pigweed.dev switched to Bazel
---------------------------------
.. _hermetic: https://bazel.build/basics/hermeticity
Back in October 2023, Pigweed adopted Bazel as its :ref:`primary build system
<seed-0111>`. For the first 12 months or so, the team focused on critical
embedded development features like automated toolchain setup and `hermetic`_
cross-platform builds. Most of that work is done now so we shifted our focus
to the last major part of Pigweed not using Bazel: our docs.
.. note::
"Hermeticity" essentially means that the build system runs in an isolated,
reproducible environment to guarantee that the build always produces the
exact same outputs for all teammates. It's one of Bazel's key value
propositions.
.. _blog-08-bazel-docgen-compare:
-------------------------------------------------
Comparing the new Bazel build to the old GN build
-------------------------------------------------
We eventually ended up with this architecture for the new Bazel-based docs
build system:
.. mermaid::
flowchart LR
Doxygen --> Breathe
Breathe --> reST
reST --> Sphinx
Rust --> Sphinx
Python --> Sphinx
The GN build had roughly the same architecture, but the architecture is
much more explicit and well-defined now.
Here's an overview of what each component does and how they connect together:
* C/C++ API references. We auto-generate our C/C++ API references with
**Doxygen** and then use **Breathe** to insert the reference content
into our main docs files, which are authored in **reST** (reStructuredText).
Example: :ref:`pw_unit_test C++ API reference <module-pw_unit_test-cpp>`
* Rust API references. We auto-generate our Rust API references with
``rustdoc`` and upload these docs to their own subsite.
Example: `Crate pw_bytes <https://pigweed.dev/rustdoc/pw_bytes/>`_
* reST and Sphinx. We author most of our docs in **reST** (reStructuredText)
and convert them into HTML with **Sphinx**. This is the heart of our
docs system. Example: :ref:`Tour of Pigweed <showcase-sense-tutorial-intro>`
* Python API references. We auto-generate our Python API references with
Sphinx's ``autodoc`` feature. Example: :ref:`pw_rpc Python client API <module-pw_rpc-py>`
The next few sections provide more detail about how we used to run some of
these components in GN and how we now run them in Bazel. (The Python API
references component is skipped.)
.. note::
I personally did most of the migration. I'm a technical writer. This
migration was my first time working with Bazel in-depth and was the largest
software engineering project I've ever done. My Pigweed teammates Alexei
Frolov, Ted Pudlik, Dave Roth, and Rob Mohr provided a lot of help and
guidance. Check out :bug:`318892911` for a granular breakdown of all the
work that was done.
.. _blog-08-bazel-docgen-compare-doxygen:
Generating C/C++ API references with Doxygen
============================================
In the GN build we needed a custom script to run Doxygen.
The script manually cleaned output directories, calculated the
absolute paths to all the headers that Doxygen should parse, and
then ran Doxygen non-hermetically. I.e. Doxygen had access to
all files in the Pigweed repository rather than only the ones it
actually needed.
In the Bazel build all of our Doxygen logic resides within the ``MODULE.bazel``
file at the root of our repo and the ``BUILD.bazel`` files distributed
throughout the codebase. We use `rules_doxygen
<https://github.com/TendTo/rules_doxygen>`_ to hermetically run Doxygen.
We just provide ``rules_doxygen`` with a Doxygen executable, tell it
what headers to process, and it handles the rest.
We chose ``rules_doxygen`` because it's actively maintained and supports `Bazel
modules <https://bazel.build/external/module>`_ (the future of external
dependency management in Bazel). Initially the repo was missing support for
hermetic builds and macOS (Apple Silicon). I worked with the repo owner,
`Ernesto Casablanca <https://github.com/TendTo>`_, to get these features
implemented. It was one of my first proper engineering collaborations on an
open source project and it was a really rewarding experience. Thank you,
Ernesto!
.. _blog-08-bazel-docgen-compare-rust:
Generating Rust API references with rustdoc
===========================================
In the GN build there is no equivalent to this step. We have always generated
our Rust API references through Bazel. We use `rules_rust
<https://github.com/bazelbuild/rules_rust>`_ to run ``rustdoc`` from within
Bazel. Previously our docs builder would generate the Rust API references with
Bazel, then use GN to build the rest of the docs, then upload the two
disconnected outputs to production. Now, the docs builder just runs a single
Bazel command and everything is generated together. Long-term, this will
probably make it easier to integrate the Rust docs more thoroughly with the
rest of the site.
.. _blog-08-bazel-docgen-compare-sphinx:
Building the reStructuredText docs
==================================
.. inclusive-language: disable
.. _Sphinx: https://www.sphinx-doc.org/en/master/
.. inclusive-language: enable
This is the heart of our docs system. We author our docs in `reStructuredText
<https://docutils.sourceforge.io/rst.html>`_ (reST) and transform them
into HTML with `Sphinx`_. We currently have around 440 reST files
distributed throughout the Pigweed codebase.
In the GN build, we basically had to implement all core docs workflows
with our own custom scripts. E.g. we had a custom script for building
the docs with Sphinx, another for locally previewing the docs, etc.
We also had a lot of custom code for gathering up the reStructuredText
files distributed throughout the codebase and reorganizing them into a
structure that's easy for Sphinx to process.
In the Bazel build, we no longer need any of this custom code.
`rules_python <https://rules-python.readthedocs.io/en/latest/>`_
provides almost all of our core docs workflows now.
See :ref:`blog-08-bazel-docgen-good-sources` and
:ref:`blog-08-bazel-docgen-good-rules_python` for more details.
.. _blog-08-bazel-docgen-compare-verify:
Verifying the outputs
=====================
Our goal was to switch from GN to Bazel without ``pigweed.dev`` readers
noticing any change. With over 440 pages of documentation, it was infeasible to
manually verify that the Bazel build was producing the same outputs as the GN
build. I ended up automating the verification workflow like this:
#. Build the docs with the old GN-based system.
#. Build them again with the new Bazel-based system.
#. Traverse the output that GN produced and check that Bazel has produced
the exact same set of HTML files.
#. Read each HTML file produced by GN as a string, then read the equivalent
HTML file produced by Bazel as a string, then compare the strings to verify
that their contents match exactly.
#. When they're not equal, use ``diff`` to manually pinpoint mismatches.
For final verification, I set up a visual diffing workflow:
#. Use `Playwright <https://playwright.dev/python/>`_ to take screenshots of
each GN-generated HTML file and its Bazel-built equivalent.
#. Visually diff the screenshots with `pixelmatch-py
<https://github.com/whtsky/pixelmatch-py>`_.
.. _blog-08-bazel-docgen-good:
--------------
What went well
--------------
We kicked off the migration project in mid-September 2024 and started using
Bazel in production in mid-January 2025. If we were in a rush, we probably
could have finished in 2 months. When you add up the work I did as well as the
help I got from others, it was about 120 hours of work. I.e. one full-time
employee working 15 full days. We expected this project to drag on for much
longer.
.. _blog-08-bazel-docgen-good-sources:
Built-in support for reorganizing sources
=========================================
Our docs are stored alongside the rest of Pigweed's code in a single
repository. To make it easier to keep the docs in-sync with code changes, each
doc lives close to its related code, like this:
.. code-block:: text
.
├── a
│   ├── a.cpp
│   └── a.rst
├── b
│   ├── b.cpp
│   └── b.rst
└── docs
├── conf.py
└── index.rst
Sphinx, however, is easiest to work with when you have a structure
like this:
.. code-block:: text
.
├── a
│   └── a.cpp
├── b
│   └── b.cpp
└── docs
├── a
│   └── a.rst
├── b
│   └── b.rst
├── conf.py
└── index.rst
By default, Sphinx considers the directory containing ``conf.py`` to
be the root docs directory. All ``*.rst`` (reST) files should be at or
below the root docs directory.
In the old GN-based system we had to hack together this reorganization
logic ourselves. Bazel has built-in support for source reorganization via
its ``prefix`` and ``strip_prefix`` features.
.. _blog-08-bazel-docgen-good-rules_python:
rules_python did the heavy lifting
==================================
We now get almost all of :ref:`our core docs workflows <contrib-docs-build>`
for free from ``rules_python``, thanks to the great work that `Richard
Levasseur <https://github.com/rickeylev>`_ has been doing. In this regard the
switch to Bazel has significantly reduced complexity in the Pigweed codebase
because our docs system now needs much less custom code.
.. _blog-08-bazel-docgen-good-speed:
Faster cold start builds
========================
Currently, building the docs from scratch in Bazel is about 27% faster than
building them from scratch in GN. However, there's still one major docs feature
being migrated over to Bazel so it's not an apples-to-apples comparison yet.
.. _blog-08-bazel-docgen-challenges:
----------
Challenges
----------
Overall the migration was a success, but I did get some scars!
.. _blog-08-bazel-docgen-challenges-incremental:
Incremental builds (or lack thereof)
====================================
Incremental builds aren't working. You change one line in one reStructuredText
file, and it takes 30-60 seconds to regenerate the docs. Unacceptable! Bazel
and Sphinx both separately support incremental builds, so we're hopeful that
we can find a path forward without opening a huge can of worms.
.. _blog-08-bazel-docgen-challenges-skylib:
Core utilities were hard to find
================================
At one point I needed to copy a directory containing generated outputs. I
searched the Bazel docs, but couldn't find a built-in mechanism for this basic
task, so I created a `genrule
<https://bazel.build/reference/be/general#genrule>`_. During code review, I
learned that there is indeed a core utility for this: `copy_directory
<https://github.com/bazelbuild/bazel-skylib/blob/main/rules/copy_directory.bzl>`_.
I was quite surprised that ``copy_directory`` is not mentioned in the official
Bazel docs.
.. _blog-08-bazel-docgen-challenges-deps:
Dependency hell
===============
Pigweed's CI/CD testing is rigorous. Before new code is allowed to merge into
Pigweed, all of Pigweed is built and tested in 10-100 different environments
(the exact number depends on what code you've touched). There's a check that
builds Pigweed with Bazel on macOS (Apple Silicon), another one that builds
Pigweed with GN on Windows (x86), and so on. We also have a bunch of
integration tests to ensure that changes to Pigweed don't break our customers'
builds or unit tests.
The :ref:`rules_python <blog-08-bazel-docgen-good-rules_python>` features that
we rely on were introduced in a fairly new version of the module, v0.36. When
I upgraded Pigweed to v0.36, I saw the dreaded red wall of integration test
results. In other words, upgrading to ``rules_python`` v0.36 would break the
builds for many Pigweed customers. The only path forward was to independently
upgrade each customer's codebase to support v0.36. My Pigweed teammate, Dave
Roth, saved the day by doing exactly that. Thank you, Dave, for helping me
escape `dependency hell <https://en.wikipedia.org/wiki/Dependency_hell>`_!
.. _blog-08-bazel-docgen-challenges-graphs:
Explicit build graphs were time consuming
=========================================
Like the rest of Pigweed's codebase, I opted to explicitly list all
sources and dependencies in the docs build rules, like this:
.. code-block:: py
sphinx_docs_library(
name = "docs",
srcs = [
"api.rst",
"code_size.rst",
"design.rst",
"docs.rst",
"guide.rst",
],
# …
)
For the initial prototyping, using globs would have been much
faster:
.. code-block:: py
sphinx_docs_library(
name = "docs",
srcs = glob([
"*.rst",
]),
# …
)
.. _blog-08-bazel-docgen-challenges-starlark:
Uncanny valley experiences with Starlark
========================================
.. _Starlark: https://github.com/bazelbuild/starlark?tab=readme-ov-file#starlark
.. _dialect: https://en.wikipedia.org/wiki/Programming_language#Dialects,_flavors_and_implementations
`Starlark`_ naturally looks and feels a lot like Python, since it's a `dialect`_
of Python. During the migration I had a few `uncanny valley
<https://en.wikipedia.org/wiki/Uncanny_valley>`_ experiences where I expected
some Python idiom to work, and then eventually figured out that Starlark
doesn't allow it. For example, to build out a dict in Python, I sometimes
use code like this:
.. code-block:: py
output_group_info = {}
for out in ctx.attr.outs:
output_group_info[out] = ctx.actions.declare_directory(out)
But this is not allowed in Starlark because dicts are immutable.
It is OK, however, to rebind the entire variable, like this:
.. code-block:: py
output_group_info = {}
for out in ctx.attr.outs:
output_group_info |= {out: ctx.actions.declare_directory(out)}
.. _blog-08-bazel-docgen-next:
-----------
What's next
-----------
Our top priorities are figuring out incremental builds and turning
down the old GN-based build.
Thank you for reading! If you'd like to discuss any of this with me, you can
find me in the ``#docs`` channel of `Pigweed's Discord
<https://discord.com/channels/691686718377558037/691686718377558040>`_.