Reland "docs: Build module docs with Bazel"

This is a reland of commit 5436d1039850f080d8c67f0ca192d72f70bf5041

Original change's description:
> docs: Build module docs with Bazel
>
> Bug: 318892911
> Change-Id: I0bff10bf25435c79707eb15a2e81bd52081ef517
> Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/248014
> Docs-Not-Needed: Kayce Basques <kayce@google.com>
> Reviewed-by: Alexei Frolov <frolv@google.com>
> Commit-Queue: Kayce Basques <kayce@google.com>

Bug: 318892911
Change-Id: If7c692f0f4a7d52f386051fda449180f7eeb5584
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/251812
Reviewed-by: Alexei Frolov <frolv@google.com>
Docs-Not-Needed: Kayce Basques <kayce@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Commit-Queue: Kayce Basques <kayce@google.com>
diff --git a/BUILD.bazel b/BUILD.bazel
index bbba691..1a6015c 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
 load("@hedron_compile_commands//:refresh_compile_commands.bzl", "refresh_compile_commands")
+load("@pigweed//pw_build:compatibility.bzl", "incompatible_with_mcu")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 
 licenses(["notice"])
 
@@ -58,3 +60,12 @@
         ],
     },
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig.zephyr",
+    ],
+    target_compatible_with = incompatible_with_mcu(),
+    visibility = ["//visibility:public"],
+)
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 7683a95..cc93b19 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -141,10 +141,200 @@
     deps = [
         ":not_prefixed",
         ":prefixed",
+        "//pw_alignment:docs",
+        "//pw_allocator:docs",
+        "//pw_analog:docs",
+        "//pw_android_toolchain:docs",
+        "//pw_arduino_build:docs",
+        "//pw_assert:docs",
+        "//pw_assert_basic:docs",
+        "//pw_assert_fuchsia:docs",
+        "//pw_assert_log:docs",
+        "//pw_assert_tokenized:docs",
+        "//pw_assert_trap:docs",
+        "//pw_assert_zephyr:docs",
+        "//pw_async:docs",
         "//pw_async2:docs",
+        "//pw_async2_basic:docs",
+        "//pw_async2_epoll:docs",
+        "//pw_async_basic:docs",
+        "//pw_async_fuchsia:docs",
+        "//pw_base64:docs",
+        "//pw_bloat:docs",
+        "//pw_blob_store:docs",
+        "//pw_bluetooth:docs",
+        "//pw_bluetooth_hci:docs",
+        "//pw_bluetooth_profiles:docs",
+        "//pw_bluetooth_proxy:docs",
+        "//pw_bluetooth_sapphire:docs",
         "//pw_boot:docs",
+        "//pw_boot_cortex_m:docs",
+        "//pw_build:docs",
+        "//pw_build_android:docs",
+        "//pw_build_info:docs",
+        "//pw_build_mcuxpresso:docs",
+        "//pw_bytes:docs",
+        "//pw_channel:docs",
+        "//pw_checksum:docs",
+        "//pw_chre:docs",
+        "//pw_chrono:docs",
+        "//pw_chrono_embos:docs",
+        "//pw_chrono_freertos:docs",
+        "//pw_chrono_rp2040:docs",
+        "//pw_chrono_stl:docs",
+        "//pw_chrono_threadx:docs",
+        "//pw_chrono_zephyr:docs",
+        "//pw_cli:docs",
+        "//pw_cli_analytics:docs",
+        "//pw_clock_tree:docs",
+        "//pw_clock_tree_mcuxpresso:docs",
+        "//pw_compilation_testing:docs",
+        "//pw_config_loader:docs",
+        "//pw_console:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_containers:docs",
+        "//pw_cpu_exception:docs",
+        "//pw_cpu_exception_cortex_m:docs",
+        "//pw_cpu_exception_risc_v:docs",
+        "//pw_crypto:docs",
+        "//pw_digital_io:docs",
+        "//pw_digital_io_linux:docs",
+        "//pw_digital_io_mcuxpresso:docs",
+        "//pw_digital_io_rp2040:docs",
+        "//pw_display:docs",
+        "//pw_dma_mcuxpresso:docs",
+        "//pw_docgen:docs",
+        "//pw_doctor:docs",
+        "//pw_emu:docs",
+        "//pw_env_setup:docs",
+        "//pw_env_setup_zephyr:docs",
+        "//pw_file:docs",
+        "//pw_format:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_function:docs",
+        "//pw_fuzzer:docs",
+        "//pw_grpc:docs",
+        "//pw_hdlc:docs",
+        "//pw_hex_dump:docs",
+        "//pw_i2c:docs",
+        "//pw_i2c_linux:docs",
+        "//pw_i2c_mcuxpresso:docs",
+        "//pw_i2c_rp2040:docs",
+        "//pw_ide:docs",
+        "//pw_interrupt:docs",
+        "//pw_interrupt_cortex_m:docs",
+        "//pw_interrupt_xtensa:docs",
+        "//pw_interrupt_zephyr:docs",
+        "//pw_intrusive_ptr:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_json:docs",
+        "//pw_kvs:docs",
+        "//pw_libc:docs",
+        "//pw_libcxx:docs",
+        "//pw_log:docs",
+        "//pw_log_android:docs",
+        "//pw_log_basic:docs",
+        "//pw_log_fuchsia:docs",
+        "//pw_log_null:docs",
+        "//pw_log_rpc:docs",
+        "//pw_log_string:docs",
+        "//pw_log_tokenized:docs",
+        "//pw_log_zephyr:docs",
+        "//pw_malloc:docs",
+        "//pw_malloc_freelist:docs",
+        "//pw_malloc_freertos:docs",
+        "//pw_metric:docs",
+        "//pw_minimal_cpp_stdlib:docs",
+        "//pw_module:docs",
+        "//pw_multibuf:docs",
+        "//pw_multisink:docs",
+        "//pw_numeric:docs",
+        "//pw_package:docs",
+        "//pw_perf_test:docs",
+        "//pw_persistent_ram:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_polyfill:docs",
+        "//pw_preprocessor:docs",
+        "//pw_presubmit:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_protobuf:docs",
+        "//pw_protobuf_compiler:docs",
+        "//pw_random:docs",
+        "//pw_random_fuchsia:docs",
+        "//pw_result:docs",
+        "//pw_ring_buffer:docs",
+        "//pw_router:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_rpc:docs",
+        "//pw_rpc_transport:docs",
+        "//pw_rust:sphinx",
+        "//pw_sensor:docs",
+        "//pw_snapshot:docs",
+        "//pw_software_update:docs",
+        "//pw_span:docs",
+        "//pw_spi:docs",
+        "//pw_spi_linux:docs",
+        "//pw_spi_mcuxpresso:docs",
+        "//pw_spi_rp2040:docs",
+        "//pw_status:docs",
+        "//pw_stm32cube_build:docs",
+        "//pw_stream:docs",
+        "//pw_stream_shmem_mcuxpresso:docs",
+        "//pw_stream_uart_linux:docs",
+        "//pw_stream_uart_mcuxpresso:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_string:docs",
+        "//pw_symbolizer:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_sync:docs",
+        "//pw_sync_baremetal:docs",
+        "//pw_sync_embos:docs",
+        "//pw_sync_freertos:docs",
+        "//pw_sync_stl:docs",
+        "//pw_sync_threadx:docs",
+        "//pw_sync_zephyr:docs",
+        "//pw_sys_io:docs",
+        "//pw_sys_io_ambiq_sdk:docs",
+        "//pw_sys_io_arduino:docs",
+        "//pw_sys_io_baremetal_lm3s6965evb:docs",
+        "//pw_sys_io_baremetal_stm32f429:docs",
+        "//pw_sys_io_emcraft_sf2:docs",
+        "//pw_sys_io_mcuxpresso:docs",
+        "//pw_sys_io_rp2040:docs",
+        "//pw_sys_io_stdio:docs",
+        "//pw_sys_io_stm32cube:docs",
+        "//pw_sys_io_zephyr:docs",
+        "//pw_system:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_target_runner:docs",
+        "//pw_thread:docs",
+        "//pw_thread_embos:docs",
+        "//pw_thread_freertos:docs",
+        "//pw_thread_stl:docs",
+        "//pw_thread_threadx:docs",
+        "//pw_thread_zephyr:docs",
+        "//pw_tls_client:docs",
+        "//pw_tls_client_boringssl:docs",
+        "//pw_tls_client_mbedtls:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_tokenizer:docs",
+        "//pw_toolchain:docs",
+        "//pw_trace:docs",
+        "//pw_trace_tokenized:docs",
+        "//pw_transfer:docs",
+        "//pw_uart:docs",
+        "//pw_uart_mcuxpresso:docs",
+        # TODO: https://pwbug.dev/378765499 - Update downstream projects.
+        # "//pw_unit_test:docs",
+        "//pw_unit_test_zephyr:docs",
+        # TODO: https://pwbug.dev/378570156 - Enable after Doxygen lands.
+        # "//pw_varint:docs",
+        "//pw_watch:docs",
+        "//pw_web:docs",
+        "//pw_work_queue:docs",
         "//seed:docs",
         "//targets:docs",
+        "//:docs",
     ],
 )
 
diff --git a/pw_alignment/BUILD.bazel b/pw_alignment/BUILD.bazel
index f6958b3..4f3abcc 100644
--- a/pw_alignment/BUILD.bazel
+++ b/pw_alignment/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -26,3 +29,12 @@
         "public/pw_alignment/alignment.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_alignment/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_allocator/BUILD.bazel b/pw_allocator/BUILD.bazel
index 7dcf951..8b2fab6 100644
--- a/pw_allocator/BUILD.bazel
+++ b/pw_allocator/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -776,8 +778,6 @@
     ],
 )
 
-# Docs
-
 cc_library(
     name = "size_reporter",
     hdrs = ["public/pw_allocator/size_reporter.h"],
@@ -789,6 +789,8 @@
     ],
 )
 
+# Docs
+
 filegroup(
     name = "doxygen",
     srcs = [
@@ -826,3 +828,16 @@
         "//pw_allocator/bucket:doxygen",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "api.rst",
+        "code_size.rst",
+        "design.rst",
+        "docs.rst",
+        "guide.rst",
+    ],
+    prefix = "pw_allocator/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_analog/BUILD.bazel b/pw_analog/BUILD.bazel
index 3214380..e00bf93 100644
--- a/pw_analog/BUILD.bazel
+++ b/pw_analog/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -104,3 +105,12 @@
         "public/pw_analog/microvolt_input.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_analog/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_android_toolchain/BUILD.bazel b/pw_android_toolchain/BUILD.bazel
new file mode 100644
index 0000000..1513198
--- /dev/null
+++ b/pw_android_toolchain/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_android_toolchain/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_arduino_build/BUILD.bazel b/pw_arduino_build/BUILD.bazel
index 9caec27..f3c8eb3 100644
--- a/pw_arduino_build/BUILD.bazel
+++ b/pw_arduino_build/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -34,3 +37,12 @@
     strip_include_prefix = "public",
     visibility = ["//visibility:public"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_arduino_build/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_assert/BUILD.bazel b/pw_assert/BUILD.bazel
index b09992d..216f0aa 100644
--- a/pw_assert/BUILD.bazel
+++ b/pw_assert/BUILD.bazel
@@ -12,7 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-load("//pw_build:compatibility.bzl", "host_backend_alias")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "host_backend_alias", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -237,3 +238,13 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_assert/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_assert_basic/BUILD.bazel b/pw_assert_basic/BUILD.bazel
index f6f23b0..bce72ff 100644
--- a/pw_assert_basic/BUILD.bazel
+++ b/pw_assert_basic/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 
 package(default_visibility = ["//visibility:public"])
@@ -89,3 +91,12 @@
     # necessary at link time.
     alwayslink = 1,
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_assert_basic/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_assert_fuchsia/BUILD.bazel b/pw_assert_fuchsia/BUILD.bazel
index 1f310ea..b5bb0e5 100644
--- a/pw_assert_fuchsia/BUILD.bazel
+++ b/pw_assert_fuchsia/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -38,8 +41,11 @@
     ],
 )
 
-# Bazel does not yet support building docs.
-filegroup(
+sphinx_docs_library(
     name = "docs",
-    srcs = ["docs.rst"],
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_assert_fuchsia/",
+    target_compatible_with = incompatible_with_mcu(),
 )
diff --git a/pw_assert_log/BUILD.bazel b/pw_assert_log/BUILD.bazel
index f19b350..49b0d6a 100644
--- a/pw_assert_log/BUILD.bazel
+++ b/pw_assert_log/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -78,3 +81,12 @@
 
 # There is no "impl" target: pw_assert_log doesn't have potential circular
 # dependencies.
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_assert_log/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_assert_tokenized/BUILD.bazel b/pw_assert_tokenized/BUILD.bazel
index 7a50f6a..e909429 100644
--- a/pw_assert_tokenized/BUILD.bazel
+++ b/pw_assert_tokenized/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -95,3 +98,12 @@
         "//pw_tokenizer",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_assert_tokenized/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_assert_trap/BUILD.bazel b/pw_assert_trap/BUILD.bazel
index 7db95dc..ccf1943 100644
--- a/pw_assert_trap/BUILD.bazel
+++ b/pw_assert_trap/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -150,3 +152,12 @@
         ":pw_assert_trap",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_assert_trap/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_assert_zephyr/BUILD.bazel b/pw_assert_zephyr/BUILD.bazel
new file mode 100644
index 0000000..2f2e277
--- /dev/null
+++ b/pw_assert_zephyr/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_assert_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_async/BUILD.bazel b/pw_async/BUILD.bazel
index 595ec78..ab2bb04 100644
--- a/pw_async/BUILD.bazel
+++ b/pw_async/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -125,3 +127,13 @@
         "public/pw_async/task_function.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_async/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_async2/BUILD.bazel b/pw_async2/BUILD.bazel
index 65f4c95..9d12647 100644
--- a/pw_async2/BUILD.bazel
+++ b/pw_async2/BUILD.bazel
@@ -373,6 +373,7 @@
     srcs = [
         "backends.rst",
         "docs.rst",
+        "guides.rst",
     ],
     prefix = "pw_async2/",
     target_compatible_with = incompatible_with_mcu(),
diff --git a/pw_async2_basic/BUILD.bazel b/pw_async2_basic/BUILD.bazel
index cef1b24..7592d98 100644
--- a/pw_async2_basic/BUILD.bazel
+++ b/pw_async2_basic/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -37,3 +40,12 @@
         "public_overrides/pw_async2/dispatcher_native.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_async2_basic/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_async2_epoll/BUILD.bazel b/pw_async2_epoll/BUILD.bazel
index 36f9765..b5e4cd6 100644
--- a/pw_async2_epoll/BUILD.bazel
+++ b/pw_async2_epoll/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -31,3 +34,12 @@
         "//pw_log",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_async2_epoll/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_async_basic/BUILD.bazel b/pw_async_basic/BUILD.bazel
index c2200cb..89ee98d 100644
--- a/pw_async_basic/BUILD.bazel
+++ b/pw_async_basic/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -103,3 +104,12 @@
         "public/pw_async_basic/dispatcher.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_async_basic/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_async_fuchsia/BUILD.bazel b/pw_async_fuchsia/BUILD.bazel
index 8bba62b..feab45e 100644
--- a/pw_async_fuchsia/BUILD.bazel
+++ b/pw_async_fuchsia/BUILD.bazel
@@ -17,7 +17,9 @@
     "fuchsia_cc_test",
     "fuchsia_unittest_package",
 )
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_bluetooth_sapphire/fuchsia:fuchsia_api_level.bzl", "FUCHSIA_API_LEVEL")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -119,8 +121,11 @@
     unit_tests = [":pw_async_fuchsia_test"],
 )
 
-# Bazel does not yet support building docs.
-filegroup(
+sphinx_docs_library(
     name = "docs",
-    srcs = ["docs.rst"],
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_async_fuchsia/",
+    target_compatible_with = incompatible_with_mcu(),
 )
diff --git a/pw_base64/BUILD.bazel b/pw_base64/BUILD.bazel
index caa332a..dfb9865 100644
--- a/pw_base64/BUILD.bazel
+++ b/pw_base64/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -52,3 +54,13 @@
         "public/pw_base64/base64.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_base64/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_bloat/BUILD.bazel b/pw_bloat/BUILD.bazel
index d89c71f..b0248c6 100644
--- a/pw_bloat/BUILD.bazel
+++ b/pw_bloat/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_cc_binary.bzl", "pw_cc_binary")
 
@@ -40,3 +41,12 @@
     target_compatible_with = incompatible_with_mcu(),
     deps = [":bloat_this_binary"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_bloat/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_blob_store/BUILD.bazel b/pw_blob_store/BUILD.bazel
index 7db73aa..318f40d 100644
--- a/pw_blob_store/BUILD.bazel
+++ b/pw_blob_store/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -116,3 +118,12 @@
         "//pw_sync:mutex",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_blob_store/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_bluetooth/BUILD.bazel b/pw_bluetooth/BUILD.bazel
index 9073fa4..4340faa 100644
--- a/pw_bluetooth/BUILD.bazel
+++ b/pw_bluetooth/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@com_google_emboss//:build_defs.bzl", "emboss_cc_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -395,3 +397,12 @@
         "public/pw_bluetooth/low_energy/peripheral2.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_bluetooth/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_bluetooth_hci/BUILD.bazel b/pw_bluetooth_hci/BUILD.bazel
index ecdff22..1b8a15f 100644
--- a/pw_bluetooth_hci/BUILD.bazel
+++ b/pw_bluetooth_hci/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -98,3 +100,12 @@
         "//pw_stream",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_bluetooth_hci/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_bluetooth_profiles/BUILD.bazel b/pw_bluetooth_profiles/BUILD.bazel
index 91dacf8..93be0f4 100644
--- a/pw_bluetooth_profiles/BUILD.bazel
+++ b/pw_bluetooth_profiles/BUILD.bazel
@@ -12,18 +12,14 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
 
-# Bazel does not yet support building docs.
-filegroup(
-    name = "docs",
-    srcs = ["docs.rst"],
-)
-
 # Device Information Service 1.1
 cc_library(
     name = "device_info_service",
@@ -45,3 +41,12 @@
     srcs = ["device_info_service_test.cc"],
     deps = [":device_info_service"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_bluetooth_profiles/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_bluetooth_proxy/BUILD.bazel b/pw_bluetooth_proxy/BUILD.bazel
index 4475966..80b7a29 100644
--- a/pw_bluetooth_proxy/BUILD.bazel
+++ b/pw_bluetooth_proxy/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -124,3 +126,12 @@
         "public/pw_bluetooth_proxy/proxy_host.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_bluetooth_proxy/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_bluetooth_sapphire/BUILD.bazel b/pw_bluetooth_sapphire/BUILD.bazel
index 125724c..70f43c6 100644
--- a/pw_bluetooth_sapphire/BUILD.bazel
+++ b/pw_bluetooth_sapphire/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//pw_bluetooth_sapphire:__subpackages__"])
@@ -287,3 +289,14 @@
         "//pw_bluetooth_sapphire/host/gap:testing",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+        "fuchsia.rst",
+    ],
+    prefix = "pw_bluetooth_sapphire/",
+    target_compatible_with = incompatible_with_mcu(),
+    visibility = ["//visibility:public"],
+)
diff --git a/pw_boot_cortex_m/BUILD.bazel b/pw_boot_cortex_m/BUILD.bazel
index 8a3b539..268adc5 100644
--- a/pw_boot_cortex_m/BUILD.bazel
+++ b/pw_boot_cortex_m/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -63,3 +66,12 @@
     }),
     deps = [":pw_boot_cortex_m"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_boot_cortex_m/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_build/BUILD.bazel b/pw_build/BUILD.bazel
index 7a2625e..69776b8 100644
--- a/pw_build/BUILD.bazel
+++ b/pw_build/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:glob_dirs.bzl", "glob_dirs", "match_dir", "match_dir_internal")
 load("//pw_build:load_phase_test.bzl", "pw_string_comparison_test", "pw_string_list_comparison_test", "return_error")
 load("//pw_build:pw_cc_binary.bzl", "pw_cc_binary_with_map")
@@ -276,3 +278,18 @@
         "public/pw_build/must_place.ld.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "bazel.rst",
+        "cmake.rst",
+        "docs.rst",
+        "gn.rst",
+        "linker_scripts.rst",
+        "project_builder.rst",
+        "python.rst",
+    ],
+    prefix = "pw_build/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_build_android/BUILD.bazel b/pw_build_android/BUILD.bazel
new file mode 100644
index 0000000..d3aeb12
--- /dev/null
+++ b/pw_build_android/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_build_android/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_build_info/BUILD.bazel b/pw_build_info/BUILD.bazel
index a926060..64580a5 100644
--- a/pw_build_info/BUILD.bazel
+++ b/pw_build_info/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_linker_script.bzl", "pw_linker_script")
 load("//pw_build:python.bzl", "pw_py_binary")
 load(
@@ -136,3 +138,12 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_build_info/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_build_mcuxpresso/BUILD.bazel b/pw_build_mcuxpresso/BUILD.bazel
new file mode 100644
index 0000000..b651560
--- /dev/null
+++ b/pw_build_mcuxpresso/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_build_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_bytes/BUILD.bazel b/pw_bytes/BUILD.bazel
index b425b02..8c57221 100644
--- a/pw_bytes/BUILD.bazel
+++ b/pw_bytes/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -157,3 +159,13 @@
         "public/pw_bytes/packed_ptr.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_bytes/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_channel/BUILD.bazel b/pw_channel/BUILD.bazel
index 290aa9a..d80042e 100644
--- a/pw_channel/BUILD.bazel
+++ b/pw_channel/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -180,3 +182,15 @@
         "public/pw_channel/stream_channel.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "design.rst",
+        "docs.rst",
+        "guides.rst",
+        "reference.rst",
+    ],
+    prefix = "pw_channel/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_checksum/BUILD.bazel b/pw_checksum/BUILD.bazel
index d58108b..66ca559 100644
--- a/pw_checksum/BUILD.bazel
+++ b/pw_checksum/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_perf_test:pw_cc_perf_test.bzl", "pw_cc_perf_test")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -86,3 +88,13 @@
         "//pw_bytes",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_checksum/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_chre/BUILD.bazel b/pw_chre/BUILD.bazel
index d0f9c5a..eadeb27 100644
--- a/pw_chre/BUILD.bazel
+++ b/pw_chre/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -67,3 +70,12 @@
         "public/pw_chre/host_link.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_chre/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_chrono/BUILD.bazel b/pw_chrono/BUILD.bazel
index 97c1fa6..a623fa7 100644
--- a/pw_chrono/BUILD.bazel
+++ b/pw_chrono/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_build:python.bzl", "pw_py_binary")
 load("//pw_protobuf_compiler:pw_proto_library.bzl", "pwpb_proto_library")
@@ -208,3 +210,13 @@
         "public/pw_chrono/virtual_clock.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_chrono/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_chrono_embos/BUILD.bazel b/pw_chrono_embos/BUILD.bazel
index 210ba4a..b9840b9 100644
--- a/pw_chrono_embos/BUILD.bazel
+++ b/pw_chrono_embos/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -75,3 +78,12 @@
         "//pw_interrupt:context",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_chrono_embos/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_chrono_freertos/BUILD.bazel b/pw_chrono_freertos/BUILD.bazel
index a0874fd..b5cfebf 100644
--- a/pw_chrono_freertos/BUILD.bazel
+++ b/pw_chrono_freertos/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -73,3 +76,12 @@
         "@freertos",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_chrono_freertos/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_chrono_rp2040/BUILD.bazel b/pw_chrono_rp2040/BUILD.bazel
index 72ea9f7..cc9efe8 100644
--- a/pw_chrono_rp2040/BUILD.bazel
+++ b/pw_chrono_rp2040/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -49,8 +51,11 @@
     deps = [":system_clock"],
 )
 
-# Bazel does not yet support building docs.
-filegroup(
+sphinx_docs_library(
     name = "docs",
-    srcs = ["docs.rst"],
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_chrono_rp2040/",
+    target_compatible_with = incompatible_with_mcu(),
 )
diff --git a/pw_chrono_stl/BUILD.bazel b/pw_chrono_stl/BUILD.bazel
index aa59b8f..126731c 100644
--- a/pw_chrono_stl/BUILD.bazel
+++ b/pw_chrono_stl/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 
 package(default_visibility = ["//visibility:public"])
@@ -59,3 +60,12 @@
         "//pw_function",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_chrono_stl/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_chrono_threadx/BUILD.bazel b/pw_chrono_threadx/BUILD.bazel
index 3313d12..5ed7f7e 100644
--- a/pw_chrono_threadx/BUILD.bazel
+++ b/pw_chrono_threadx/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -47,3 +50,12 @@
     name = "config_override",
     build_setting_default = "//pw_build:default_module_config",
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_chrono_threadx/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_chrono_zephyr/BUILD.bazel b/pw_chrono_zephyr/BUILD.bazel
new file mode 100644
index 0000000..6d29e4a
--- /dev/null
+++ b/pw_chrono_zephyr/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_chrono_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_cli/BUILD.bazel b/pw_cli/BUILD.bazel
new file mode 100644
index 0000000..dd8f244
--- /dev/null
+++ b/pw_cli/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "api.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_cli/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_cli_analytics/BUILD.bazel b/pw_cli_analytics/BUILD.bazel
new file mode 100644
index 0000000..4f3452d
--- /dev/null
+++ b/pw_cli_analytics/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_cli_analytics/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_clock_tree/BUILD.bazel b/pw_clock_tree/BUILD.bazel
index 23a0e4d..17a74e1 100644
--- a/pw_clock_tree/BUILD.bazel
+++ b/pw_clock_tree/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -53,3 +55,15 @@
         "public/pw_clock_tree/clock_tree.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "api.rst",
+        "docs.rst",
+        "examples.rst",
+        "implementations.rst",
+    ],
+    prefix = "pw_clock_tree/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_clock_tree_mcuxpresso/BUILD.bazel b/pw_clock_tree_mcuxpresso/BUILD.bazel
index 2932e6f..ce66c38 100644
--- a/pw_clock_tree_mcuxpresso/BUILD.bazel
+++ b/pw_clock_tree_mcuxpresso/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -46,3 +48,12 @@
         "public/pw_clock_tree_mcuxpresso/clock_tree.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_clock_tree_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_compilation_testing/BUILD.bazel b/pw_compilation_testing/BUILD.bazel
index 48d28fa..6cfeba8 100644
--- a/pw_compilation_testing/BUILD.bazel
+++ b/pw_compilation_testing/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 licenses(["notice"])
 
 # Negative compilation testing is not yet supported in Bazel.
@@ -21,3 +24,13 @@
     strip_include_prefix = "public",
     visibility = ["//:__subpackages__"],  # Restrict to Pigweed
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_compilation_testing/",
+    target_compatible_with = incompatible_with_mcu(),
+    visibility = ["//visibility:public"],
+)
diff --git a/pw_config_loader/BUILD.bazel b/pw_config_loader/BUILD.bazel
new file mode 100644
index 0000000..17cb29f
--- /dev/null
+++ b/pw_config_loader/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_config_loader/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_console/BUILD.bazel b/pw_console/BUILD.bazel
new file mode 100644
index 0000000..fc9a17f
--- /dev/null
+++ b/pw_console/BUILD.bazel
@@ -0,0 +1,32 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+        "embedding.rst",
+        "internals.rst",
+        "plugins.rst",
+        "testing.rst",
+        "//pw_console/py:pw_console/docs/user_guide.rst",
+    ],
+    prefix = "pw_console/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_containers/BUILD.bazel b/pw_containers/BUILD.bazel
index 7101f90..3dd3d7f 100644
--- a/pw_containers/BUILD.bazel
+++ b/pw_containers/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -467,3 +469,13 @@
         "public/pw_containers/intrusive_set.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_containers/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_cpu_exception/BUILD.bazel b/pw_cpu_exception/BUILD.bazel
index 8f6c312..c954352 100644
--- a/pw_cpu_exception/BUILD.bazel
+++ b/pw_cpu_exception/BUILD.bazel
@@ -12,7 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-load("//pw_build:compatibility.bzl", "boolean_constraint_value")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "boolean_constraint_value", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 
 package(default_visibility = ["//visibility:public"])
@@ -124,3 +125,13 @@
     name = "support_backend",
     build_setting_default = "//pw_build:unspecified_backend",
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_cpu_exception/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_cpu_exception_cortex_m/BUILD.bazel b/pw_cpu_exception_cortex_m/BUILD.bazel
index 1030a84..79e91a9 100644
--- a/pw_cpu_exception_cortex_m/BUILD.bazel
+++ b/pw_cpu_exception_cortex_m/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_protobuf_compiler:pw_proto_library.bzl", "pwpb_proto_library")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
@@ -312,3 +314,12 @@
         "//pw_sync:mutex",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_cpu_exception_cortex_m/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_cpu_exception_risc_v/BUILD.bazel b/pw_cpu_exception_risc_v/BUILD.bazel
index 86e10e5..1b1b790 100644
--- a/pw_cpu_exception_risc_v/BUILD.bazel
+++ b/pw_cpu_exception_risc_v/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_protobuf_compiler:pw_proto_library.bzl", "pwpb_proto_library")
 
 package(default_visibility = ["//visibility:public"])
@@ -111,3 +113,12 @@
         "//pw_thread:thread_pwpb",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_cpu_exception_risc_v/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_crypto/BUILD.bazel b/pw_crypto/BUILD.bazel
index d123d1a..e0efade 100644
--- a/pw_crypto/BUILD.bazel
+++ b/pw_crypto/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -214,3 +216,12 @@
         "public/pw_crypto/sha256.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_crypto/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_digital_io/BUILD.bazel b/pw_digital_io/BUILD.bazel
index 16d8f40..b4a08ab 100644
--- a/pw_digital_io/BUILD.bazel
+++ b/pw_digital_io/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
     "pwpb_proto_library",
@@ -120,3 +122,13 @@
         "public/pw_digital_io/digital_io_mock.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_digital_io/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_digital_io_linux/BUILD.bazel b/pw_digital_io_linux/BUILD.bazel
index f576257..dabbab8 100644
--- a/pw_digital_io_linux/BUILD.bazel
+++ b/pw_digital_io_linux/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -108,3 +110,12 @@
         "//pw_thread_stl:thread",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_digital_io_linux/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_digital_io_mcuxpresso/BUILD.bazel b/pw_digital_io_mcuxpresso/BUILD.bazel
index 7622ff7..d07206e 100644
--- a/pw_digital_io_mcuxpresso/BUILD.bazel
+++ b/pw_digital_io_mcuxpresso/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -44,3 +46,12 @@
     srcs = ["mimxrt595_test.cc"],
     deps = [":pw_digital_io_mcuxpresso"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_digital_io_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_digital_io_rp2040/BUILD.bazel b/pw_digital_io_rp2040/BUILD.bazel
index 7fdfef7..5eee8e4 100644
--- a/pw_digital_io_rp2040/BUILD.bazel
+++ b/pw_digital_io_rp2040/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -40,3 +42,12 @@
     srcs = ["digital_io_test.cc"],
     deps = [":pw_digital_io_rp2040"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_digital_io_rp2040/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_display/BUILD.bazel b/pw_display/BUILD.bazel
index 39800fe..1525d0f 100644
--- a/pw_display/BUILD.bazel
+++ b/pw_display/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -40,3 +42,13 @@
         "public/pw_display/color.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "api.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_display/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_dma_mcuxpresso/BUILD.bazel b/pw_dma_mcuxpresso/BUILD.bazel
index c74cc45..9b6ea30 100644
--- a/pw_dma_mcuxpresso/BUILD.bazel
+++ b/pw_dma_mcuxpresso/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -26,3 +29,12 @@
         "//targets:mcuxpresso_sdk",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_dma_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_docgen/BUILD.bazel b/pw_docgen/BUILD.bazel
index 7bb1810..e20d2db 100644
--- a/pw_docgen/BUILD.bazel
+++ b/pw_docgen/BUILD.bazel
@@ -12,6 +12,18 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_docgen/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_doctor/BUILD.bazel b/pw_doctor/BUILD.bazel
index 7bb1810..f1096eb 100644
--- a/pw_doctor/BUILD.bazel
+++ b/pw_doctor/BUILD.bazel
@@ -12,6 +12,18 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_doctor/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_emu/BUILD.bazel b/pw_emu/BUILD.bazel
new file mode 100644
index 0000000..5f95a34
--- /dev/null
+++ b/pw_emu/BUILD.bazel
@@ -0,0 +1,32 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "api.rst",
+        "cli.rst",
+        "config.rst",
+        "design.rst",
+        "docs.rst",
+        "guide.rst",
+    ],
+    prefix = "pw_emu/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_env_setup/BUILD.bazel b/pw_env_setup/BUILD.bazel
index 486d1ed..d67f57b 100644
--- a/pw_env_setup/BUILD.bazel
+++ b/pw_env_setup/BUILD.bazel
@@ -11,3 +11,16 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_env_setup/",
+    target_compatible_with = incompatible_with_mcu(),
+    visibility = ["//visibility:public"],
+)
diff --git a/pw_env_setup_zephyr/BUILD.bazel b/pw_env_setup_zephyr/BUILD.bazel
new file mode 100644
index 0000000..c512176
--- /dev/null
+++ b/pw_env_setup_zephyr/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_env_setup_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_file/BUILD.bazel b/pw_file/BUILD.bazel
index 6fd1d6a..1d56aec 100644
--- a/pw_file/BUILD.bazel
+++ b/pw_file/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
     "pwpb_proto_library",
@@ -86,3 +88,12 @@
         "//pw_status",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_file/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_format/BUILD.bazel b/pw_format/BUILD.bazel
new file mode 100644
index 0000000..b7f0942
--- /dev/null
+++ b/pw_format/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_format/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_function/BUILD.bazel b/pw_function/BUILD.bazel
index 07672bf..5362b51 100644
--- a/pw_function/BUILD.bazel
+++ b/pw_function/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@bazel_skylib//lib:selects.bzl", "selects")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -120,3 +122,13 @@
         "public/pw_function/scope_guard.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_function/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_fuzzer/BUILD.bazel b/pw_fuzzer/BUILD.bazel
index 474bbae..6f1c9e9 100644
--- a/pw_fuzzer/BUILD.bazel
+++ b/pw_fuzzer/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -83,3 +86,16 @@
     ],
     strip_include_prefix = "public",
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "concepts.rst",
+        "docs.rst",
+        "guides/fuzztest.rst",
+        "guides/libfuzzer.rst",
+        "guides/reproducing_oss_fuzz_bugs.rst",
+    ],
+    prefix = "pw_fuzzer/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_grpc/BUILD.bazel b/pw_grpc/BUILD.bazel
index a98d95d..740ba80 100644
--- a/pw_grpc/BUILD.bazel
+++ b/pw_grpc/BUILD.bazel
@@ -14,6 +14,7 @@
 
 load("@io_bazel_rules_go//go:def.bzl", "go_test")
 load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
@@ -249,3 +250,12 @@
         "@org_golang_google_grpc_examples//features/proto/echo",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_grpc/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_hdlc/BUILD.bazel b/pw_hdlc/BUILD.bazel
index 1d453ba..7e93e3c 100644
--- a/pw_hdlc/BUILD.bazel
+++ b/pw_hdlc/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -197,3 +199,19 @@
         "public/pw_hdlc/router.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "api.rst",
+        "design.rst",
+        "docs.rst",
+        "guide.rst",
+        "router.rst",
+        "size.rst",
+        "//pw_hdlc/rpc_example:docs",
+    ],
+    prefix = "pw_hdlc/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_hdlc/rpc_example/BUILD.bazel b/pw_hdlc/rpc_example/BUILD.bazel
index eab96cd..50693b7 100644
--- a/pw_hdlc/rpc_example/BUILD.bazel
+++ b/pw_hdlc/rpc_example/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_cc_binary.bzl", "pw_cc_binary")
 
 pw_cc_binary(
@@ -29,3 +31,12 @@
         "//pw_rpc/system_server",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    target_compatible_with = incompatible_with_mcu(),
+    visibility = ["//visibility:public"],
+)
diff --git a/pw_hex_dump/BUILD.bazel b/pw_hex_dump/BUILD.bazel
index 91e3f27..59caffa 100644
--- a/pw_hex_dump/BUILD.bazel
+++ b/pw_hex_dump/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -80,3 +82,13 @@
         "public/pw_hex_dump/log_bytes.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "api.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_hex_dump/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_i2c/BUILD.bazel b/pw_i2c/BUILD.bazel
index c39305f..7a50079 100644
--- a/pw_i2c/BUILD.bazel
+++ b/pw_i2c/BUILD.bazel
@@ -14,6 +14,7 @@
 
 load("@rules_proto//proto:defs.bzl", "proto_library")
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
@@ -233,3 +234,15 @@
         "public/pw_i2c/register_device.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+        "guides.rst",
+        "reference.rst",
+    ],
+    prefix = "pw_i2c/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_i2c_linux/BUILD.bazel b/pw_i2c_linux/BUILD.bazel
index fa0108a..0691265 100644
--- a/pw_i2c_linux/BUILD.bazel
+++ b/pw_i2c_linux/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -58,3 +60,12 @@
         "public/pw_i2c_linux/initiator.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_i2c_linux/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_i2c_mcuxpresso/BUILD.bazel b/pw_i2c_mcuxpresso/BUILD.bazel
index 59257a8..0d94616 100644
--- a/pw_i2c_mcuxpresso/BUILD.bazel
+++ b/pw_i2c_mcuxpresso/BUILD.bazel
@@ -11,6 +11,9 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -70,3 +73,12 @@
         "//third_party/fuchsia:stdcompat",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_i2c_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_i2c_rp2040/BUILD.bazel b/pw_i2c_rp2040/BUILD.bazel
index 348be43..b6b51bc 100644
--- a/pw_i2c_rp2040/BUILD.bazel
+++ b/pw_i2c_rp2040/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -46,3 +48,12 @@
     srcs = ["initiator_test.cc"],
     deps = [":pw_i2c_rp2040"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_i2c_rp2040/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_ide/BUILD.bazel b/pw_ide/BUILD.bazel
index 443fbb6..4ec1736 100644
--- a/pw_ide/BUILD.bazel
+++ b/pw_ide/BUILD.bazel
@@ -12,4 +12,22 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "design/cpp.rst",
+        "design/index.rst",
+        "design/projects.rst",
+        "docs.rst",
+        "guide/cli.rst",
+        "guide/index.rst",
+        "guide/vscode/index.rst",
+    ],
+    prefix = "pw_ide/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_interrupt/BUILD.bazel b/pw_interrupt/BUILD.bazel
index 7493938..55eecd9 100644
--- a/pw_interrupt/BUILD.bazel
+++ b/pw_interrupt/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 
 package(default_visibility = ["//visibility:public"])
@@ -52,3 +54,13 @@
         "public/pw_interrupt/context.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_interrupt/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_interrupt_cortex_m/BUILD.bazel b/pw_interrupt_cortex_m/BUILD.bazel
index 71ab025..b8f0a20 100644
--- a/pw_interrupt_cortex_m/BUILD.bazel
+++ b/pw_interrupt_cortex_m/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -37,3 +40,12 @@
         "//pw_preprocessor:cortex_m",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_interrupt_cortex_m/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_interrupt_xtensa/BUILD.bazel b/pw_interrupt_xtensa/BUILD.bazel
index 3f98859..94f138d 100644
--- a/pw_interrupt_xtensa/BUILD.bazel
+++ b/pw_interrupt_xtensa/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -29,3 +32,12 @@
         "//pw_interrupt:context.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_interrupt_xtensa/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_interrupt_zephyr/BUILD.bazel b/pw_interrupt_zephyr/BUILD.bazel
new file mode 100644
index 0000000..36ac968
--- /dev/null
+++ b/pw_interrupt_zephyr/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_interrupt_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_intrusive_ptr/BUILD.bazel b/pw_intrusive_ptr/BUILD.bazel
index f1b1786..154b0df 100644
--- a/pw_intrusive_ptr/BUILD.bazel
+++ b/pw_intrusive_ptr/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -65,3 +67,12 @@
     }),
     deps = [":pw_intrusive_ptr"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_intrusive_ptr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_json/BUILD.bazel b/pw_json/BUILD.bazel
index feb4493..90d67bb 100644
--- a/pw_json/BUILD.bazel
+++ b/pw_json/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -48,3 +50,12 @@
         "public/pw_json/builder.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_json/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_kvs/BUILD.bazel b/pw_kvs/BUILD.bazel
index 2d42dd1..e6ef03a 100644
--- a/pw_kvs/BUILD.bazel
+++ b/pw_kvs/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -832,3 +833,12 @@
         "pw_kvs_private/config.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_kvs/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_libc/BUILD.bazel b/pw_libc/BUILD.bazel
index 12b1c2b..2c289d6 100644
--- a/pw_libc/BUILD.bazel
+++ b/pw_libc/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -28,3 +30,12 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_libc/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_libcxx/BUILD.bazel b/pw_libcxx/BUILD.bazel
index c81a228..fbc1baf 100644
--- a/pw_libcxx/BUILD.bazel
+++ b/pw_libcxx/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_toolchain/cc/current_toolchain:conditions.bzl", "if_compiler_is_clang")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -47,3 +49,12 @@
         "include/__external_threading",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_libcxx/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_log/BUILD.bazel b/pw_log/BUILD.bazel
index dcba950..58020d8 100644
--- a/pw_log/BUILD.bazel
+++ b/pw_log/BUILD.bazel
@@ -13,7 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
-load("//pw_build:compatibility.bzl", "host_backend_alias")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "host_backend_alias", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
@@ -252,3 +253,15 @@
         "public/pw_log/tokenized_args.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+        "protobuf.rst",
+        "tokenized_args.rst",
+    ],
+    prefix = "pw_log/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_log_android/BUILD.bazel b/pw_log_android/BUILD.bazel
new file mode 100644
index 0000000..0e30d30
--- /dev/null
+++ b/pw_log_android/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_log_android/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_log_basic/BUILD.bazel b/pw_log_basic/BUILD.bazel
index 10fdf38..10dab6f 100644
--- a/pw_log_basic/BUILD.bazel
+++ b/pw_log_basic/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -67,3 +70,12 @@
         "//pw_log_string:handler.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_log_basic/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_log_fuchsia/BUILD.bazel b/pw_log_fuchsia/BUILD.bazel
index 12b1e80..b0c7b99 100644
--- a/pw_log_fuchsia/BUILD.bazel
+++ b/pw_log_fuchsia/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -41,8 +44,11 @@
     ],
 )
 
-# Bazel does not yet support building docs.
-filegroup(
+sphinx_docs_library(
     name = "docs",
-    srcs = ["docs.rst"],
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_log_fuchsia/",
+    target_compatible_with = incompatible_with_mcu(),
 )
diff --git a/pw_log_null/BUILD.bazel b/pw_log_null/BUILD.bazel
index cabb7bb..80a5531 100644
--- a/pw_log_null/BUILD.bazel
+++ b/pw_log_null/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -51,3 +53,12 @@
     ],
     deps = [":pw_log_null"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_log_null/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_log_rpc/BUILD.bazel b/pw_log_rpc/BUILD.bazel
index 14724c3..6d49f9a 100644
--- a/pw_log_rpc/BUILD.bazel
+++ b/pw_log_rpc/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -229,3 +231,12 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_log_rpc/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_log_string/BUILD.bazel b/pw_log_string/BUILD.bazel
index ccfe12c..76b3cc5 100644
--- a/pw_log_string/BUILD.bazel
+++ b/pw_log_string/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 
 package(default_visibility = ["//visibility:public"])
@@ -82,3 +84,12 @@
         "public/pw_log_string/handler.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_log_string/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_log_tokenized/BUILD.bazel b/pw_log_tokenized/BUILD.bazel
index a57cdc9..db6e27f 100644
--- a/pw_log_tokenized/BUILD.bazel
+++ b/pw_log_tokenized/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -169,3 +171,12 @@
         "public/pw_log_tokenized/metadata.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_log_tokenized/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_log_zephyr/BUILD.bazel b/pw_log_zephyr/BUILD.bazel
new file mode 100644
index 0000000..8f9550c
--- /dev/null
+++ b/pw_log_zephyr/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_log_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_malloc/BUILD.bazel b/pw_malloc/BUILD.bazel
index 58fc2bc..be558fb 100644
--- a/pw_malloc/BUILD.bazel
+++ b/pw_malloc/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -350,3 +352,13 @@
         "public/pw_malloc/malloc.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_malloc/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_malloc_freelist/BUILD.bazel b/pw_malloc_freelist/BUILD.bazel
index 24d1476..ff02eea 100644
--- a/pw_malloc_freelist/BUILD.bazel
+++ b/pw_malloc_freelist/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -26,3 +29,12 @@
     name = "pw_malloc_freelist",
     actual = "//pw_malloc:bucket_allocator",
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_malloc_freelist/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_malloc_freertos/BUILD.bazel b/pw_malloc_freertos/BUILD.bazel
index d27cdfb..c568155 100644
--- a/pw_malloc_freertos/BUILD.bazel
+++ b/pw_malloc_freertos/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -40,3 +43,12 @@
         "//pw_malloc:facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_malloc_freertos/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_metric/BUILD.bazel b/pw_metric/BUILD.bazel
index e716580..4938b88 100644
--- a/pw_metric/BUILD.bazel
+++ b/pw_metric/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
     "nanopb_proto_library",
@@ -196,3 +198,12 @@
         "//pw_rpc/raw:test_method_context",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_metric/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_minimal_cpp_stdlib/BUILD.bazel b/pw_minimal_cpp_stdlib/BUILD.bazel
index 4ed616b..3e9e293 100644
--- a/pw_minimal_cpp_stdlib/BUILD.bazel
+++ b/pw_minimal_cpp_stdlib/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -94,3 +96,13 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_minimal_cpp_stdlib/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_module/BUILD.bazel b/pw_module/BUILD.bazel
new file mode 100644
index 0000000..812d074
--- /dev/null
+++ b/pw_module/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_module/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_multibuf/BUILD.bazel b/pw_multibuf/BUILD.bazel
index 257c190..ae90702 100644
--- a/pw_multibuf/BUILD.bazel
+++ b/pw_multibuf/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -251,3 +253,12 @@
         "public/pw_multibuf/stream.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_multibuf/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_multisink/BUILD.bazel b/pw_multisink/BUILD.bazel
index 0ef9ec4..dcbd1ab 100644
--- a/pw_multisink/BUILD.bazel
+++ b/pw_multisink/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -125,3 +126,13 @@
         ":stl_test_thread",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_multisink/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_numeric/BUILD.bazel b/pw_numeric/BUILD.bazel
index d884208..535dab6 100644
--- a/pw_numeric/BUILD.bazel
+++ b/pw_numeric/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pigweed.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -39,3 +41,12 @@
         "public/pw_numeric/integer_division.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_numeric/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_package/BUILD.bazel b/pw_package/BUILD.bazel
new file mode 100644
index 0000000..b1e29ae
--- /dev/null
+++ b/pw_package/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_package/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_perf_test/BUILD.bazel b/pw_perf_test/BUILD.bazel
index c1ff104..e67afa1 100644
--- a/pw_perf_test/BUILD.bazel
+++ b/pw_perf_test/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_perf_test:pw_cc_perf_test.bzl", "pw_cc_perf_test")
@@ -199,10 +200,13 @@
     srcs = ["examples/example_perf_test.cc"],
 )
 
-# Bazel does not yet support building docs.
-filegroup(
+sphinx_docs_library(
     name = "docs",
-    srcs = ["docs.rst"],
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_perf_test/",
+    target_compatible_with = incompatible_with_mcu(),
 )
 
 filegroup(
diff --git a/pw_persistent_ram/BUILD.bazel b/pw_persistent_ram/BUILD.bazel
index aa00241..83e517a 100644
--- a/pw_persistent_ram/BUILD.bazel
+++ b/pw_persistent_ram/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -83,3 +85,12 @@
         ":flat_file_system_entry",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_persistent_ram/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_polyfill/BUILD.bazel b/pw_polyfill/BUILD.bazel
index 091218f..4add763 100644
--- a/pw_polyfill/BUILD.bazel
+++ b/pw_polyfill/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -57,3 +59,13 @@
         "public/pw_polyfill/standard.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_polyfill/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_preprocessor/BUILD.bazel b/pw_preprocessor/BUILD.bazel
index 38a00be..ef25d7f 100644
--- a/pw_preprocessor/BUILD.bazel
+++ b/pw_preprocessor/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -75,3 +77,13 @@
         "public/pw_preprocessor/compiler.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_preprocessor/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_presubmit/BUILD.bazel b/pw_presubmit/BUILD.bazel
new file mode 100644
index 0000000..f1e9b4c
--- /dev/null
+++ b/pw_presubmit/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+        "format.rst",
+    ],
+    prefix = "pw_presubmit/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_protobuf/BUILD.bazel b/pw_protobuf/BUILD.bazel
index cb7b973..7f779fd 100644
--- a/pw_protobuf/BUILD.bazel
+++ b/pw_protobuf/BUILD.bazel
@@ -14,6 +14,8 @@
 
 load("@rules_proto//proto:defs.bzl", "proto_library")
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
 load("//pw_perf_test:pw_cc_perf_test.bzl", "pw_cc_perf_test")
 load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_filegroup", "pwpb_proto_library")
@@ -394,3 +396,13 @@
         "public/pw_protobuf/find.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+        "size_report.rst",
+    ],
+    prefix = "pw_protobuf/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_protobuf_compiler/BUILD.bazel b/pw_protobuf_compiler/BUILD.bazel
index 3179a78..2652e42 100644
--- a/pw_protobuf_compiler/BUILD.bazel
+++ b/pw_protobuf_compiler/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 load("@rules_proto//proto:defs.bzl", "proto_library")
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
     "nanopb_proto_library",
@@ -113,3 +115,12 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_protobuf_compiler/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_random/BUILD.bazel b/pw_random/BUILD.bazel
index c1c70cb..03cf09c 100644
--- a/pw_random/BUILD.bazel
+++ b/pw_random/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -62,3 +64,13 @@
         "public/pw_random/xor_shift.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_random/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_random_fuchsia/BUILD.bazel b/pw_random_fuchsia/BUILD.bazel
index 9774982..f42601b 100644
--- a/pw_random_fuchsia/BUILD.bazel
+++ b/pw_random_fuchsia/BUILD.bazel
@@ -17,7 +17,9 @@
     "fuchsia_cc_test",
     "fuchsia_unittest_package",
 )
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_bluetooth_sapphire/fuchsia:fuchsia_api_level.bzl", "FUCHSIA_API_LEVEL")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -54,8 +56,11 @@
     unit_tests = [":zircon_random_generator_test"],
 )
 
-# Bazel does not yet support building docs.
-filegroup(
+sphinx_docs_library(
     name = "docs",
-    srcs = ["docs.rst"],
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_random_fuchsia/",
+    target_compatible_with = incompatible_with_mcu(),
 )
diff --git a/pw_result/BUILD.bazel b/pw_result/BUILD.bazel
index 2b9506b..daface4 100644
--- a/pw_result/BUILD.bazel
+++ b/pw_result/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -68,3 +70,13 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_result/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_ring_buffer/BUILD.bazel b/pw_ring_buffer/BUILD.bazel
index a97c9d4..ce58abf 100644
--- a/pw_ring_buffer/BUILD.bazel
+++ b/pw_ring_buffer/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -45,3 +47,12 @@
         "//pw_varint",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_ring_buffer/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_router/BUILD.bazel b/pw_router/BUILD.bazel
index a1f20d7..5eec46a 100644
--- a/pw_router/BUILD.bazel
+++ b/pw_router/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -66,3 +68,13 @@
         ":static_router",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_router/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_rpc/BUILD.bazel b/pw_rpc/BUILD.bazel
index dc4e286..15bba78 100644
--- a/pw_rpc/BUILD.bazel
+++ b/pw_rpc/BUILD.bazel
@@ -14,6 +14,8 @@
 
 load("@rules_proto//proto:defs.bzl", "proto_library")
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
     "nanopb_proto_library",
@@ -558,3 +560,22 @@
         "public/pw_rpc/synchronous_call.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "cpp.rst",
+        "design.rst",
+        "docs.rst",
+        "guides.rst",
+        "libraries.rst",
+        "protocol.rst",
+        "ts/docs.rst",
+        "//pw_rpc/nanopb:docs",
+        "//pw_rpc/pwpb:docs",
+        "//pw_rpc/py:docs",
+    ],
+    prefix = "pw_rpc/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_rpc/nanopb/BUILD.bazel b/pw_rpc/nanopb/BUILD.bazel
index 4d59da9..50cbd55 100644
--- a/pw_rpc/nanopb/BUILD.bazel
+++ b/pw_rpc/nanopb/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -338,3 +339,11 @@
         "//pw_work_queue:test_thread_header",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_rpc/pwpb/BUILD.bazel b/pw_rpc/pwpb/BUILD.bazel
index b52f040..f0ce550 100644
--- a/pw_rpc/pwpb/BUILD.bazel
+++ b/pw_rpc/pwpb/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -328,3 +329,11 @@
         "//pw_work_queue:test_thread_header",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_rpc/py/BUILD.bazel b/pw_rpc/py/BUILD.bazel
index 958640d..144037d 100644
--- a/pw_rpc/py/BUILD.bazel
+++ b/pw_rpc/py/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:defs.bzl", "py_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:python.bzl", "pw_py_binary", "pw_py_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -226,3 +228,11 @@
         ":pw_rpc",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_rpc_transport/BUILD.bazel b/pw_rpc_transport/BUILD.bazel
index 6828e5b..f147f49 100644
--- a/pw_rpc_transport/BUILD.bazel
+++ b/pw_rpc_transport/BUILD.bazel
@@ -13,6 +13,7 @@
 # the License.
 
 load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
@@ -341,3 +342,12 @@
     pwpb_proto_library_deps = [":test_protos_pwpb"],
     deps = [":test_protos"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_rpc_transport/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_rust/BUILD.bazel b/pw_rust/BUILD.bazel
index 8b5b294..12a51bc 100644
--- a/pw_rust/BUILD.bazel
+++ b/pw_rust/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("@rules_rust//rust:defs.bzl", "rust_docs")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 
@@ -40,3 +41,13 @@
     ],
     target_compatible_with = incompatible_with_mcu(),
 )
+
+sphinx_docs_library(
+    name = "sphinx",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_rust/",
+    target_compatible_with = incompatible_with_mcu(),
+    visibility = ["//visibility:public"],
+)
diff --git a/pw_sensor/BUILD.bazel b/pw_sensor/BUILD.bazel
index 970c777..7c8ac33 100644
--- a/pw_sensor/BUILD.bazel
+++ b/pw_sensor/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 load(
     "sensor.bzl",
@@ -65,8 +67,12 @@
     ],
 )
 
-# Bazel does not yet support building docs.
-filegroup(
+sphinx_docs_library(
     name = "docs",
-    srcs = ["docs.rst"],
+    srcs = [
+        "docs.rst",
+        "//pw_sensor/py:docs",
+    ],
+    prefix = "pw_sensor/",
+    target_compatible_with = incompatible_with_mcu(),
 )
diff --git a/pw_sensor/py/BUILD.bazel b/pw_sensor/py/BUILD.bazel
index 61eb810..47090fe 100644
--- a/pw_sensor/py/BUILD.bazel
+++ b/pw_sensor/py/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:defs.bzl", "py_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:python.bzl", "pw_py_binary", "pw_py_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -75,3 +77,11 @@
         "@python_packages//pyyaml",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_snapshot/BUILD.bazel b/pw_snapshot/BUILD.bazel
index 293b347..f45c458 100644
--- a/pw_snapshot/BUILD.bazel
+++ b/pw_snapshot/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_protobuf_compiler:pw_proto_library.bzl", "pwpb_proto_library")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -111,3 +113,16 @@
         "//pw_status",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "design_discussion.rst",
+        "docs.rst",
+        "module_usage.rst",
+        "proto_format.rst",
+        "setup.rst",
+    ],
+    prefix = "pw_snapshot/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_software_update/BUILD.bazel b/pw_software_update/BUILD.bazel
index 271e693..a57fc13 100644
--- a/pw_software_update/BUILD.bazel
+++ b/pw_software_update/BUILD.bazel
@@ -14,6 +14,8 @@
 
 load("@rules_proto//proto:defs.bzl", "proto_library")
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_protobuf_compiler:pw_proto_library.bzl", "pwpb_proto_library")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -238,3 +240,16 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "cli.rst",
+        "design.rst",
+        "docs.rst",
+        "get_started.rst",
+        "guides.rst",
+    ],
+    prefix = "pw_software_update/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_span/BUILD.bazel b/pw_span/BUILD.bazel
index 9a350bf..6435fa6 100644
--- a/pw_span/BUILD.bazel
+++ b/pw_span/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -56,3 +58,13 @@
         "public/pw_span/internal/config.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_span/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_spi/BUILD.bazel b/pw_spi/BUILD.bazel
index 847095a..71fc558 100644
--- a/pw_spi/BUILD.bazel
+++ b/pw_spi/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -136,3 +138,13 @@
         "public/pw_spi/chip_selector_digital_out.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_spi/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_spi_linux/BUILD.bazel b/pw_spi_linux/BUILD.bazel
index 2bedca1..fbaa150 100644
--- a/pw_spi_linux/BUILD.bazel
+++ b/pw_spi_linux/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -65,3 +67,12 @@
         "//pw_unit_test",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_spi_linux/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_spi_mcuxpresso/BUILD.bazel b/pw_spi_mcuxpresso/BUILD.bazel
index d7b359a..06fa35b 100644
--- a/pw_spi_mcuxpresso/BUILD.bazel
+++ b/pw_spi_mcuxpresso/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -111,3 +113,12 @@
     ],
     deps = [":flexio_spi"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_spi_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_spi_rp2040/BUILD.bazel b/pw_spi_rp2040/BUILD.bazel
index 71d1d75..7e36e4b 100644
--- a/pw_spi_rp2040/BUILD.bazel
+++ b/pw_spi_rp2040/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -47,3 +49,12 @@
         "//pw_spi:device",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_spi_rp2040/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_status/BUILD.bazel b/pw_status/BUILD.bazel
index 49e76de..361f578 100644
--- a/pw_status/BUILD.bazel
+++ b/pw_status/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -81,3 +83,15 @@
         "public/pw_status/try.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+        "guide.rst",
+        "reference.rst",
+    ],
+    prefix = "pw_status/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_stm32cube_build/BUILD.bazel b/pw_stm32cube_build/BUILD.bazel
new file mode 100644
index 0000000..c17eaa8
--- /dev/null
+++ b/pw_stm32cube_build/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_stm32cube_build/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_stream/BUILD.bazel b/pw_stream/BUILD.bazel
index 603933f..a2d49ec 100644
--- a/pw_stream/BUILD.bazel
+++ b/pw_stream/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "boolean_constraint_value", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -210,3 +211,14 @@
         "public/pw_stream/stream.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_stream/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_stream_shmem_mcuxpresso/BUILD.bazel b/pw_stream_shmem_mcuxpresso/BUILD.bazel
index 3b8bc6d..8b4f935 100644
--- a/pw_stream_shmem_mcuxpresso/BUILD.bazel
+++ b/pw_stream_shmem_mcuxpresso/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -42,3 +44,12 @@
     ],
     deps = [":pw_stream_shmem_mcuxpresso"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_stream_shmem_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_stream_uart_linux/BUILD.bazel b/pw_stream_uart_linux/BUILD.bazel
index 756d996..e08c0e0 100644
--- a/pw_stream_uart_linux/BUILD.bazel
+++ b/pw_stream_uart_linux/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -50,3 +52,12 @@
         "public/pw_stream_uart_linux/stream.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_stream_uart_linux/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_stream_uart_mcuxpresso/BUILD.bazel b/pw_stream_uart_mcuxpresso/BUILD.bazel
index 42c6a16..c1ccd68 100644
--- a/pw_stream_uart_mcuxpresso/BUILD.bazel
+++ b/pw_stream_uart_mcuxpresso/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -76,3 +78,12 @@
     ],
     deps = [":pw_stream_uart_interrupt_safe_writer_mcuxpresso"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_stream_uart_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_string/BUILD.bazel b/pw_string/BUILD.bazel
index d98dbf9..0db12dc 100644
--- a/pw_string/BUILD.bazel
+++ b/pw_string/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -204,3 +206,17 @@
         "public/pw_string/util.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "api.rst",
+        "code_size.rst",
+        "design.rst",
+        "docs.rst",
+        "guide.rst",
+    ],
+    prefix = "pw_string/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_symbolizer/BUILD.bazel b/pw_symbolizer/BUILD.bazel
index 7d19ffe..13d9c3f 100644
--- a/pw_symbolizer/BUILD.bazel
+++ b/pw_symbolizer/BUILD.bazel
@@ -12,6 +12,18 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_symbolizer/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sync/BUILD.bazel b/pw_sync/BUILD.bazel
index d9ced6b..cd014f0 100644
--- a/pw_sync/BUILD.bazel
+++ b/pw_sync/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load(
     "//pw_build:compatibility.bzl",
     "host_backend_alias",
@@ -547,3 +548,13 @@
         "public/pw_sync/virtual_basic_lockable.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_sync/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sync_baremetal/BUILD.bazel b/pw_sync_baremetal/BUILD.bazel
index cca1033..dba531f 100644
--- a/pw_sync_baremetal/BUILD.bazel
+++ b/pw_sync_baremetal/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -74,3 +77,12 @@
         "//pw_sync:recursive_mutex.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sync_baremetal/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sync_embos/BUILD.bazel b/pw_sync_embos/BUILD.bazel
index b16338b..9204235 100644
--- a/pw_sync_embos/BUILD.bazel
+++ b/pw_sync_embos/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -149,3 +152,12 @@
         "//pw_sync:interrupt_spin_lock.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sync_embos/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sync_freertos/BUILD.bazel b/pw_sync_freertos/BUILD.bazel
index cbf9801..58a04ca 100644
--- a/pw_sync_freertos/BUILD.bazel
+++ b/pw_sync_freertos/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -309,3 +312,12 @@
 #         "//pw_thread_freertos:static_test_threads",
 #     ],
 # )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sync_freertos/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sync_stl/BUILD.bazel b/pw_sync_stl/BUILD.bazel
index 7753a1c..6c8f5e8 100644
--- a/pw_sync_stl/BUILD.bazel
+++ b/pw_sync_stl/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 
 package(default_visibility = ["//visibility:public"])
@@ -165,3 +166,12 @@
 #         "//pw_thread_stl:non_portable_test_thread_options",
 #     ]
 # )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sync_stl/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sync_threadx/BUILD.bazel b/pw_sync_threadx/BUILD.bazel
index 7ff6788..28103f9 100644
--- a/pw_sync_threadx/BUILD.bazel
+++ b/pw_sync_threadx/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -136,3 +139,12 @@
         "//pw_sync:interrupt_spin_lock.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sync_threadx/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sync_zephyr/BUILD.bazel b/pw_sync_zephyr/BUILD.bazel
new file mode 100644
index 0000000..1f8bf54
--- /dev/null
+++ b/pw_sync_zephyr/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_sync_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io/BUILD.bazel b/pw_sys_io/BUILD.bazel
index 0b5ce5d..e202033 100644
--- a/pw_sys_io/BUILD.bazel
+++ b/pw_sys_io/BUILD.bazel
@@ -12,7 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-load("//pw_build:compatibility.bzl", "host_backend_alias")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "host_backend_alias", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 
 package(default_visibility = ["//visibility:public"])
@@ -56,3 +57,13 @@
         "public/pw_sys_io/sys_io.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_ambiq_sdk/BUILD.bazel b/pw_sys_io_ambiq_sdk/BUILD.bazel
index 0b9ee75..33e0042 100644
--- a/pw_sys_io_ambiq_sdk/BUILD.bazel
+++ b/pw_sys_io_ambiq_sdk/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -28,3 +31,12 @@
         "//pw_sys_io:pw_sys_io.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_ambiq_sdk/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_arduino/BUILD.bazel b/pw_sys_io_arduino/BUILD.bazel
index 2d86592..12f6e00 100644
--- a/pw_sys_io_arduino/BUILD.bazel
+++ b/pw_sys_io_arduino/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -36,3 +39,12 @@
     strip_include_prefix = "public",
     visibility = ["//visibility:public"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_arduino/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel b/pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel
index f6f16a2..147f734 100644
--- a/pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel
+++ b/pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -31,3 +34,12 @@
         "//pw_sys_io:pw_sys_io.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_baremetal_lm3s6965evb/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_baremetal_stm32f429/BUILD.bazel b/pw_sys_io_baremetal_stm32f429/BUILD.bazel
index 990d171..8195630 100644
--- a/pw_sys_io_baremetal_stm32f429/BUILD.bazel
+++ b/pw_sys_io_baremetal_stm32f429/BUILD.bazel
@@ -11,7 +11,9 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
-load("//pw_build:compatibility.bzl", "boolean_constraint_value")
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "boolean_constraint_value", "incompatible_with_mcu")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -31,3 +33,12 @@
         "//pw_sys_io:pw_sys_io.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_baremetal_stm32f429/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_emcraft_sf2/BUILD.bazel b/pw_sys_io_emcraft_sf2/BUILD.bazel
index 46ab25d..b46e911 100644
--- a/pw_sys_io_emcraft_sf2/BUILD.bazel
+++ b/pw_sys_io_emcraft_sf2/BUILD.bazel
@@ -11,7 +11,9 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
-load("//pw_build:compatibility.bzl", "boolean_constraint_value")
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "boolean_constraint_value", "incompatible_with_mcu")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -42,3 +44,12 @@
     name = "config_override",
     build_setting_default = "//pw_build:default_module_config",
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_emcraft_sf2/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_mcuxpresso/BUILD.bazel b/pw_sys_io_mcuxpresso/BUILD.bazel
index 6e784cb..b44d121 100644
--- a/pw_sys_io_mcuxpresso/BUILD.bazel
+++ b/pw_sys_io_mcuxpresso/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -31,3 +34,12 @@
         "//targets:mcuxpresso_sdk",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_rp2040/BUILD.bazel b/pw_sys_io_rp2040/BUILD.bazel
index 14b99fd..4c86b1a 100644
--- a/pw_sys_io_rp2040/BUILD.bazel
+++ b/pw_sys_io_rp2040/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -29,3 +32,12 @@
         "@pico-sdk//src/rp2_common/pico_stdlib",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_rp2040/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_stdio/BUILD.bazel b/pw_sys_io_stdio/BUILD.bazel
index e96a9d5..35f3311 100644
--- a/pw_sys_io_stdio/BUILD.bazel
+++ b/pw_sys_io_stdio/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -24,3 +27,12 @@
         "//pw_sys_io:pw_sys_io.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_stdio/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_stm32cube/BUILD.bazel b/pw_sys_io_stm32cube/BUILD.bazel
index 386edc4..13c2884 100644
--- a/pw_sys_io_stm32cube/BUILD.bazel
+++ b/pw_sys_io_stm32cube/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -39,3 +42,12 @@
     name = "config_override",
     build_setting_default = "//pw_build:default_module_config",
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_stm32cube/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_sys_io_zephyr/BUILD.bazel b/pw_sys_io_zephyr/BUILD.bazel
new file mode 100644
index 0000000..377d72b
--- /dev/null
+++ b/pw_sys_io_zephyr/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_sys_io_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_system/BUILD.bazel b/pw_system/BUILD.bazel
index 23cc5fc..c8f22fa 100644
--- a/pw_system/BUILD.bazel
+++ b/pw_system/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_cc_binary.bzl", "pw_cc_binary")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load(
@@ -742,3 +744,14 @@
         "public/pw_system/system.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "cli.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_system/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_target_runner/BUILD.bazel b/pw_target_runner/BUILD.bazel
index 38c26f7..22b3455 100644
--- a/pw_target_runner/BUILD.bazel
+++ b/pw_target_runner/BUILD.bazel
@@ -14,6 +14,7 @@
 
 load("@io_bazel_rules_go//proto:def.bzl", "go_grpc_library", "go_proto_library")
 load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 
 package(default_visibility = ["//visibility:public"])
@@ -41,3 +42,13 @@
     proto = ":exec_server_config_protos",
     target_compatible_with = incompatible_with_mcu(),
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+        "//pw_target_runner/go:docs",
+    ],
+    prefix = "pw_target_runner/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_target_runner/go/BUILD.bazel b/pw_target_runner/go/BUILD.bazel
index ef082f1..82d7de8 100644
--- a/pw_target_runner/go/BUILD.bazel
+++ b/pw_target_runner/go/BUILD.bazel
@@ -13,6 +13,7 @@
 # the License.
 
 load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 
 package(default_visibility = ["//visibility:public"])
@@ -33,3 +34,11 @@
         "@org_golang_google_grpc//status",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_thread/BUILD.bazel b/pw_thread/BUILD.bazel
index 6abe968..9397774 100644
--- a/pw_thread/BUILD.bazel
+++ b/pw_thread/BUILD.bazel
@@ -13,7 +13,8 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
-load("//pw_build:compatibility.bzl", "host_backend_alias")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "host_backend_alias", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
@@ -441,3 +442,13 @@
         "public/pw_thread/thread.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_thread/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_thread_embos/BUILD.bazel b/pw_thread_embos/BUILD.bazel
index 3ef86ae..d0bc686 100644
--- a/pw_thread_embos/BUILD.bazel
+++ b/pw_thread_embos/BUILD.bazel
@@ -12,6 +12,9 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
@@ -173,3 +176,12 @@
     # TODO: b/234876414 - This should depend on embOS but our third parties
     # currently do not have Bazel support.
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_thread_embos/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_thread_freertos/BUILD.bazel b/pw_thread_freertos/BUILD.bazel
index 165b8bd..1fbeded 100644
--- a/pw_thread_freertos/BUILD.bazel
+++ b/pw_thread_freertos/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -308,3 +310,12 @@
         "//pw_thread:test_thread_context.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_thread_freertos/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_thread_stl/BUILD.bazel b/pw_thread_stl/BUILD.bazel
index 7e30691..219ad3d 100644
--- a/pw_thread_stl/BUILD.bazel
+++ b/pw_thread_stl/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_build:selects.bzl",
@@ -147,3 +148,12 @@
         "//pw_thread:test_thread_context.facade",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_thread_stl/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_thread_threadx/BUILD.bazel b/pw_thread_threadx/BUILD.bazel
index c27a2c7..d8631cd 100644
--- a/pw_thread_threadx/BUILD.bazel
+++ b/pw_thread_threadx/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -173,3 +175,12 @@
         "//pw_thread:thread_cc.pwpb",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_thread_threadx/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_thread_zephyr/BUILD.bazel b/pw_thread_zephyr/BUILD.bazel
new file mode 100644
index 0000000..18629a7
--- /dev/null
+++ b/pw_thread_zephyr/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_thread_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_tls_client/BUILD.bazel b/pw_tls_client/BUILD.bazel
index 017d5a4..5bf7536 100644
--- a/pw_tls_client/BUILD.bazel
+++ b/pw_tls_client/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_facade.bzl", "pw_facade")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -111,3 +113,13 @@
     tags = ["manual"],
     deps = [":test_server"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_tls_client/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_tls_client_boringssl/BUILD.bazel b/pw_tls_client_boringssl/BUILD.bazel
index 83a4e1b..9d2739a 100644
--- a/pw_tls_client_boringssl/BUILD.bazel
+++ b/pw_tls_client_boringssl/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -45,3 +47,12 @@
         ":pw_tls_client_boringssl",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_tls_client_boringssl/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_tls_client_mbedtls/BUILD.bazel b/pw_tls_client_mbedtls/BUILD.bazel
index 5bf2211..363eef6 100644
--- a/pw_tls_client_mbedtls/BUILD.bazel
+++ b/pw_tls_client_mbedtls/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -50,3 +52,12 @@
         ":pw_tls_client_mbedtls",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_tls_client_mbedtls/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_tokenizer/BUILD.bazel b/pw_tokenizer/BUILD.bazel
index 2240f2e..90b3235 100644
--- a/pw_tokenizer/BUILD.bazel
+++ b/pw_tokenizer/BUILD.bazel
@@ -13,6 +13,7 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_cc_binary.bzl", "pw_cc_binary")
 load("//pw_build:pw_cc_blob_library.bzl", "pw_cc_blob_info", "pw_cc_blob_library")
@@ -361,3 +362,18 @@
         "public/pw_tokenizer/tokenize.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        # "api.rst",
+        "detokenization.rst",
+        "docs.rst",
+        "get_started.rst",
+        "tokenization.rst",
+        "token_databases.rst",
+    ],
+    prefix = "pw_tokenizer/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_toolchain/BUILD.bazel b/pw_toolchain/BUILD.bazel
index f625d0b..1df7919 100644
--- a/pw_toolchain/BUILD.bazel
+++ b/pw_toolchain/BUILD.bazel
@@ -13,6 +13,8 @@
 # the License.
 
 load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -118,3 +120,14 @@
         "public/pw_toolchain/no_destructor.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "bazel.rst",
+        "docs.rst",
+        "gn.rst",
+    ],
+    prefix = "pw_toolchain/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_trace/BUILD.bazel b/pw_trace/BUILD.bazel
index 853c9a9..f9737f9 100644
--- a/pw_trace/BUILD.bazel
+++ b/pw_trace/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_cc_binary.bzl", "pw_cc_binary")
 load("//pw_build:pw_facade.bzl", "pw_facade")
@@ -150,3 +151,13 @@
         "//pw_log",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_trace/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_trace_tokenized/BUILD.bazel b/pw_trace_tokenized/BUILD.bazel
index 94ec2ce..19921c1 100644
--- a/pw_trace_tokenized/BUILD.bazel
+++ b/pw_trace_tokenized/BUILD.bazel
@@ -13,6 +13,7 @@
 # the License.
 
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_build:pw_cc_binary.bzl", "pw_cc_binary")
 load(
@@ -364,3 +365,12 @@
         "//pw_trace",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_trace_tokenized/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_transfer/BUILD.bazel b/pw_transfer/BUILD.bazel
index d8c04c5..107e33d 100644
--- a/pw_transfer/BUILD.bazel
+++ b/pw_transfer/BUILD.bazel
@@ -14,6 +14,8 @@
 
 load("@rules_proto//proto:defs.bzl", "proto_library")
 load("@rules_python//python:proto.bzl", "py_proto_library")
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load(
     "//pw_protobuf_compiler:pw_proto_library.bzl",
     "pwpb_proto_library",
@@ -288,3 +290,13 @@
         "public/pw_transfer/atomic_file_transfer_handler.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "api.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_transfer/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_uart/BUILD.bazel b/pw_uart/BUILD.bazel
index 060b94c..20b5a68 100644
--- a/pw_uart/BUILD.bazel
+++ b/pw_uart/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -144,10 +146,14 @@
     ],
 )
 
-# Bazel does not yet support building docs.
-filegroup(
+sphinx_docs_library(
     name = "docs",
-    srcs = ["docs.rst"],
+    srcs = [
+        "backends.rst",
+        "docs.rst",
+    ],
+    prefix = "pw_uart/",
+    target_compatible_with = incompatible_with_mcu(),
 )
 
 filegroup(
diff --git a/pw_uart_mcuxpresso/BUILD.bazel b/pw_uart_mcuxpresso/BUILD.bazel
index a3939eb..2249507 100644
--- a/pw_uart_mcuxpresso/BUILD.bazel
+++ b/pw_uart_mcuxpresso/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -45,3 +47,12 @@
     ],
     deps = [":pw_uart_mcuxpresso"],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_uart_mcuxpresso/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_unit_test/BUILD.bazel b/pw_unit_test/BUILD.bazel
index 2d4eb27..3fcf629 100644
--- a/pw_unit_test/BUILD.bazel
+++ b/pw_unit_test/BUILD.bazel
@@ -14,6 +14,9 @@
 
 load("@local_config_platform//:constraints.bzl", "HOST_CONSTRAINTS")
 load("@rules_python//python:proto.bzl", "py_proto_library")
+
+# TODO: https://pwbug.dev/378765499 - Update downstream projects.
+# load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "boolean_constraint_value", "incompatible_with_mcu")
 load("//pw_build:pw_cc_binary.bzl", "pw_cc_binary")
 load("//pw_build:pw_facade.bzl", "pw_facade")
@@ -512,3 +515,13 @@
         "public/pw_unit_test/test_record_event_handler.h",
     ],
 )
+
+# TODO: https://pwbug.dev/378765499 - Update downstream projects.
+# sphinx_docs_library(
+#     name = "docs",
+#     srcs = [
+#         "docs.rst",
+#     ],
+#     prefix = "pw_unit_test/",
+#     target_compatible_with = incompatible_with_mcu(),
+# )
diff --git a/pw_unit_test_zephyr/BUILD.bazel b/pw_unit_test_zephyr/BUILD.bazel
new file mode 100644
index 0000000..0532b7a
--- /dev/null
+++ b/pw_unit_test_zephyr/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_unit_test_zephyr/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_varint/BUILD.bazel b/pw_varint/BUILD.bazel
index dd101b9..38f344c 100644
--- a/pw_varint/BUILD.bazel
+++ b/pw_varint/BUILD.bazel
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
 package(default_visibility = ["//visibility:public"])
@@ -82,3 +84,13 @@
         "public/pw_varint/varint.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "Kconfig",
+        "docs.rst",
+    ],
+    prefix = "pw_varint/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_watch/BUILD.bazel b/pw_watch/BUILD.bazel
new file mode 100644
index 0000000..f5307ba
--- /dev/null
+++ b/pw_watch/BUILD.bazel
@@ -0,0 +1,29 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "cli.rst",
+        "docs.rst",
+        "guide.rst",
+    ],
+    prefix = "pw_watch/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_web/BUILD.bazel b/pw_web/BUILD.bazel
new file mode 100644
index 0000000..80adbea
--- /dev/null
+++ b/pw_web/BUILD.bazel
@@ -0,0 +1,30 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
+load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
+
+package(default_visibility = ["//visibility:public"])
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+        "log_viewer.rst",
+        "repl.rst",
+        "testing.rst",
+    ],
+    prefix = "pw_web/",
+    target_compatible_with = incompatible_with_mcu(),
+)
diff --git a/pw_work_queue/BUILD.bazel b/pw_work_queue/BUILD.bazel
index 7f3de5e..365606e 100644
--- a/pw_work_queue/BUILD.bazel
+++ b/pw_work_queue/BUILD.bazel
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
 load("//pw_build:compatibility.bzl", "incompatible_with_mcu")
 load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
 
@@ -86,3 +87,12 @@
         "public/pw_work_queue/work_queue.h",
     ],
 )
+
+sphinx_docs_library(
+    name = "docs",
+    srcs = [
+        "docs.rst",
+    ],
+    prefix = "pw_work_queue/",
+    target_compatible_with = incompatible_with_mcu(),
+)