Expand bazel build to include configuration options and broader support. (#1731)

* Add host Bazel build

Updates target_compatible_with across the repo to ensure that wildcard
builds for both host and rp2040 succeed.

* Get unit tests building

* Add Python script to identify build system differences

Uses the build system tags to make it easier to identify differences
between the CMake and Bazel builds.

* Temporarily disable pico divider test

* Support PICO_BARE_METAL in Bazel

* Support PICO_NO_GC_SECTIONS in Bazel

* Support boot2 configuration in Bazel

Adds support for PICO_DEFAULT_BOOT_STAGE2 and
PICO_DEFAULT_BOOT_STAGE2_FILE in the Bazel build.

* Allowlist some CMake-only options

* Support CXX configuration options in Bazel

* Move multiple_choice_flag.bzl

* Support all pico boards

* Support linking multiple stdio implementations

Changes the Bazel build so stdio implementations are no longer mutually
exclusive.

* Add PICO_BOOT_STAGE2_LINK_IMAGE

* Support PICO_CMSIS_PATH in Bazel

* Support PICO_USE_DEFAULT_MAX_PAGE_SIZE in Bazel

* Silence PICO_CMSIS_VENDOR and PICO_CMSIS_DEVICE differences

* Support PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS in Bazel

* Properly support version defines

* Support embedding binary info in Bazel

* Embed build type in binary

* Support different linker scripts in Bazel build

* Finish out missing PICO_BUILD_DEFINE in Bazel build

* Support PICO_NO_TARGET_NAME

* Reorganize initial configuration options in Bazel

Cleans up and reorganizes some of the initial configuration options
added to the Bazel build so everything is consistent.

* Add builds for pioasm and elf2uf2

* Use Python rules from rules_python

* Actually link in output formats in pioasm tool

* Make tools have public visibility

* Add UF2 Bazel aspect

* Add TODOs for pioasm/uf2 helpers

* Fix compile flag typo

* Update Bazel SDK configuration strings to match recent CMake changes

* Fix pico_divider test

* Clean up straggling TODOs

* Clarify pico_stdio_test compatibility

* Initial Bazel Pico W support

* Add new files from develop

* Clean up compatibility expressions in Bazel build

* Clean up rp2 constraint handling in Bazel

* More Bazel docs cleanup

* Format Bazel build files

* Consolidate transitions in the Pico SDK

* Make every _allowlist_function_transition explicit

* More docs cleanup

* Add a few missing defines

* Improve PICO_CONFIG_HEADER correctness in Bazel

* Minor docs clarifications
diff --git a/MODULE.bazel b/MODULE.bazel
index 394d414..883fad7 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -8,7 +8,7 @@
 # module will not ensure that the root Bazel module has that same version of
 # rules_cc. For that reason, this primarily acts as a FYI. You'll still need
 # to explicitly list this dependency in your own project's MODULE.bazel file.
-bazel_dep(name = "rules_cc", version = "0.0.10")
+bazel_dep(name = "rules_cc", version = "0.0.9")
 
 # rules_cc v0.0.10 is not yet cut, so manually pull in the desired version.
 # This does not apply to dependent projects, so it needs to be copied to your
@@ -63,6 +63,33 @@
     sha256 = "ac57109bba00d26ffa33312d5f334990ec9a9a4d82bf890ed8b825b4610d1da2",
 )
 
+# TODO: Provide btstack as a proper Bazel module.
+http_archive(
+    name = "btstack",
+    url = "https://github.com/bluekitchen/btstack/archive/72ef1732c954d938091467961e41f4aa9b976b34.zip",
+    strip_prefix = "btstack-72ef1732c954d938091467961e41f4aa9b976b34",
+    build_file = "//src/rp2_common/pico_btstack:btstack.BUILD",
+    sha256 = "f45d72b5d404dd2f8e311287de6f2ba3561fc8ae956737eeb611b277aadc2391",
+)
+
+# TODO: Provide btstack as a proper Bazel module.
+http_archive(
+    name = "cyw43-driver",
+    url = "https://github.com/georgerobotics/cyw43-driver/archive/8ef38a6d32c54f850bff8f189bdca19ded33792a.zip",
+    strip_prefix = "cyw43-driver-8ef38a6d32c54f850bff8f189bdca19ded33792a",
+    build_file = "//src/rp2_common/pico_cyw43_driver:cyw43-driver.BUILD",
+    sha256 = "0b44a19ea58537ee954357606cde5ed20c3a42be77adfebb07b7c0e4740f6228",
+)
+
+# TODO: Provide lwip as a proper Bazel module.
+http_archive(
+    name = "lwip",
+    url = "https://github.com/lwip-tcpip/lwip/archive/239918ccc173cb2c2a62f41a40fd893f57faf1d6.zip",
+    strip_prefix = "lwip-239918ccc173cb2c2a62f41a40fd893f57faf1d6",
+    build_file = "//src/rp2_common/pico_lwip:lwip.BUILD",
+    sha256 = "7ee9e02f2719c0422377e1fcce5a21716ca2e2e855cca56695f9ef7cb020e5dd",
+)
+
 register_toolchains(
     "//bazel/toolchain:arm_gcc_linux-x86_64",
     "//bazel/toolchain:arm_gcc_win-x86_64",
diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel
index b76e1aa..e4ab8f3 100644
--- a/bazel/BUILD.bazel
+++ b/bazel/BUILD.bazel
@@ -1,3 +1,5 @@
+load("@rules_python//python:defs.bzl", "py_binary")
+
 package(default_visibility = ["//visibility:public"])
 
 py_binary(
@@ -18,17 +20,38 @@
 # configuring these `label_flag`s:
 #
 #     # Specify the library that provides "pico_config_extra_headers.h"
-#     --@pico-sdk//bazel/config:pico_config_extra_headers=//my_proj:my_custom_headers
+#     --@pico-sdk//bazel/config:PICO_CONFIG_EXTRA_HEADER=//my_proj:my_custom_headers
 #
 #     # Specify the library that provides "pico_config_platform_headers.h"
-#     --@pico-sdk//bazel/config:pico_config_platform_headers=//my_proj:my_custom_platform_headers
+#     --@pico-sdk//bazel/config:PICO_CONFIG_PLATFORM_HEADER=//my_proj:my_custom_platform_headers
 cc_library(
     name = "generate_config_header",
     hdrs = ["include/pico/config_autogen.h"],
     includes = ["include"],
     visibility = ["//:__subpackages__"],
     deps = [
-        "//bazel/config:pico_config_extra_headers",
-        "//bazel/config:pico_config_platform_headers",
+        "//bazel/config:PICO_CONFIG_EXTRA_HEADER",
+        "//bazel/config:PICO_CONFIG_PLATFORM_HEADER",
     ],
 )
+
+genrule(
+    name = "empty_extra_headers_file",
+    outs = ["generated_extra_include/pico_config_extra_headers.h"],
+    cmd = "echo > $@",
+    cmd_bat = "copy NUL $@",
+    visibility = ["//visibility:private"],
+)
+
+cc_library(
+    name = "no_extra_headers",
+    hdrs = ["generated_extra_include/pico_config_extra_headers.h"],
+    includes = ["generated_extra_include"],
+    visibility = ["//visibility:private"],
+)
+
+# An empty stub, useful for label_flag flags that need to point to a library,
+# but for some purposes the library needs to be a no-op.
+cc_library(
+    name = "empty_cc_lib",
+)
diff --git a/bazel/README.md b/bazel/README.md
index db5078c..1db0c93 100644
--- a/bazel/README.md
+++ b/bazel/README.md
@@ -1,10 +1,4 @@
 # Bazel build
-The Bazel build for the Pico SDK is currently community-maintained, and should
-be considered an experimental work-in-progress. There are missing features,
-and you may encounter significant breakages with future versions.
-
-You are welcome and encouraged to file issues for any problems you encounter
-along the way.
 
 ## Using the Pico SDK in a Bazel project.
 
@@ -23,7 +17,7 @@
 # module will not ensure that the root Bazel module has that same version of
 # rules_cc. For that reason, this primarily acts as a FYI. You'll still need
 # to explicitly list this dependency in your own project's MODULE.bazel file.
-bazel_dep(name = "rules_cc", version = "0.0.10")
+bazel_dep(name = "rules_cc", version = "0.0.9")
 
 # rules_cc v0.0.10 is not yet cut, so manually pull in the desired version.
 # This does not apply to dependent projects, so it needs to be copied to your
@@ -65,45 +59,33 @@
 $ bazelisk build --platforms=@pico-sdk//bazel/platform:rp2040 //...
 ```
 
-## SDK configuration [experimental]
-These configuration options are a work in progress and may see significant
-breaking changes in future versions.
+## SDK configuration
+An exhaustive list of build system configuration options is available in
+`//bazel/config:BUILD.bazel`.
 
 ### Selecting a different board
-Currently there are three configurable flags for targeting a different board:
-1. `pico_config_extra_headers`: This should always point to a `cc_library `that
-   provides a `"pico_config_extra_headers.h"` header. You can configure this
-   by including a flag like the following in your build invocation:
-   ```
-   --@pico-sdk//bazel/config:pico_config_extra_headers=//path/to:custom_extra_headers
-   ```
-2. `pico_config_platform_headers`: This should always point to a `cc_library`
-   that provides a `"pico_config_platform_headers.h"` header.
-   ```
-   --@pico-sdk//bazel/config:pico_config_platform_headers=//path/to:custom_platform_headers
-   ```
-3. `pico_config_header`: This should point to a `cc_library` that sets all
-   necessary SDK defines. Most notably, `PICO_BOARD`, `PICO_CONFIG_HEADER`,
-   `PICO_ON_DEVICE`, `PICO_NO_HARDWARE`, and `PICO_BUILD`. See
-   `//src/boards:BUILD.bazel` for working examples. Any `defines` set on this
-   library will propagate to the rest of the Pico SDK. To set this configuration
-   option, pass a flag like the following in your Bazel build invocation:
-   ```
-   --@pico-sdk//bazel/config:pico_config_platform_headers=//path/to:pico_board_config
-   ```
+A different board can be selected specifying `--@pico-sdk//bazel/config:PICO_BOARD`:
+```console
+$ bazelisk build --platforms=//bazel/platform:rp2040 --@pico-sdk//bazel/config:PICO_BOARD=pico_w //...
+```
 
-### Selecting a stdio mode
-To select a different stdio mode, add it to your `platform` definition. For
-example:
-```python
-platform(
-    name = "rp2040",
-    constraint_values = [
-        "@pico-sdk//bazel/constraint:rp2040",
-        "@pico-sdk//bazel/constraint:stdio_usb", # Configures stdio_mode.
-        "@platforms//cpu:armv6-m",
-    ],
-)
+If you have a bespoke board definition, you can configure the Pico SDK to use it
+by pointing `--@pico-sdk//bazel/config:PICO_CONFIG_HEADER` to a `cc_library`
+that defines `PICO_BOARD` and either a `PICO_CONFIG_HEADER` define or a
+`pico/config_autogen.h` header. Make sure any required `includes`, `hdrs`, and
+`deps` are also provided.
+
+## Generating UF2 firmware images
+Creation of UF2 images can be done as explicit build steps on a per-binary
+rule basis, or through an aspect. Running a wildcard build with the
+`pico_uf2_aspect` enabled is the easiest way to create a UF2 for every ELF
+firmware image.
+
+```console
+$ bazel build --platforms=@pico-sdk//bazel/platform:rp2040 \
+    --aspects @pico-sdk//tools:uf2_aspect.bzl%pico_uf2_aspect \
+    --output_groups=+pico_uf2_files \
+    //...
 ```
 
 ## Building the Pico SDK itself
@@ -121,17 +103,14 @@
 $ bazelisk build --platforms=//bazel/platform:rp2040 //...
 ```
 
-**Note:** Since the Bazel build does not yet have any `cc_binary` rules with a
-`main()` function, there won't be any binaries to flash on your board. For now,
-this only builds the SDK as a collection of libraries.
-
 ## Known issues and limitations
-The Bazel build is currently experimental and incomplete. At this time, only the
-stock Pi Pico board is supported, and the only configuration options are
-changing the STDIO mode between UART and USB serial.
+The Bazel build for the Pico SDK is relatively new, but most features and
+configuration options available in the CMake build are also available in Bazel.
+You are welcome and encouraged to file issues for any problems and limitations
+you encounter along the way.
 
-Keep in mind the following limitations:
-* Pico-W is not yet supported.
-* Selecting an alternative board is not yet supported.
-* Nearly all preexisting CMake configuration options are not yet supported.
-* Targeting the host build of the Pico SDK is not yet supported.
+Currently, the following features are not supported:
+
+* "None" variants of pico_double, pico_float, and pico_printf are not yet
+  supported.
+* The pioasm parser cannot be built from source via Bazel.
diff --git a/bazel/config/BUILD.bazel b/bazel/config/BUILD.bazel
index 1376f22..50530ec 100644
--- a/bazel/config/BUILD.bazel
+++ b/bazel/config/BUILD.bazel
@@ -1,52 +1,159 @@
+load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "int_flag", "string_flag")
+
 package(default_visibility = ["//visibility:public"])
 
-# This should always point to a cc_library that provides
-# a "pico_config_extra_headers.h".
-label_flag(
-    name = "pico_config_extra_headers",
-    build_setting_default = ":no_extra_headers",
+# PICO_BAZEL_CONFIG: PICO_BOARD, The board name being built for, type=string, default=pico, group=build
+string_flag(
+    name = "PICO_BOARD",
+    build_setting_default = "pico",
 )
 
-# This should always point to a cc_library that provides
-# a "pico_config_platform_headers.h".
-label_flag(
-    name = "pico_config_platform_headers",
-    build_setting_default = ":no_platform_headers",
+# PICO_BAZEL_CONFIG: PICO_BARE_METAL, Flag to exclude anything except base headers from the build, type=bool, default=0, group=build
+bool_flag(
+    name = "PICO_BARE_METAL",
+    build_setting_default = False,
 )
 
-# This should always point to a cc_library that defines PICO_CONFIG_HEADER and
-# any other defines that should be applied when building the SDK.
+# PICO_BAZEL_CONFIG: PICO_NO_GC_SECTIONS, Disable -ffunction-sections -fdata-sections and --gc-sections, type=bool, default=0, advanced=true, group=pico_standard_link
+bool_flag(
+    name = "PICO_NO_GC_SECTIONS",
+    build_setting_default = False,
+)
+
+# PICO_BAZEL_CONFIG: PICO_DEFAULT_BOOT_STAGE2_FILE, Boot stage 2 file to use; this should point to a filegroup with the .S file to use, type=string, group=build
 label_flag(
-    name = "pico_config_header",
+    name = "PICO_DEFAULT_BOOT_STAGE2_FILE",
+    build_setting_default = "//src/rp2_common/boot_stage2:build_selected_boot2",
+)
+
+# PICO_BAZEL_CONFIG: PICO_DEFAULT_BOOT_STAGE2, Simpler alternative to specifying PICO_DEFAULT_BOOT_STAGE2_FILE where the file is src/rp2_common/boot_stage2/{PICO_DEFAULT_BOOT_STAGE2}.S, type=string, default=compile_time_choice, group=build
+string_flag(
+    name = "PICO_DEFAULT_BOOT_STAGE2",
+    build_setting_default = "compile_time_choice",
+)
+
+# PICO_BAZEL_CONFIG: PICO_BOOT_STAGE2_LINK_IMAGE, [Bazel only] The final boot_stage2 image target to link in. Use this to fully override/replace boot_stage2, default=@pico-sdk//src/rp2_common/boot_stage2:boot_stage2, group=build
+label_flag(
+    name = "PICO_BOOT_STAGE2_LINK_IMAGE",
+    build_setting_default = "//src/rp2_common/boot_stage2:boot_stage2",
+)
+
+# PICO_BAZEL_CONFIG: PICO_CXX_ENABLE_EXCEPTIONS, Enabled CXX exception handling, type=bool, default=0, group=pico_cxx_options
+bool_flag(
+    name = "PICO_CXX_ENABLE_EXCEPTIONS",
+    build_setting_default = False,
+)
+
+# PICO_BAZEL_CONFIG: PICO_CXX_ENABLE_RTTI, Enabled CXX rtti, type=bool, default=0, group=pico_cxx_options
+bool_flag(
+    name = "PICO_CXX_ENABLE_RTTI",
+    build_setting_default = False,
+)
+
+# PICO_BAZEL_CONFIG: PICO_CXX_ENABLE_CXA_ATEXIT, Enabled cxa-atexit, type=bool, default=0, group=pico_cxx_options
+bool_flag(
+    name = "PICO_CXX_ENABLE_CXA_ATEXIT",
+    build_setting_default = False,
+)
+
+# PICO_BAZEL_CONFIG: PICO_STDIO_UART, OPTION: Globally enable stdio UART, type=bool, default=1, group=pico_stdlib
+bool_flag(
+    name = "PICO_STDIO_UART",
+    build_setting_default = True,
+)
+
+# PICO_BAZEL_CONFIG: PICO_STDIO_USB, OPTION: Globally enable stdio USB, type=bool, default=0, group=pico_stdlib
+bool_flag(
+    name = "PICO_STDIO_USB",
+    build_setting_default = False,
+)
+
+# PICO_BAZEL_CONFIG: PICO_STDIO_SEMIHOSTING, OPTION: Globally enable stdio semihosting, type=bool, default=0, group=pico_stdlib
+bool_flag(
+    name = "PICO_STDIO_SEMIHOSTING",
+    build_setting_default = False,
+)
+
+# PICO_BAZEL_CONFIG: PICO_CMSIS_PATH, Label of a cc_ibrary providing CMSIS core, default="included stub CORE only impl", group=build
+label_flag(
+    name = "PICO_CMSIS_PATH",
+    build_setting_default = "//src/rp2_common/cmsis:cmsis_core",
+)
+
+# PICO_BAZEL_CONFIG: PICO_USE_DEFAULT_MAX_PAGE_SIZE, Don't shrink linker max page to 4096, type=bool, default=0, advanced=true, group=pico_standard_link
+bool_flag(
+    name = "PICO_USE_DEFAULT_MAX_PAGE_SIZE",
+    build_setting_default = False,
+)
+
+# PICO_BAZEL_CONFIG: PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS, Maximum number of milliseconds to wait during initialization for a CDC connection from the host (negative means indefinite) during initialization, type=int, default=0, group=pico_stdio_usb
+int_flag(
+    name = "PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS",
+    build_setting_default = 0,
+)
+
+# PICO_BAZEL_CONFIG: PICO_TINYUSB_LIB, [Bazel only] The library that provides TinyUSB, default=@tinyusb//:tinyusb, group=build
+label_flag(
+    name = "PICO_TINYUSB_LIB",
+    build_setting_default = "@tinyusb//:tinyusb",
+)
+
+# PICO_BAZEL_CONFIG: PICO_DEFAULT_BINARY_INFO, [Bazel only] The library that provides custom_pico_binary_info to link into all binaries, default=//src/rp2_common/pico_standard_link:default_binary_info, group=pico_standard_link
+label_flag(
+    name = "PICO_DEFAULT_BINARY_INFO",
+    build_setting_default = "//src/rp2_common/pico_standard_link:default_binary_info",
+)
+
+# PICO_BAZEL_CONFIG: PICO_BAZEL_BUILD_TYPE, The type of build (e.g. Debug or Release) to embed in binary info, type=string, default=pico, group=build
+string_flag(
+    name = "PICO_BAZEL_BUILD_TYPE",
+    build_setting_default = "Debug",
+)
+
+# PICO_BAZEL_CONFIG: PICO_DEFAULT_LINKER_SCRIPT, [Bazel only] The library that provides a linker script to link into all binaries, default=//src/rp2_common/pico_standard_link:default_linker_script, group=pico_standard_link
+label_flag(
+    name = "PICO_DEFAULT_LINKER_SCRIPT",
+    build_setting_default = "//src/rp2_common/pico_standard_link:default_linker_script",
+)
+
+# PICO_BAZEL_CONFIG: PICO_NO_TARGET_NAME, Don't define PICO_TARGET_NAME, type=bool, default=0, group=build
+bool_flag(
+    name = "PICO_NO_TARGET_NAME",
+    build_setting_default = False,
+)
+
+# PICO_BAZEL_CONFIG: PICO_CONFIG_EXTRA_HEADER, [Bazel only] The cc_library that provides "pico_config_extra_headers.h", default=//bazel:no_extra_headers, group=pico_base
+label_flag(
+    name = "PICO_CONFIG_EXTRA_HEADER",
+    build_setting_default = "//bazel:no_extra_headers",
+)
+
+# PICO_BAZEL_CONFIG: PICO_CONFIG_PLATFORM_HEADER, [Bazel only] The cc_library that provides "pico_config_platform_headers.h" and defines PICO_BOARD, default=//src/common/pico_base:default_platform_headers, group=pico_base
+label_flag(
+    name = "PICO_CONFIG_PLATFORM_HEADER",
     build_setting_default = "//src/boards:default",
 )
 
-genrule(
-    name = "empty_extra_headers_file",
-    outs = ["generated_include/pico_config_extra_headers.h"],
-    cmd = "echo > $@",
-    cmd_bat = "copy NUL $@",
-    visibility = ["//visibility:private"],
+# PICO_BAZEL_CONFIG: PICO_CONFIG_HEADER, [Bazel only] The cc_library that defines PICO_CONFIG_HEADER or pico/config_autogen.h and other SDK critical defines (overrides PICO_BOARD setting), default=//bazel:generate_config_header, group=pico_base
+label_flag(
+    name = "PICO_CONFIG_HEADER",
+    build_setting_default = "//bazel:generate_config_header",
 )
 
-genrule(
-    name = "empty_platform_headers_file",
-    outs = ["generated_include/pico_config_platform_headers.h"],
-    cmd = "echo > $@",
-    cmd_bat = "copy NUL $@",
-    visibility = ["//visibility:private"],
+# PICO_BAZEL_CONFIG: PICO_BTSTACK_CONFIG, [Bazel only] The cc_library that provides btstack_config.h, default=//bazel:empty_cc_lib, group=wireless
+label_flag(
+    name = "PICO_BTSTACK_CONFIG",
+    build_setting_default = "//bazel:empty_cc_lib",
 )
 
-cc_library(
-    name = "no_extra_headers",
-    hdrs = ["generated_include/pico_config_extra_headers.h"],
-    includes = ["generated_include"],
-    visibility = ["//visibility:private"],
+# PICO_BAZEL_CONFIG: PICO_LWIP_CONFIG, [Bazel only] The cc_library that provides lwipopts.h, default=//bazel:empty_cc_lib, group=wireless
+label_flag(
+    name = "PICO_LWIP_CONFIG",
+    build_setting_default = "//bazel:empty_cc_lib",
 )
 
-cc_library(
-    name = "no_platform_headers",
-    hdrs = ["generated_include/pico_config_platform_headers.h"],
-    includes = ["generated_include"],
-    visibility = ["//visibility:private"],
+# PICO_BAZEL_CONFIG: PICO_FREERTOS_LIB, [Bazel only] The cc_library that provides FreeRTOS, default=//bazel:empty_cc_lib, group=wireless
+label_flag(
+    name = "PICO_FREERTOS_LIB",
+    build_setting_default = "//bazel:empty_cc_lib",
 )
diff --git a/bazel/constraint/BUILD.bazel b/bazel/constraint/BUILD.bazel
index 8af93fa..e11d442 100644
--- a/bazel/constraint/BUILD.bazel
+++ b/bazel/constraint/BUILD.bazel
@@ -21,38 +21,87 @@
     constraint_setting = ":sdk_target",
 )
 
-# This constraint value is used to guide parts of the build that apply to all
-# rp2-class chips.
-config_setting(
-    name = "rp2",
-    constraint_values = [
-        ":rp2040",
-    ],
-)
-
-# This constraint setting guides Bazel's build file evaluation differences
-# across different stdio configurations (e.g. stdio_usb needs TinyUSB).
 constraint_setting(
-    name = "stdio_mode",
-    default_constraint_value = "stdio_uart",
+    name = "wireless_support",
+    default_constraint_value = "no_wireless",
 )
 
-# When this constraint value is active, stdio is built against a hardware UART.
 constraint_value(
-    name = "stdio_uart",
-    constraint_setting = ":stdio_mode",
+    name = "no_wireless",
+    constraint_setting = ":wireless_support",
 )
 
-# When this constraint value is active, stdio is built against TinyUSB-based
-# USB serial.
 constraint_value(
-    name = "stdio_usb",
-    constraint_setting = ":stdio_mode",
+    name = "cyw43_wireless",
+    constraint_setting = ":wireless_support",
 )
 
-# When this constraint value is active, stdio is built against an ARM
-# semihosting library.
-constraint_value(
-    name = "stdio_semihosting",
-    constraint_setting = ":stdio_mode",
+config_setting(
+    name = "is_pico_w",
+    flag_values = {"//bazel/config:PICO_BOARD": "pico_w"},
+)
+
+config_setting(
+    name = "pico_baremetal_enabled",
+    flag_values = {"//bazel/config:PICO_BARE_METAL": "True"},
+)
+
+config_setting(
+    name = "pico_no_gc_sections_enabled",
+    flag_values = {"//bazel/config:PICO_NO_GC_SECTIONS": "True"},
+)
+
+config_setting(
+    name = "pico_cxx_enable_exceptions_enabled",
+    flag_values = {"//bazel/config:PICO_CXX_ENABLE_EXCEPTIONS": "True"},
+)
+
+config_setting(
+    name = "pico_cxx_enable_rtti_enabled",
+    flag_values = {"//bazel/config:PICO_CXX_ENABLE_RTTI": "True"},
+)
+
+config_setting(
+    name = "pico_cxx_enable_cxa_atexit_enabled",
+    flag_values = {"//bazel/config:PICO_CXX_ENABLE_RTTI": "True"},
+)
+
+config_setting(
+    name = "pico_stdio_uart_enabled",
+    flag_values = {"//bazel/config:PICO_STDIO_UART": "True"},
+)
+
+config_setting(
+    name = "pico_stdio_usb_enabled",
+    flag_values = {"//bazel/config:PICO_STDIO_USB": "True"},
+)
+
+config_setting(
+    name = "pico_stdio_semihosting_enabled",
+    flag_values = {"//bazel/config:PICO_STDIO_SEMIHOSTING": "True"},
+)
+
+config_setting(
+    name = "pico_use_default_max_page_size_enabled",
+    flag_values = {"//bazel/config:PICO_USE_DEFAULT_MAX_PAGE_SIZE": "True"},
+)
+
+config_setting(
+    name = "pico_no_target_name_enabled",
+    flag_values = {"//bazel/config:PICO_NO_TARGET_NAME": "True"},
+)
+
+config_setting(
+    name = "pico_btstack_config_unset",
+    flag_values = {"//bazel/config:PICO_BTSTACK_CONFIG": "@pico-sdk//bazel:empty_cc_lib"},
+)
+
+config_setting(
+    name = "pico_lwip_config_unset",
+    flag_values = {"//bazel/config:PICO_LWIP_CONFIG": "@pico-sdk//bazel:empty_cc_lib"},
+)
+
+config_setting(
+    name = "pico_freertos_unset",
+    flag_values = {"//bazel/config:PICO_FREERTOS_LIB": "@pico-sdk//bazel:empty_cc_lib"},
 )
diff --git a/bazel/defs.bzl b/bazel/defs.bzl
new file mode 100644
index 0000000..0e9df1a
--- /dev/null
+++ b/bazel/defs.bzl
@@ -0,0 +1,116 @@
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+load("@rules_cc//cc:defs.bzl", "cc_library")
+
+def _pico_generate_pio_header_impl(ctx):
+    generated_headers = []
+    for f in ctx.files.srcs:
+        out = ctx.actions.declare_file(
+            "{}_pio_generated/{}.h".format(ctx.label.name, f.basename),
+        )
+        generated_headers.append(out)
+        ctx.actions.run(
+            executable = ctx.executable._pioasm_tool,
+            arguments = [
+                "-o",
+                "c-sdk",
+                f.path,
+                out.path,
+            ],
+            inputs = [f],
+            outputs = [out],
+        )
+
+    cc_ctx = cc_common.create_compilation_context(
+        headers = depset(direct = generated_headers),
+        includes = depset(direct = [generated_headers[0].dirname]),
+    )
+    return [
+        DefaultInfo(files = depset(direct = generated_headers)),
+        CcInfo(compilation_context = cc_ctx),
+    ]
+
+pico_generate_pio_header = rule(
+    implementation = _pico_generate_pio_header_impl,
+    doc = """Generates a .h header file for each listed pio source.
+
+Each source file listed in `srcs` will be available as `[pio file name].h` on
+the include path if you depend on this rule from a `cc_library`.
+
+pico_generate_pio_header(
+    name = "my_fun_pio",
+    srcs = ["my_fun_pio.pio"],
+)
+
+# This library can #include "my_fun_pio.pio.h".
+cc_library(
+    name = "libfoo",
+    deps = [":my_fun_pio"],
+    srcs = ["libfoo.c"],
+)
+""",
+    attrs = {
+        "srcs": attr.label_list(mandatory = True, allow_files = True),
+        "_pioasm_tool": attr.label(
+            default = "@pico-sdk//tools/pioasm:pioasm",
+            cfg = "exec",
+            executable = True,
+        ),
+    },
+    provides = [CcInfo],
+)
+
+# Because the syntax for target_compatible_with when used with config_setting
+# rules is both confusing and verbose, provide some helpers that make it much
+# easier and clearer to express compatibility.
+#
+# Context: https://github.com/bazelbuild/bazel/issues/12614
+
+def compatible_with_config(config_label):
+    """Expresses compatibility with a config_setting."""
+    return select({
+        config_label: [],
+        "//conditions:default": ["@platforms//:incompatible"],
+    })
+
+def incompatible_with_config(config_label):
+    """Expresses incompatibility with a config_setting."""
+    return select({
+        config_label: ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    })
+
+def compatible_with_rp2():
+    """Expresses a rule is compatible with the rp2 family."""
+    return incompatible_with_config("//bazel/constraint:host")
+
+def compatible_with_pico_w():
+    """Expresses a rule is compatible a Pico W."""
+    return select({
+        "@pico-sdk//bazel/constraint:cyw43_wireless": [],
+        "@pico-sdk//bazel/constraint:is_pico_w": [],
+        "//conditions:default": ["@platforms//:incompatible"],
+    })
+
+def pico_board_config(name, platform_includes, **kwargs):
+    """A helper macro for declaring a Pico board to use with PICO_CONFIG_HEADER.
+
+    This generates pico_config_platform_headers.h using the list of
+    includes provided in `platform_includes`, and the final artifact is
+    a cc_library that you can configure //bazel/config:PICO_CONFIG_HEADER to
+    point to.
+    """
+    _hdr_dir = "{}_generated_includes".format(name)
+    _hdr_path = "{}/pico_config_platform_headers.h".format(_hdr_dir)
+    write_file(
+        name = "{}_platform_headers_file".format(name),
+        out = _hdr_path,
+        content = ['#include "{}"'.format(inc) for inc in platform_includes],
+    )
+    kwargs.setdefault("hdrs", [])
+    kwargs["hdrs"].append(_hdr_path)
+    kwargs.setdefault("includes", [])
+    kwargs["includes"].append(_hdr_dir)
+    cc_library(
+        name = name,
+        **kwargs
+    )
diff --git a/bazel/include/pico/config_autogen.h b/bazel/include/pico/config_autogen.h
index 027dfe2..e1bd61e 100644
--- a/bazel/include/pico/config_autogen.h
+++ b/bazel/include/pico/config_autogen.h
@@ -2,11 +2,11 @@
 // is checked in directly.
 //
 // You can change what is included by configuring these `label_flag`s:
-//   --@pico-sdk//bazel/config:pico_config_extra_headers=//my_proj:my_custom_headers
-//   --@pico-sdk//bazel/config:pico_config_platform_headers=//my_proj:my_custom_headers
+//   --@pico-sdk//bazel/config:PICO_CONFIG_EXTRA_HEADER=//my_proj:my_custom_headers
+//   --@pico-sdk//bazel/config:PICO_CONFIG_PLATFORM_HEADER=//my_proj:my_custom_headers
 
-// This header must be provided by //bazel/config:pico_config_extra_headers:
+// This header must be provided by //bazel/config:PICO_CONFIG_EXTRA_HEADER:
 #include "pico_config_extra_headers.h"
 
-// This header must be provided by //bazel/config:pico_config_platform_headers:
+// This header must be provided by //bazel/config:PICO_CONFIG_PLATFORM_HEADER:
 #include "pico_config_platform_headers.h"
diff --git a/bazel/toolchain/BUILD.bazel b/bazel/toolchain/BUILD.bazel
index a246e0d..775ee18 100644
--- a/bazel/toolchain/BUILD.bazel
+++ b/bazel/toolchain/BUILD.bazel
@@ -2,6 +2,9 @@
 load("@rules_cc//cc/toolchains:args_list.bzl", "cc_args_list")
 load("@rules_cc//cc/toolchains:feature.bzl", "cc_feature")
 load("@rules_cc//cc/toolchains:toolchain.bzl", "cc_toolchain")
+load("configurable_feature.bzl", "configurable_toolchain_feature")
+
+package(default_visibility = ["//visibility:public"])
 
 cc_args(
     name = "cortex-m0",
@@ -44,6 +47,43 @@
     ],
 )
 
+configurable_toolchain_feature(
+    name = "gc_sections",
+    copts = [
+        "-ffunction-sections",
+        "-fdata-sections",
+    ],
+    disable_if = "//bazel/constraint:pico_no_gc_sections_enabled",
+    linkopts = ["-Wl,--gc-sections"],
+)
+
+configurable_toolchain_feature(
+    name = "cxx_no_exceptions",
+    cxxopts = [
+        "-fno-exceptions",
+        "-fno-unwind-tables",
+    ],
+    disable_if = "//bazel/constraint:pico_cxx_enable_exceptions_enabled",
+)
+
+configurable_toolchain_feature(
+    name = "cxx_no_rtti",
+    cxxopts = ["-fno-rtti"],
+    disable_if = "//bazel/constraint:pico_cxx_enable_rtti_enabled",
+)
+
+configurable_toolchain_feature(
+    name = "cxx_no_cxa_atexit",
+    cxxopts = ["-fno-use-cxa-atexit"],
+    disable_if = "//bazel/constraint:pico_cxx_enable_cxa_atexit_enabled",
+)
+
+configurable_toolchain_feature(
+    name = "override_max_page_size",
+    disable_if = "//bazel/constraint:pico_use_default_max_page_size_enabled",
+    linkopts = ["-Wl,-z,max-page-size=4096"],
+)
+
 # TODO: Make this shim unnecessary.
 cc_args_list(
     name = "all_opt_debug_args",
@@ -134,6 +174,11 @@
     toolchain_features = [
         "@pico-sdk//bazel/toolchain:legacy_features",
         "@pico-sdk//bazel/toolchain:override_debug",
+        "@pico-sdk//bazel/toolchain:gc_sections",
+        "@pico-sdk//bazel/toolchain:cxx_no_exceptions",
+        "@pico-sdk//bazel/toolchain:cxx_no_rtti",
+        "@pico-sdk//bazel/toolchain:cxx_no_cxa_atexit",
+        "@pico-sdk//bazel/toolchain:override_max_page_size",
     ],
 ) for host_os, host_cpu in HOSTS]
 
diff --git a/bazel/toolchain/configurable_feature.bzl b/bazel/toolchain/configurable_feature.bzl
new file mode 100644
index 0000000..019c3df
--- /dev/null
+++ b/bazel/toolchain/configurable_feature.bzl
@@ -0,0 +1,54 @@
+load("@rules_cc//cc/toolchains:args.bzl", "cc_args")
+load("@rules_cc//cc/toolchains:args_list.bzl", "cc_args_list")
+load("@rules_cc//cc/toolchains:feature.bzl", "cc_feature")
+
+def configurable_toolchain_feature(name, copts = [], cxxopts = [], linkopts = [], enable_if = None, disable_if = None):
+    if enable_if != None and disable_if != None:
+        fail("Cannot specify both enable_if and disable_if")
+    if enable_if == None and disable_if == None:
+        fail("Must specify at least one of enable_if and disable_if")
+    if enable_if == None:
+        enable_if = "//conditions:default"
+    if disable_if == None:
+        disable_if = "//conditions:default"
+
+    all_args = []
+
+    if copts:
+        cc_args(
+            name = name + "_cc_args",
+            actions = ["@rules_cc//cc/toolchains/actions:compile_actions"],
+            args = copts,
+        )
+        all_args.append(name + "_cc_args")
+
+    if cxxopts:
+        cc_args(
+            name = name + "_cxx_args",
+            actions = ["@rules_cc//cc/toolchains/actions:cpp_compile_actions"],
+            args = cxxopts,
+        )
+        all_args.append(name + "_cxx_args")
+
+    if linkopts:
+        cc_args(
+            name = name + "_link_args",
+            actions = ["@rules_cc//cc/toolchains/actions:link_actions"],
+            args = linkopts,
+        )
+        all_args.append(name + "_link_args")
+
+    cc_args_list(
+        name = name + "_args",
+        args = all_args,
+    )
+
+    cc_feature(
+        name = name,
+        feature_name = name,
+        args = [":{}_args".format(name)],
+        enabled = select({
+            disable_if: False,
+            enable_if: True,
+        }),
+    )
diff --git a/bazel/util/multiple_choice_flag.bzl b/bazel/util/multiple_choice_flag.bzl
new file mode 100644
index 0000000..1806e40
--- /dev/null
+++ b/bazel/util/multiple_choice_flag.bzl
@@ -0,0 +1,38 @@
+def declare_flag_choices(flag, choices):
+    """Declares a `config_setting` for each known choice for the provided flag.
+
+    The name of each config setting uses the name of the `config_setting` is:
+        [flag label name]_[choice]
+
+    This can be used with select_choice() to map `config_setting`s to values.
+
+    Args:
+      flag: The flag that guides the declared `config_setting`s.
+      pkg: The package that declare_flag_choices() was declared in.
+      choice_map: A mapping of distinct choices to
+    """
+    flag_name = flag.split(":")[1]
+    [
+        native.config_setting(
+            name = "{}_{}".format(flag_name, choice),
+            flag_values = {flag: choice},
+        )
+        for choice in choices
+    ]
+
+def flag_choice(flag, pkg, choice_map):
+    """Creates a `select()` based on choices declared by `declare_choices()`.
+
+    Args:
+      flag: The flag that guides the select.
+      pkg: The package that `declare_flag_choices()` was called in.
+      choice_map: A mapping of distinct choices to the final intended value.
+    """
+    return {
+        "{}:{}_{}".format(
+            pkg.split(":")[0],
+            flag.split(":")[1],
+            choice,
+        ): val
+        for choice, val in choice_map.items()
+    }
diff --git a/bazel/util/sdk_define.bzl b/bazel/util/sdk_define.bzl
new file mode 100644
index 0000000..cd9b499
--- /dev/null
+++ b/bazel/util/sdk_define.bzl
@@ -0,0 +1,43 @@
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
+
+def _pico_sdk_define_impl(ctx):
+    val = ctx.attr.from_flag[BuildSettingInfo].value
+
+    if type(val) == "string":
+        # Strings need quotes.
+        val = "\"{}\"".format(val)
+    elif type(val) == "bool":
+        # Convert bools to 0 or 1.
+        val = 1 if val else 0
+    cc_ctx = cc_common.create_compilation_context(
+        defines = depset(
+            direct = ["{}={}".format(ctx.attr.define_name, val)],
+        ),
+    )
+    return [CcInfo(compilation_context = cc_ctx)]
+
+pico_sdk_define = rule(
+    implementation = _pico_sdk_define_impl,
+    doc = """A simple rule that offers a skylib flag as a define.
+
+These can be listed in the `deps` attribute of a `cc_library` to get access
+to the value of a define.
+
+Example:
+
+    bool_flag(
+        name = "my_flag",
+        build_setting_default = False,
+    )
+
+    pico_sdk_define(
+        name = "flag_define",
+        define_name = "MY_FLAG_DEFINE",
+        from_flag = ":my_flag",
+    )
+""",
+    attrs = {
+        "define_name": attr.string(mandatory = True),
+        "from_flag": attr.label(mandatory = True),
+    },
+)
diff --git a/bazel/util/transition.bzl b/bazel/util/transition.bzl
index c768c0a..d0baf0d 100644
--- a/bazel/util/transition.bzl
+++ b/bazel/util/transition.bzl
@@ -1,38 +1,57 @@
-# A transition in Bazel is a way to force changes to the way the build is
-# evaluated for all dependencies of a given rule.
-#
-# Imagine the following simple dependency graph:
-#
-#     ->: depends on
-#     a -> b -> c
-#
-# Normally, if you set `defines` on a, they couldn't apply to b or c because
-# they are dependencies of a. There's no way for b or c to know about a's
-# settings, because they don't even know a exists!
-#
-# We can fix this via a transition! If we put a transition in front of `a`
-# that sets --copts=-DFOO=42, we're telling Bazel to build a and all of its
-# dependencies under that configuration.
-#
-# Note: Flags must be referenced as e.g. `//command_line_option:copt` in
-# transitions.
-#
-# `declare_transition()` eliminates the frustrating amount of boilerplate. All
-# you need to do is provide a set of attrs, and then a `flag_overrides`
-# dictionary that tells `declare_transition()` which attrs to pull flag values
-# from. The common `src` attr tells the transition which build rule to apply
-# the transition to.
-def declare_transtion(attrs, flag_overrides, executable = True):
-    def _flag_override_impl(settings, attrs):
-        return {
-            key: str(getattr(attrs, value))
-            for key, value in flag_overrides.items()
-        }
+def declare_transtion(attrs, flag_overrides = None, append_to_flags = None, executable = True):
+    """A helper that drastically simplifies declaration of a transition.
 
+    A transition in Bazel is a way to force changes to the way the build is
+    evaluated for all dependencies of a given rule.
+
+    Imagine the following simple dependency graph:
+
+        ->: depends on
+        a -> b -> c
+
+    Normally, if you set `defines` on a, they couldn't apply to b or c because
+    they are dependencies of a. There's no way for b or c to know about a's
+    settings, because they don't even know a exists!
+
+    We can fix this via a transition! If we put a transition in front of `a`
+    that sets --copts=-DFOO=42, we're telling Bazel to build a and all of its
+    dependencies under that configuration.
+
+    Note: Flags must be referenced as e.g. `//command_line_option:copt` in
+    transitions.
+
+    `declare_transition()` eliminates the frustrating amount of boilerplate. All
+    you need to do is provide a set of attrs, and then a `flag_overrides`
+    dictionary that tells `declare_transition()` which attrs to pull flag values
+    from. The common `src` attr tells the transition which build rule to apply
+    the transition to.
+    """
+
+    def _flag_override_impl(settings, attrs):
+        final_overrides = {}
+        if flag_overrides != None:
+            final_overrides = {
+                key: str(getattr(attrs, value))
+                for key, value in flag_overrides.items()
+            }
+        if append_to_flags != None:
+            for flag, field in append_to_flags.items():
+                accumulated_flags = final_overrides.get(flag, settings.get(flag, []))
+                accumulated_flags.extend(
+                    [str(val) for val in getattr(attrs, field)],
+                )
+                final_overrides[flag] = accumulated_flags
+        return final_overrides
+
+    output_flags = []
+    if flag_overrides != None:
+        output_flags.extend(flag_overrides.keys())
+    if append_to_flags != None:
+        output_flags.extend(append_to_flags.keys())
     _transition = transition(
         implementation = _flag_override_impl,
-        inputs = [],
-        outputs = flag_overrides.keys(),
+        inputs = append_to_flags.keys() if append_to_flags != None else [],
+        outputs = output_flags,
     )
 
     def _symlink_artifact_impl(ctx):
@@ -56,15 +75,18 @@
                 executable = executable,
                 mandatory = True,
             ),
-            "_allowlist_function_transition": attr.label(
-                default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
-            ),
         } | attrs,
     )
 
+# This transition is applied before building the boot_stage2 image.
 rp2040_bootloader_binary = declare_transtion(
     attrs = {
-        "_malloc": attr.label(default = "//src/rp2_common/boot_stage2:no_malloc"),
+        "_malloc": attr.label(default = "//bazel:empty_cc_lib"),
+        # This could be shared, but we don't in order to make it clearer that
+        # a transition is in use.
+        "_allowlist_function_transition": attr.label(
+            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+        ),
     },
     flag_overrides = {
         # We don't want --custom_malloc to ever apply to the bootloader, so
@@ -72,3 +94,37 @@
         "//command_line_option:custom_malloc": "_malloc",
     },
 )
+
+# This transition sets SDK configuration options required to build test binaries
+# for the kitchen_sink suite of tests.
+kitchen_sink_test_binary = declare_transtion(
+    attrs = {
+        "bt_stack_config": attr.label(mandatory = True),
+        "lwip_config": attr.label(mandatory = True),
+        # This could be shared, but we don't in order to make it clearer that
+        # a transition is in use.
+        "_allowlist_function_transition": attr.label(
+            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+        ),
+    },
+    flag_overrides = {
+        "@pico-sdk//bazel/config:PICO_BTSTACK_CONFIG": "bt_stack_config",
+        "@pico-sdk//bazel/config:PICO_LWIP_CONFIG": "lwip_config",
+    },
+)
+
+# This is a general purpose transition that applies the listed copt flags to
+# all transitive dependencies.
+extra_copts_for_all_deps = declare_transtion(
+    attrs = {
+        "extra_copts": attr.string_list(),
+        # This could be shared, but we don't in order to make it clearer that
+        # a transition is in use.
+        "_allowlist_function_transition": attr.label(
+            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+        ),
+    },
+    append_to_flags = {
+        "//command_line_option:copt": "extra_copts",
+    },
+)
diff --git a/src/boards/BUILD.bazel b/src/boards/BUILD.bazel
index 31d74b9..ba0dfa2 100644
--- a/src/boards/BUILD.bazel
+++ b/src/boards/BUILD.bazel
@@ -1,105 +1,101 @@
+load("//bazel:defs.bzl", "pico_board_config")
+load("//bazel/util:multiple_choice_flag.bzl", "declare_flag_choices", "flag_choice")
+
 package(default_visibility = ["//visibility:public"])
 
-# TODO: Add a macro/helper to support the rest of the boards.
-cc_library(
-    name = "unsupported_boards",
-    srcs = [
-        "include/boards/adafruit_feather_rp2040.h",
-        "include/boards/adafruit_itsybitsy_rp2040.h",
-        "include/boards/adafruit_kb2040.h",
-        "include/boards/adafruit_macropad_rp2040.h",
-        "include/boards/adafruit_qtpy_rp2040.h",
-        "include/boards/adafruit_trinkey_qt2040.h",
-        "include/boards/arduino_nano_rp2040_connect.h",
-        "include/boards/datanoisetv_rp2040_dsp.h",
-        "include/boards/eetree_gamekit_rp2040.h",
-        "include/boards/garatronic_pybstick26_rp2040.h",
-        "include/boards/melopero_shake_rp2040.h",
-        "include/boards/none.h",
-        "include/boards/nullbits_bit_c_pro.h",
-        "include/boards/pico_w.h",
-        "include/boards/pimoroni_badger2040.h",
-        "include/boards/pimoroni_interstate75.h",
-        "include/boards/pimoroni_keybow2040.h",
-        "include/boards/pimoroni_motor2040.h",
-        "include/boards/pimoroni_pga2040.h",
-        "include/boards/pimoroni_picolipo_16mb.h",
-        "include/boards/pimoroni_picolipo_4mb.h",
-        "include/boards/pimoroni_picosystem.h",
-        "include/boards/pimoroni_plasma2040.h",
-        "include/boards/pimoroni_servo2040.h",
-        "include/boards/pimoroni_tiny2040.h",
-        "include/boards/pimoroni_tiny2040_2mb.h",
-        "include/boards/pololu_3pi_2040_robot.h",
-        "include/boards/seeed_xiao_rp2040.h",
-        "include/boards/solderparty_rp2040_stamp.h",
-        "include/boards/solderparty_rp2040_stamp_carrier.h",
-        "include/boards/solderparty_rp2040_stamp_round_carrier.h",
-        "include/boards/sparkfun_micromod.h",
-        "include/boards/sparkfun_promicro.h",
-        "include/boards/sparkfun_thingplus.h",
-        "include/boards/vgaboard.h",
-        "include/boards/waveshare_rp2040_lcd_0.96.h",
-        "include/boards/waveshare_rp2040_lcd_1.28.h",
-        "include/boards/waveshare_rp2040_one.h",
-        "include/boards/waveshare_rp2040_plus_16mb.h",
-        "include/boards/waveshare_rp2040_plus_4mb.h",
-        "include/boards/waveshare_rp2040_zero.h",
-        "include/boards/wiznet_w5100s_evb_pico.h",
-    ],
-    includes = ["include"],
-    visibility = ["//visibility:private"],
+# Known board choices:
+BOARD_CHOICES = [
+    "adafruit_feather_rp2040",
+    "adafruit_itsybitsy_rp2040",
+    "adafruit_kb2040",
+    "adafruit_macropad_rp2040",
+    "adafruit_qtpy_rp2040",
+    "adafruit_trinkey_qt2040",
+    "arduino_nano_rp2040_connect",
+    "datanoisetv_rp2040_dsp",
+    "eetree_gamekit_rp2040",
+    "garatronic_pybstick26_rp2040",
+    "melopero_shake_rp2040",
+    "none",
+    "nullbits_bit_c_pro",
+    "pico",
+    "pico_w",
+    "pimoroni_badger2040",
+    "pimoroni_interstate75",
+    "pimoroni_keybow2040",
+    "pimoroni_motor2040",
+    "pimoroni_pga2040",
+    "pimoroni_picolipo_16mb",
+    "pimoroni_picolipo_4mb",
+    "pimoroni_picosystem",
+    "pimoroni_plasma2040",
+    "pimoroni_servo2040",
+    "pimoroni_tiny2040",
+    "pimoroni_tiny2040_2mb",
+    "pololu_3pi_2040_robot",
+    "seeed_xiao_rp2040",
+    "solderparty_rp2040_stamp",
+    "solderparty_rp2040_stamp_carrier",
+    "solderparty_rp2040_stamp_round_carrier",
+    "sparkfun_micromod",
+    "sparkfun_promicro",
+    "sparkfun_thingplus",
+    "vgaboard",
+    "waveshare_rp2040_lcd_0.96",
+    "waveshare_rp2040_lcd_1.28",
+    "waveshare_rp2040_one",
+    "waveshare_rp2040_plus_16mb",
+    "waveshare_rp2040_plus_4mb",
+    "waveshare_rp2040_zero",
+    "wiznet_w5100s_evb_pico",
+    "cytron_maker_pi_rp2040",
+    "metrotech_xerxes_rp2040",
+    "pololu_zumo_2040_robot",
+    "weact_studio_rp2040_16mb",
+    "weact_studio_rp2040_2mb",
+    "weact_studio_rp2040_4mb",
+    "weact_studio_rp2040_8mb",
+]
+
+BOARD_CHOICE_FILES = ["include/boards/" + c + ".h" for c in BOARD_CHOICES]
+
+BOARD_CHOICE_MAP = {c: [":{}".format(c)] for c in BOARD_CHOICES}
+
+# PICO_BUILD_DEFINE: PICO_BOARD, Name of board, type=string, default=CMake PICO_BOARD variable, group=pico_base
+[
+    pico_board_config(
+        name = board,
+        hdrs = BOARD_CHOICE_FILES,
+        defines = [
+            'PICO_BOARD=\\"{}\\"'.format(board),
+        ],
+        includes = ["include"],
+        platform_includes = [
+            "cmsis/rename_exceptions.h",
+            "boards/{}.h".format(board),
+        ],
+        deps = ["//src/rp2_common/cmsis:rename_exceptions"],
+    )
+    for board in BOARD_CHOICES
+]
+
+# Creates a config_setting for each known board option with the name:
+#     PICO_BOARD_[choice]
+declare_flag_choices(
+    "//bazel/config:PICO_BOARD",
+    BOARD_CHOICES,
 )
 
 cc_library(
-    name = "stdio_defines",
-    defines = select({
-        "//bazel/constraint:stdio_semihosting": ["LIB_PICO_STDIO_SEMIHOSTING=1"],
-        "//bazel/constraint:stdio_uart": ["LIB_PICO_STDIO_UART=1"],
-        "//bazel/constraint:stdio_usb": ["LIB_PICO_STDIO_USB=1"],
-    }),
-)
-
-cc_library(
-    name = "common_board_defines",
-    defines = [
-        "PICO_ON_DEVICE=1",
-        "PICO_NO_HARDWARE=0",
-        "PICO_BUILD=1",
-    ],
-)
-
-cc_library(
-    name = "pico",
-    hdrs = ["include/boards/pico.h"],
-    defines = [
-        'PICO_BOARD=\\"rp2040\\"',
-        'PICO_CONFIG_HEADER="boards/pico.h"',
-    ],
-    includes = ["include"],
-    deps = [
-        ":common_board_defines",
-        ":stdio_defines",
-    ],
-)
-
-cc_library(
-    name = "host",
-    hdrs = ["include/boards/none.h"],
-    defines = [
-        'PICO_BOARD=\\"none\\"',
-        'PICO_CONFIG_HEADER="boards/none.h"',
-        "PICO_ON_DEVICE=0",
-        "PICO_NO_HARDWARE=1",
-        "PICO_BUILD=1",
-    ],
-    includes = ["include"],
-)
-
-alias(
     name = "default",
-    actual = select({
-        "//bazel/constraint:rp2": ":pico",
-        "//bazel/constraint:host": ":host",
-    }),
+    deps = select(
+        flag_choice(
+            "//bazel/config:PICO_BOARD",
+            ":__pkg__",
+            BOARD_CHOICE_MAP,
+        ) | {
+            "//bazel/constraint:host": [":none"],
+            "//conditions:default": [":none"],
+        },
+    ),
 )
diff --git a/src/common/pico_base/BUILD.bazel b/src/common/pico_base/BUILD.bazel
index dd8cf99..4204efd 100644
--- a/src/common/pico_base/BUILD.bazel
+++ b/src/common/pico_base/BUILD.bazel
@@ -2,14 +2,31 @@
 
 package(default_visibility = ["//visibility:public"])
 
+# PICO_BAZEL_CONFIG: PICO_SDK_VERSION_STRING, SDK version, type=string, group=pico_base
+PICO_SDK_VERSION_STRING = module_version() if module_version() != None else "0.0.1-WORKSPACE"
+
+_version_parts = PICO_SDK_VERSION_STRING.split(".")
+
+# PICO_BAZEL_CONFIG: PICO_SDK_VERSION_MAJOR, SDK major version number, type=int, group=pico_base
+PICO_SDK_VERSION_MAJOR = int(_version_parts[0])
+
+# PICO_BAZEL_CONFIG: PICO_SDK_VERSION_MINOR, SDK minor version number, type=int, group=pico_base
+PICO_SDK_VERSION_MINOR = int(_version_parts[1])
+
+_revision_parts = _version_parts[2].split("-")
+
+# PICO_BAZEL_CONFIG: PICO_SDK_VERSION_REVISION, SDK version revision, type=int, group=pico_base
+PICO_SDK_VERSION_REVISION = int(_revision_parts[0])
+
+# PICO_BAZEL_CONFIG: PICO_SDK_VERSION_PRE_RELEASE_ID, optional SDK pre-release version identifier, type=string, group=pico_base
+PICO_SDK_VERSION_PRE_RELEASE_ID = _revision_parts[1] if len(_revision_parts) > 1 else None
+
 run_binary(
     name = "version_header",
     srcs = ["include/pico/version.h.in"],
     outs = ["generated_include/pico/version.h"],
     args = [
-        "--version-string={}".format(
-            module_version() if module_version() != None else "0.0.1-WORKSPACE",
-        ),
+        "--version-string={}".format(PICO_SDK_VERSION_STRING),
         "--template=$(location include/pico/version.h.in)",
         "--output=$(location generated_include/pico/version.h)",
     ],
@@ -17,9 +34,20 @@
     visibility = ["//visibility:private"],
 )
 
+# PICO_BUILD_DEFINE: PICO_SDK_VERSION_MAJOR, SDK major version number, type=int, group=pico_base
+# PICO_BUILD_DEFINE: PICO_SDK_VERSION_MINOR, SDK minor version number, type=int, group=pico_base
+# PICO_BUILD_DEFINE: PICO_SDK_VERSION_REVISION, SDK version revision, type=int, group=pico_base
+# PICO_BUILD_DEFINE: PICO_SDK_VERSION_PRE_RELEASE_ID, optional SDK pre-release version identifier, type=string, group=pico_base
+# PICO_BUILD_DEFINE: PICO_SDK_VERSION_STRING, SDK version, type=string, group=pico_base
 cc_library(
     name = "version",
     hdrs = ["generated_include/pico/version.h"],
+    defines = [
+        'PICO_SDK_VERSION_STRING=\\"{}\\"'.format(PICO_SDK_VERSION_STRING),
+        "PICO_SDK_VERSION_MAJOR={}".format(PICO_SDK_VERSION_MAJOR),
+        'PICO_SDK_VERSION_MINOR={}"'.format(PICO_SDK_VERSION_MINOR),
+        'PICO_SDK_VERSION_REVISION={}"'.format(PICO_SDK_VERSION_REVISION),
+    ] + [] if PICO_SDK_VERSION_PRE_RELEASE_ID == None else ['PICO_SDK_VERSION_PRE_RELEASE_ID=\\"{}\\"'.format(PICO_SDK_VERSION_PRE_RELEASE_ID)],
     includes = ["generated_include"],
 )
 
@@ -27,7 +55,7 @@
     name = "platform_defs",
     actual = select({
         "//bazel/constraint:host": "//src/host/pico_platform:platform_defs",
-        "//bazel/constraint:rp2": "//src/rp2_common/pico_platform:platform_defs",
+        "//conditions:default": "//src/rp2_common/pico_platform:platform_defs",
     }),
 )
 
@@ -35,7 +63,28 @@
     name = "pico_platform",
     actual = select({
         "//bazel/constraint:host": "//src/host/pico_platform:pico_platform",
-        "//bazel/constraint:rp2": "//src/rp2_common/pico_platform:pico_platform",
+        "//conditions:default": "//src/rp2_common/pico_platform:pico_platform",
+    }),
+)
+
+# PICO_BAZEL_CONFIG: PICO_NO_HARDWARE, OPTION: Whether the build is not targeting an RP2040 device, type=bool, default=1 when PICO_PLATFORM is host, 0 otherwise, group=build
+# PICO_BUILD_DEFINE: PICO_NO_HARDWARE, Whether the build is not targeting an RP2040 device, type=bool, default=1 when PICO_PLATFORM is host, 0 otherwise, group=build
+# PICO_BAZEL_CONFIG: PICO_ON_DEVICE, OPTION: Whether the build is targeting an RP2040 device, type=bool, default=0 when PICO_PLATFORM is host, 1 otherwise, group=build
+# PICO_BUILD_DEFINE: PICO_ON_DEVICE, Whether the build is targeting an RP2040 device, type=bool, default=0 when PICO_PLATFORM is host, 1 otherwise, group=build
+# PICO_BUILD is undocumented in CMake.
+cc_library(
+    name = "common_sdk_defines",
+    defines = select({
+        "//bazel/constraint:host": [
+            "PICO_ON_DEVICE=0",
+            "PICO_NO_HARDWARE=1",
+            "PICO_BUILD=1",
+        ],
+        "//conditions:default": [
+            "PICO_ON_DEVICE=1",
+            "PICO_NO_HARDWARE=0",
+            "PICO_BUILD=1",
+        ],
     }),
 )
 
@@ -61,6 +110,7 @@
         "//src/common/pico_sync:__pkg__",
         "//src/common/pico_time:__pkg__",
         "//src/common/pico_util:__pkg__",
+        "//src/host/hardware_timer:__pkg__",
         "//src/host/pico_platform:__pkg__",
         "//src/rp2_common/boot_stage2:__pkg__",
         "//src/rp2_common/hardware_claim:__pkg__",
@@ -81,23 +131,24 @@
         "//src/rp2_common/pico_standard_link:__pkg__",
     ],
     deps = [
+        ":common_sdk_defines",
         ":version",
-        "//bazel:generate_config_header",
-        "//bazel/config:pico_config_header",
+        "//bazel/config:PICO_CONFIG_HEADER",
     ],
 )
 
 cc_library(
     name = "pico_base",
+    implementation_deps = select({
+        "//bazel/constraint:host": [],
+        "//conditions:default": [
+            "//src/rp2_common/pico_platform:platform_link_deps",
+        ],
+    }),
     deps = [
         # :pico_platform creates circular dependencies, so break them
         # via an intermediate.
         ":pico_platform",
         ":pico_base_interface",
-        "//src/rp2_common/cmsis:cmsis_core",
-        "//src/rp2_common/boot_stage2",
-        "//src/rp2_common/pico_bootrom",
-        "//src/rp2_common/pico_runtime",
-        "//src/rp2_common/pico_standard_link",
     ],
 )
diff --git a/src/common/pico_binary_info/BUILD.bazel b/src/common/pico_binary_info/BUILD.bazel
index f3b996a..c4fa71b 100644
--- a/src/common/pico_binary_info/BUILD.bazel
+++ b/src/common/pico_binary_info/BUILD.bazel
@@ -1,7 +1,5 @@
 package(default_visibility = ["//visibility:public"])
 
-# TODO: Flags to support PICO_PROGRAM_*.
-
 cc_library(
     name = "pico_binary_info",
     hdrs = [
diff --git a/src/common/pico_binary_info/binary_info.bzl b/src/common/pico_binary_info/binary_info.bzl
new file mode 100644
index 0000000..a07a2c6
--- /dev/null
+++ b/src/common/pico_binary_info/binary_info.bzl
@@ -0,0 +1,39 @@
+load("@rules_cc//cc:defs.bzl", "cc_library")
+
+# PICO_BUILD_DEFINE: PICO_PROGRAM_NAME, Provided by PICO_DEFAULT_BINARY_INFO or a manually linked custom_pico_binary_info target, type=string, group=pico_binary_info
+# PICO_BUILD_DEFINE: PICO_PROGRAM_DESCRIPTION, Provided by PICO_DEFAULT_BINARY_INFO or a manually linked custom_pico_binary_info target, type=string, group=pico_binary_info
+# PICO_BUILD_DEFINE: PICO_PROGRAM_URL, Provided by PICO_DEFAULT_BINARY_INFO or a manually linked custom_pico_binary_info target, type=string, group=pico_binary_info
+# PICO_BUILD_DEFINE: PICO_PROGRAM_VERSION_STRING, Provided by PICO_DEFAULT_BINARY_INFO or a manually linked custom_pico_binary_info target, type=string, group=pico_binary_info
+# PICO_BUILD_DEFINE: PICO_TARGET_NAME, The name of the build target being compiled, type=string, default=target name, group=build
+def custom_pico_binary_info(name = None, program_name = None, program_description = None, program_url = None, program_version_string = None, build_target_name = None):
+    _all_defines = []
+    if program_name != None:
+        _all_defines.append('PICO_PROGRAM_NAME=\\"{}\\"'.format(program_name))
+    if program_description != None:
+        _all_defines.append('PICO_PROGRAM_DESCRIPTION=\\"{}\\"'.format(program_description))
+    if program_url != None:
+        _all_defines.append('PICO_PROGRAM_URL=\\"{}\\"'.format(program_url))
+    if program_version_string != None:
+        _all_defines.append('PICO_PROGRAM_VERSION_STRING=\\"{}\\"'.format(program_version_string))
+
+    # TODO: There's no practical way to support this correctly without a
+    # `pico_cc_binary` wrapper. Either way, this would be the right place to put
+    # it.
+    _build_target_name_defines = []
+    if build_target_name != None:
+        _build_target_name_defines.append('PICO_TARGET_NAME=\\"{}\\"'.format(build_target_name))
+    cc_library(
+        name = name,
+        defines = _all_defines + select({
+            "@pico-sdk//bazel/constraint:pico_no_target_name_enabled": [],
+            "//conditions:default": _build_target_name_defines,
+        }),
+        srcs = ["@pico-sdk//src/rp2_common/pico_standard_link:binary_info_srcs"],
+        deps = [
+            "@pico-sdk//src/rp2_common/pico_standard_link:PICO_BAZEL_BUILD_TYPE",
+            "@pico-sdk//src/common/pico_base:version",
+            "@pico-sdk//src/common/pico_binary_info",
+            "@pico-sdk//src/rp2_common/boot_stage2:config",
+        ],
+        alwayslink = True,
+    )
diff --git a/src/common/pico_bit_ops/BUILD.bazel b/src/common/pico_bit_ops/BUILD.bazel
index 3a81933..0d55f1e 100644
--- a/src/common/pico_bit_ops/BUILD.bazel
+++ b/src/common/pico_bit_ops/BUILD.bazel
@@ -8,6 +8,7 @@
     hdrs = ["include/pico/bit_ops.h"],
     includes = ["include"],
     visibility = [
+        "//src/host/pico_bit_ops:__pkg__",
         "//src/rp2_common/pico_bit_ops:__pkg__",
     ],
     deps = [
@@ -19,13 +20,10 @@
     name = "pico_bit_ops",
     hdrs = ["include/pico/bit_ops.h"],
     includes = ["include"],
-    # TODO: Add `select()` for host redirections.
-    target_compatible_with = select({
-        "//bazel/constraint:rp2": [],
-        "//conditions:default": ["@platforms//:incompatible"],
-    }),
     deps = [
         "//src/common/pico_base",
-        "//src/rp2_common/pico_bit_ops",
-    ],
+    ] + select({
+        "//bazel/constraint:host": ["//src/host/pico_bit_ops"],
+        "//conditions:default": ["//src/rp2_common/pico_bit_ops"],
+    }),
 )
diff --git a/src/common/pico_divider/BUILD.bazel b/src/common/pico_divider/BUILD.bazel
index 2349eaa..ed77bcc 100644
--- a/src/common/pico_divider/BUILD.bazel
+++ b/src/common/pico_divider/BUILD.bazel
@@ -7,7 +7,7 @@
     deps = [
         "//src/common/pico_base",
     ] + select({
-        "//bazel/constraint:rp2": ["//src/rp2_common/hardware_divider"],
         "//bazel/constraint:host": ["//src/host/hardware_divider"],
+        "//conditions:default": ["//src/rp2_common/pico_divider"],
     }),
 )
diff --git a/src/common/pico_stdlib/BUILD.bazel b/src/common/pico_stdlib/BUILD.bazel
index 0e34e67..a332e1f 100644
--- a/src/common/pico_stdlib/BUILD.bazel
+++ b/src/common/pico_stdlib/BUILD.bazel
@@ -6,39 +6,46 @@
     name = "pico_stdlib_interface",
     hdrs = ["include/pico/stdlib.h"],
     includes = ["include"],
-    # TODO: Add `select()` for host redirections.
-    target_compatible_with = select({
-        "//bazel/constraint:rp2": [],
-        "//conditions:default": ["@platforms//:incompatible"],
-    }),
     visibility = [
+        "//src/host/pico_stdio:__pkg__",
+        "//src/host/pico_stdlib:__pkg__",
         "//src/rp2_common/pico_stdlib:__pkg__",
         "//src/rp2_common/tinyusb:__pkg__",
     ],
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_time",
-        "//src/rp2_common/hardware_gpio",
-        "//src/rp2_common/hardware_uart",
-        "//src/rp2_common/pico_stdio",
-    ],
+    ] + select({
+        "//bazel/constraint:host": [
+            "//src/host/hardware_gpio",
+            "//src/host/hardware_uart",
+        ],
+        "//conditions:default": [
+            "//src/rp2_common/hardware_gpio",
+            "//src/rp2_common/hardware_uart",
+        ],
+    }),
 )
 
 cc_library(
     name = "pico_stdlib",
     hdrs = ["include/pico/stdlib.h"],
     includes = ["include"],
-    # TODO: Add `select()` for host redirections.
-    target_compatible_with = select({
-        "//bazel/constraint:rp2": [],
-        "//conditions:default": ["@platforms//:incompatible"],
-    }),
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_time",
-        "//src/rp2_common/hardware_gpio",
-        "//src/rp2_common/hardware_uart",
-        "//src/rp2_common/pico_stdio",
-        "//src/rp2_common/pico_stdlib",
-    ],
+    ] + select({
+        "//bazel/constraint:host": [
+            "//src/host/hardware_gpio",
+            "//src/host/hardware_uart",
+            "//src/host/pico_stdio",
+            "//src/host/pico_stdlib",
+        ],
+        "//conditions:default": [
+            "//src/rp2_common/hardware_gpio",
+            "//src/rp2_common/hardware_uart",
+            "//src/rp2_common/pico_stdio",
+            "//src/rp2_common/pico_stdlib",
+        ],
+    }),
 )
diff --git a/src/common/pico_sync/BUILD.bazel b/src/common/pico_sync/BUILD.bazel
index 68042a9..53c56b4 100644
--- a/src/common/pico_sync/BUILD.bazel
+++ b/src/common/pico_sync/BUILD.bazel
@@ -16,14 +16,15 @@
         "include/pico/sync.h",
     ],
     includes = ["include"],
-    # TODO: Add `select()` for host redirections.
-    target_compatible_with = select({
-        "//bazel/constraint:rp2": [],
-        "//conditions:default": ["@platforms//:incompatible"],
-    }),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_time:pico_time_headers",
-        "//src/rp2_common/hardware_sync",
-    ],
+    ] + select({
+        "//bazel/constraint:host": [
+            "//src/host/hardware_sync",
+        ],
+        "//conditions:default": [
+            "//src/rp2_common/hardware_sync",
+        ],
+    }),
 )
diff --git a/src/common/pico_time/BUILD.bazel b/src/common/pico_time/BUILD.bazel
index b32dc5b..b429dc5 100644
--- a/src/common/pico_time/BUILD.bazel
+++ b/src/common/pico_time/BUILD.bazel
@@ -14,9 +14,14 @@
         "//src/common/pico_sync:__pkg__",
         "//src/rp2_common/tinyusb:__pkg__",
     ],
-    deps = [
-        "//src/rp2_common/hardware_timer:hardware_timer_headers",
-    ],
+    deps = select({
+        "//bazel/constraint:host": [
+            "//src/host/hardware_timer:hardware_timer_headers",
+        ],
+        "//conditions:default": [
+            "//src/rp2_common/hardware_timer:hardware_timer_headers",
+        ],
+    }),
 )
 
 cc_library(
@@ -30,15 +35,16 @@
         "include/pico/timeout_helper.h",
     ],
     includes = ["include"],
-    # TODO: Add `select()` for host redirections.
-    target_compatible_with = select({
-        "//bazel/constraint:rp2": [],
-        "//conditions:default": ["@platforms//:incompatible"],
-    }),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_sync",
         "//src/common/pico_util",
-        "//src/rp2_common/hardware_timer",
-    ],
+    ] + select({
+        "//bazel/constraint:host": [
+            "//src/host/hardware_timer",
+        ],
+        "//conditions:default": [
+            "//src/rp2_common/hardware_timer",
+        ],
+    }),
 )
diff --git a/src/common/pico_util/BUILD.bazel b/src/common/pico_util/BUILD.bazel
index 00d7b23..7eed87a 100644
--- a/src/common/pico_util/BUILD.bazel
+++ b/src/common/pico_util/BUILD.bazel
@@ -13,14 +13,15 @@
         "include/pico/util/queue.h",
     ],
     includes = ["include"],
-    # TODO: Add `select()` for host redirections.
-    target_compatible_with = select({
-        "//bazel/constraint:rp2": [],
-        "//conditions:default": ["@platforms//:incompatible"],
-    }),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_sync",
-        "//src/rp2_common/hardware_sync",
-    ],
+    ] + select({
+        "//bazel/constraint:host": [
+            "//src/host/hardware_sync",
+        ],
+        "//conditions:default": [
+            "//src/rp2_common/hardware_sync",
+        ],
+    }),
 )
diff --git a/src/host/hardware_divider/BUILD.bazel b/src/host/hardware_divider/BUILD.bazel
new file mode 100644
index 0000000..3548f1b
--- /dev/null
+++ b/src/host/hardware_divider/BUILD.bazel
@@ -0,0 +1,10 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "hardware_divider",
+    srcs = ["divider.c"],
+    hdrs = ["include/hardware/divider.h"],
+    includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
+    deps = ["//src/common/pico_base"],
+)
diff --git a/src/host/hardware_gpio/BUILD.bazel b/src/host/hardware_gpio/BUILD.bazel
new file mode 100644
index 0000000..816ebb4
--- /dev/null
+++ b/src/host/hardware_gpio/BUILD.bazel
@@ -0,0 +1,10 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "hardware_gpio",
+    srcs = ["gpio.c"],
+    hdrs = ["include/hardware/gpio.h"],
+    includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
+    deps = ["//src/common/pico_base"],
+)
diff --git a/src/host/hardware_sync/BUILD.bazel b/src/host/hardware_sync/BUILD.bazel
new file mode 100644
index 0000000..f218d36
--- /dev/null
+++ b/src/host/hardware_sync/BUILD.bazel
@@ -0,0 +1,11 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "hardware_sync",
+    srcs = ["sync_core0_only.c"],
+    hdrs = ["include/hardware/sync.h"],
+    implementation_deps = ["//src/host/pico_platform:platform_defs"],
+    includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
+    deps = ["//src/common/pico_base"],
+)
diff --git a/src/host/hardware_timer/BUILD.bazel b/src/host/hardware_timer/BUILD.bazel
new file mode 100644
index 0000000..af37e2f
--- /dev/null
+++ b/src/host/hardware_timer/BUILD.bazel
@@ -0,0 +1,30 @@
+package(default_visibility = ["//visibility:public"])
+
+_DEFINES = [
+    "PICO_HARDWARE_TIMER_RESOLUTION_US=1000",
+    # TODO: This seems to be the default, make configurable eventually.
+    "PICO_TIME_DEFAULT_ALARM_POOL_DISABLED=1",
+]
+
+# This exists to break a dependency cycle between
+# this library and //src/common/pico_time.
+# Application code should always use :hardware_timer instead.
+cc_library(
+    name = "hardware_timer_headers",
+    hdrs = ["include/hardware/timer.h"],
+    defines = _DEFINES,
+    includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
+    visibility = ["//src/common/pico_time:__pkg__"],
+    deps = ["//src/common/pico_base:pico_base_interface"],
+)
+
+cc_library(
+    name = "hardware_timer",
+    srcs = ["timer.c"],
+    hdrs = ["include/hardware/timer.h"],
+    defines = _DEFINES,
+    includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
+    deps = ["//src/common/pico_base"],
+)
diff --git a/src/host/hardware_uart/BUILD.bazel b/src/host/hardware_uart/BUILD.bazel
new file mode 100644
index 0000000..5288d14
--- /dev/null
+++ b/src/host/hardware_uart/BUILD.bazel
@@ -0,0 +1,10 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "hardware_uart",
+    srcs = ["uart.c"],
+    hdrs = ["include/hardware/uart.h"],
+    includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
+    deps = ["//src/common/pico_base"],
+)
diff --git a/src/host/pico_bit_ops/BUILD.bazel b/src/host/pico_bit_ops/BUILD.bazel
new file mode 100644
index 0000000..a0e7dc4
--- /dev/null
+++ b/src/host/pico_bit_ops/BUILD.bazel
@@ -0,0 +1,8 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "pico_bit_ops",
+    srcs = ["bit_ops.c"],
+    implementation_deps = ["//src/common/pico_bit_ops:pico_bit_ops_headers"],
+    target_compatible_with = ["//bazel/constraint:host"],
+)
diff --git a/src/host/pico_divider/BUILD.bazel b/src/host/pico_divider/BUILD.bazel
new file mode 100644
index 0000000..e82785f
--- /dev/null
+++ b/src/host/pico_divider/BUILD.bazel
@@ -0,0 +1,8 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "pico_divider",
+    srcs = ["divider.c"],
+    implementation_deps = ["//src/common/pico_divider"],
+    target_compatible_with = ["//bazel/constraint:host"],
+)
diff --git a/src/host/pico_multicore/BUILD.bazel b/src/host/pico_multicore/BUILD.bazel
new file mode 100644
index 0000000..61b1092
--- /dev/null
+++ b/src/host/pico_multicore/BUILD.bazel
@@ -0,0 +1,9 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "pico_multicore",
+    hdrs = ["include/pico/multicore.h"],
+    includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
+    deps = ["//src/common/pico_base"],
+)
diff --git a/src/host/pico_platform/BUILD.bazel b/src/host/pico_platform/BUILD.bazel
index 2cad9cd..d89fd3a 100644
--- a/src/host/pico_platform/BUILD.bazel
+++ b/src/host/pico_platform/BUILD.bazel
@@ -6,6 +6,7 @@
         "include/hardware/platform_defs.h",
     ],
     includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
 )
 
 cc_library(
@@ -15,6 +16,7 @@
         "include/pico/platform.h",
     ],
     includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
     deps = [
         ":platform_defs",
         "//src/common/pico_base:pico_base_interface",
diff --git a/src/host/pico_printf/BUILD.bazel b/src/host/pico_printf/BUILD.bazel
new file mode 100644
index 0000000..b6a4bb7
--- /dev/null
+++ b/src/host/pico_printf/BUILD.bazel
@@ -0,0 +1,6 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "pico_printf",
+    target_compatible_with = ["//bazel/constraint:host"],
+)
diff --git a/src/host/pico_stdio/BUILD.bazel b/src/host/pico_stdio/BUILD.bazel
new file mode 100644
index 0000000..abdc993
--- /dev/null
+++ b/src/host/pico_stdio/BUILD.bazel
@@ -0,0 +1,13 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "pico_stdio",
+    srcs = ["stdio.c"],
+    hdrs = ["include/pico/stdio.h"],
+    implementation_deps = [
+        "//src/common/pico_stdlib:pico_stdlib_interface",
+        "//src/host/hardware_uart",
+    ],
+    includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:host"],
+)
diff --git a/src/host/pico_stdlib/BUILD.bazel b/src/host/pico_stdlib/BUILD.bazel
new file mode 100644
index 0000000..245f693
--- /dev/null
+++ b/src/host/pico_stdlib/BUILD.bazel
@@ -0,0 +1,11 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "pico_stdlib",
+    srcs = ["stdlib.c"],
+    implementation_deps = [
+        "//src/common/pico_stdlib:pico_stdlib_interface",
+        "//src/host/pico_stdio",
+    ],
+    target_compatible_with = ["//bazel/constraint:host"],
+)
diff --git a/src/rp2040/hardware_regs/BUILD.bazel b/src/rp2040/hardware_regs/BUILD.bazel
index 6bd0081..d2793e6 100644
--- a/src/rp2040/hardware_regs/BUILD.bazel
+++ b/src/rp2040/hardware_regs/BUILD.bazel
@@ -9,6 +9,7 @@
         "include/hardware/platform_defs.h",
     ],
     includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:rp2040"],
 )
 
 cc_library(
@@ -50,4 +51,5 @@
         "include/hardware/regs/xosc.h",
     ],
     includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:rp2040"],
 )
diff --git a/src/rp2040/hardware_structs/BUILD.bazel b/src/rp2040/hardware_structs/BUILD.bazel
index 692334c..a4ddb7b 100644
--- a/src/rp2040/hardware_structs/BUILD.bazel
+++ b/src/rp2040/hardware_structs/BUILD.bazel
@@ -40,4 +40,5 @@
         "include/hardware/structs/xosc.h",
     ],
     includes = ["include"],
+    target_compatible_with = ["//bazel/constraint:rp2040"],
 )
diff --git a/src/rp2_common/boot_stage2/BUILD.bazel b/src/rp2_common/boot_stage2/BUILD.bazel
index df6a041..1edd115 100644
--- a/src/rp2_common/boot_stage2/BUILD.bazel
+++ b/src/rp2_common/boot_stage2/BUILD.bazel
@@ -1,41 +1,78 @@
 load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
 load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
+load("@rules_python//python:defs.bzl", "py_binary")
+load("//bazel:defs.bzl", "compatible_with_rp2")
 load("//bazel/toolchain:objcopy.bzl", "objcopy_to_bin")
+load("//bazel/util:multiple_choice_flag.bzl", "declare_flag_choices", "flag_choice")
 load("//bazel/util:transition.bzl", "rp2040_bootloader_binary")
 
+# There's a lot of implementation details in here that shouldn't be considered
+# stable, so allowlist visibility to just the public-facing pieces.
 package(default_visibility = ["//visibility:private"])
 
+# Known choices for boot2:
+BOOT2_CHOICES = [
+    "boot2_at25sf128a",
+    "boot2_generic_03h",
+    "boot2_is25lp080",
+    "boot2_usb_blinky",
+    "boot2_w25q080",
+    "boot2_w25x10cl",
+    "compile_time_choice",
+]
+
+BOOT2_CHOICE_FILES = [c + ".S" for c in BOOT2_CHOICES]
+
+BOOT2_CHOICE_FILE_MAP = {c: [c + ".S"] for c in BOOT2_CHOICES}
+
+BOOT2_CHOICE_DEFINE_MAP = {c: ['PICO_BUILD_BOOT_STAGE2_NAME=\\"{}\\"'.format(c)] for c in BOOT2_CHOICES}
+
+# Define shouldn't be set for compile_time_choice.
+BOOT2_CHOICE_DEFINE_MAP["compile_time_choice"] = []
+
 cc_library(
     name = "config",
     hdrs = [
         "asminclude/boot2_helpers/exit_from_boot2.S",
         "asminclude/boot2_helpers/read_flash_sreg.S",
         "asminclude/boot2_helpers/wait_ssi_ready.S",
-        "boot2_at25sf128a.S",
-        "boot2_generic_03h.S",
-        "boot2_is25lp080.S",
-        "boot2_usb_blinky.S",
-        "boot2_w25q080.S",
-        "boot2_w25x10cl.S",
         "include/boot_stage2/config.h",
-    ],
+    ] + BOOT2_CHOICE_FILES,
+    defines = select(flag_choice(
+        "//bazel/config:PICO_DEFAULT_BOOT_STAGE2",
+        ":__pkg__",
+        BOOT2_CHOICE_DEFINE_MAP,
+    )),
     includes = [
         "asminclude",
         "include",
     ],
-    visibility = ["//src/rp2_common/pico_standard_link:__pkg__"],
+    target_compatible_with = compatible_with_rp2(),
+    visibility = ["//visibility:public"],
 )
 
-# Stub library to prevent custom malloc from getting linked in. boot2 will never
-# need malloc, so letting it link can only cause problems.
-cc_library(
-    name = "no_malloc",
+# Creates a config_setting for each known boot2 option with the name:
+#     PICO_DEFAULT_BOOT_STAGE2_[choice]
+declare_flag_choices(
+    "//bazel/config:PICO_DEFAULT_BOOT_STAGE2",
+    BOOT2_CHOICES,
+)
+
+filegroup(
+    name = "build_selected_boot2",
+    srcs = select(flag_choice(
+        "//bazel/config:PICO_DEFAULT_BOOT_STAGE2",
+        ":__pkg__",
+        BOOT2_CHOICE_FILE_MAP,
+    )),
 )
 
 cc_binary(
     name = "boot_stage2_elf_actual",
-    srcs = ["compile_time_choice.S"],
+    srcs = ["//bazel/config:PICO_DEFAULT_BOOT_STAGE2_FILE"],
     copts = ["-fPIC"],
+    # Incompatible with section garbage collection.
+    features = ["-gc_sections"],
     linkopts = [
         "-Wl,--no-gc-sections",
         "-nostartfiles",
@@ -43,8 +80,9 @@
     ],
     # this does nothing if someone passes --custom_malloc, so the
     # rp2040_bootloader_binary transition forcibly clobbers --custom_malloc.
-    malloc = ":no_malloc",
-    tags = ["manual"],
+    malloc = "//bazel:empty_cc_lib",
+    tags = ["manual"],  # Only build as an explicit dependency.
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "boot_stage2.ld",
         ":config",
@@ -63,6 +101,7 @@
     name = "boot_stage2_bin",
     src = ":boot_stage2_elf",
     out = "boot_stage2.bin",
+    target_compatible_with = compatible_with_rp2(),
 )
 
 # WORKAROUND: Python rules always require a .py extension.
@@ -70,11 +109,13 @@
     name = "copy_tool_to_py",
     src = "pad_checksum",
     out = "pad_checksum_tool.py",
+    target_compatible_with = ["//bazel/constraint:host"],
 )
 
 py_binary(
     name = "pad_checksum_tool",
     srcs = ["pad_checksum_tool.py"],
+    target_compatible_with = ["//bazel/constraint:host"],
 )
 
 run_binary(
@@ -86,12 +127,14 @@
         "$(location boot_stage2_bin)",
         "$(location boot_stage2.S)",
     ],
+    target_compatible_with = compatible_with_rp2(),
     tool = ":pad_checksum_tool",
 )
 
 cc_library(
     name = "boot_stage2",
     srcs = [":boot_stage2_padded"],
+    target_compatible_with = compatible_with_rp2(),
     visibility = ["//visibility:public"],
     # This isn't referenced as a symbol, so alwayslink is required to ensure
     # it doesn't get dropped before the linker script can find it.
diff --git a/src/rp2_common/cmsis/BUILD.bazel b/src/rp2_common/cmsis/BUILD.bazel
index 24cf5df..c378729 100644
--- a/src/rp2_common/cmsis/BUILD.bazel
+++ b/src/rp2_common/cmsis/BUILD.bazel
@@ -1,8 +1,18 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
+# This is added to @pico-sdk//src/common/pico_base:default_platform_headers
+# by default.
 cc_library(
     name = "rename_exceptions",
     hdrs = ["include/cmsis/rename_exceptions.h"],
+    # This is mildly odd, but intentional. We really don't want this header
+    # to have extra deps, and this header is touched by the host build.
+    defines = select({
+        "//bazel/constraint:host": [],
+        "//conditions:default": ["LIB_CMSIS_CORE=1"],
+    }),
     includes = ["include"],
 )
 
@@ -24,10 +34,12 @@
         "stub/CMSIS/Device/RaspberryPi/RP2040/Include/RP2040.h",
         "stub/CMSIS/Device/RaspberryPi/RP2040/Include/system_RP2040.h",
     ],
+    defines = ["LIB_CMSIS_CORE=1"],
     includes = [
         "stub/CMSIS/Core/Include",
         "stub/CMSIS/Device/RaspberryPi/RP2040/Include",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/rp2_common/hardware_clocks",
     ],
diff --git a/src/rp2_common/hardware_adc/BUILD.bazel b/src/rp2_common/hardware_adc/BUILD.bazel
index 29dfd09..b1532a3 100644
--- a/src/rp2_common/hardware_adc/BUILD.bazel
+++ b/src/rp2_common/hardware_adc/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["adc.c"],
     hdrs = ["include/hardware/adc.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_gpio",
diff --git a/src/rp2_common/hardware_base/BUILD.bazel b/src/rp2_common/hardware_base/BUILD.bazel
index ebf54b9..10c5140 100644
--- a/src/rp2_common/hardware_base/BUILD.bazel
+++ b/src/rp2_common/hardware_base/BUILD.bazel
@@ -1,7 +1,10 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "hardware_base",
     hdrs = ["include/hardware/address_mapped.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
 )
diff --git a/src/rp2_common/hardware_claim/BUILD.bazel b/src/rp2_common/hardware_claim/BUILD.bazel
index b9875ca..395e8e0 100644
--- a/src/rp2_common/hardware_claim/BUILD.bazel
+++ b/src/rp2_common/hardware_claim/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["claim.c"],
     hdrs = ["include/hardware/claim.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_base:pico_platform",
diff --git a/src/rp2_common/hardware_clocks/BUILD.bazel b/src/rp2_common/hardware_clocks/BUILD.bazel
index 7152804..8fe301b 100644
--- a/src/rp2_common/hardware_clocks/BUILD.bazel
+++ b/src/rp2_common/hardware_clocks/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 # This exists to break a dependency cycle between
@@ -7,6 +9,7 @@
     name = "hardware_clocks_headers",
     hdrs = ["include/hardware/clocks.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     visibility = [
         "//src/rp2_common/hardware_pll:__pkg__",
         "//src/rp2_common/hardware_xosc:__pkg__",
@@ -22,6 +25,7 @@
     srcs = ["clocks.c"],
     hdrs = ["include/hardware/clocks.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/rp2_common/hardware_gpio",
diff --git a/src/rp2_common/hardware_divider/BUILD.bazel b/src/rp2_common/hardware_divider/BUILD.bazel
index 0b3dc9c..72cf376 100644
--- a/src/rp2_common/hardware_divider/BUILD.bazel
+++ b/src/rp2_common/hardware_divider/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -8,6 +10,7 @@
         "include/hardware/divider_helper.S",
     ],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/pico_platform",
diff --git a/src/rp2_common/hardware_dma/BUILD.bazel b/src/rp2_common/hardware_dma/BUILD.bazel
index fceabfb..4e0c753 100644
--- a/src/rp2_common/hardware_dma/BUILD.bazel
+++ b/src/rp2_common/hardware_dma/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["dma.c"],
     hdrs = ["include/hardware/dma.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_claim",
diff --git a/src/rp2_common/hardware_exception/BUILD.bazel b/src/rp2_common/hardware_exception/BUILD.bazel
index 72b4c41..b19f9cf 100644
--- a/src/rp2_common/hardware_exception/BUILD.bazel
+++ b/src/rp2_common/hardware_exception/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["exception.c"],
     hdrs = ["include/hardware/exception.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_base",
diff --git a/src/rp2_common/hardware_flash/BUILD.bazel b/src/rp2_common/hardware_flash/BUILD.bazel
index ee79e6f..c5b8650 100644
--- a/src/rp2_common/hardware_flash/BUILD.bazel
+++ b/src/rp2_common/hardware_flash/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["flash.c"],
     hdrs = ["include/hardware/flash.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/pico_bootrom",
diff --git a/src/rp2_common/hardware_gpio/BUILD.bazel b/src/rp2_common/hardware_gpio/BUILD.bazel
index 5d035eb..5dacb7d 100644
--- a/src/rp2_common/hardware_gpio/BUILD.bazel
+++ b/src/rp2_common/hardware_gpio/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["gpio.c"],
     hdrs = ["include/hardware/gpio.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_binary_info",
diff --git a/src/rp2_common/hardware_i2c/BUILD.bazel b/src/rp2_common/hardware_i2c/BUILD.bazel
index cd9110f..9dd54e7 100644
--- a/src/rp2_common/hardware_i2c/BUILD.bazel
+++ b/src/rp2_common/hardware_i2c/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["i2c.c"],
     hdrs = ["include/hardware/i2c.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_time",
diff --git a/src/rp2_common/hardware_interp/BUILD.bazel b/src/rp2_common/hardware_interp/BUILD.bazel
index fa8106f..861fae5 100644
--- a/src/rp2_common/hardware_interp/BUILD.bazel
+++ b/src/rp2_common/hardware_interp/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["interp.c"],
     hdrs = ["include/hardware/interp.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_claim",
diff --git a/src/rp2_common/hardware_irq/BUILD.bazel b/src/rp2_common/hardware_irq/BUILD.bazel
index 0e929c9..ffa7b57 100644
--- a/src/rp2_common/hardware_irq/BUILD.bazel
+++ b/src/rp2_common/hardware_irq/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -8,6 +10,7 @@
     ],
     hdrs = ["include/hardware/irq.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_sync",
diff --git a/src/rp2_common/hardware_pio/BUILD.bazel b/src/rp2_common/hardware_pio/BUILD.bazel
index 0cf474d..518e875 100644
--- a/src/rp2_common/hardware_pio/BUILD.bazel
+++ b/src/rp2_common/hardware_pio/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -8,6 +10,7 @@
         "include/hardware/pio_instructions.h",
     ],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_base",
diff --git a/src/rp2_common/hardware_pll/BUILD.bazel b/src/rp2_common/hardware_pll/BUILD.bazel
index ea0199b..4f1ab28 100644
--- a/src/rp2_common/hardware_pll/BUILD.bazel
+++ b/src/rp2_common/hardware_pll/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["pll.c"],
     hdrs = ["include/hardware/pll.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_base:pico_platform",
diff --git a/src/rp2_common/hardware_pwm/BUILD.bazel b/src/rp2_common/hardware_pwm/BUILD.bazel
index 2b9b4d1..9797d60 100644
--- a/src/rp2_common/hardware_pwm/BUILD.bazel
+++ b/src/rp2_common/hardware_pwm/BUILD.bazel
@@ -1,9 +1,12 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "hardware_pwm",
     hdrs = ["include/hardware/pwm.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/pico_platform:hardware_regs",
diff --git a/src/rp2_common/hardware_resets/BUILD.bazel b/src/rp2_common/hardware_resets/BUILD.bazel
index 294ed23..2a0fa99 100644
--- a/src/rp2_common/hardware_resets/BUILD.bazel
+++ b/src/rp2_common/hardware_resets/BUILD.bazel
@@ -1,9 +1,12 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "hardware_resets",
     hdrs = ["include/hardware/resets.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/rp2_common/pico_platform:hardware_structs",
diff --git a/src/rp2_common/hardware_rtc/BUILD.bazel b/src/rp2_common/hardware_rtc/BUILD.bazel
index 7992511..31226ec 100644
--- a/src/rp2_common/hardware_rtc/BUILD.bazel
+++ b/src/rp2_common/hardware_rtc/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["rtc.c"],
     hdrs = ["include/hardware/rtc.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_clocks",
diff --git a/src/rp2_common/hardware_spi/BUILD.bazel b/src/rp2_common/hardware_spi/BUILD.bazel
index e9bccc9..edbc821 100644
--- a/src/rp2_common/hardware_spi/BUILD.bazel
+++ b/src/rp2_common/hardware_spi/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["spi.c"],
     hdrs = ["include/hardware/spi.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_clocks",
diff --git a/src/rp2_common/hardware_sync/BUILD.bazel b/src/rp2_common/hardware_sync/BUILD.bazel
index 3805642..ebf7608 100644
--- a/src/rp2_common/hardware_sync/BUILD.bazel
+++ b/src/rp2_common/hardware_sync/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 # This exists to break a dependency cycle between
@@ -7,6 +9,7 @@
     name = "hardware_sync_headers",
     hdrs = ["include/hardware/sync.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     visibility = ["//src/rp2_common/hardware_claim:__pkg__"],
     deps = [
         "//src/common/pico_base:pico_base_interface",
@@ -20,6 +23,7 @@
     srcs = ["sync.c"],
     hdrs = ["include/hardware/sync.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/rp2_common/hardware_base",
diff --git a/src/rp2_common/hardware_timer/BUILD.bazel b/src/rp2_common/hardware_timer/BUILD.bazel
index b97bcde..1b76b77 100644
--- a/src/rp2_common/hardware_timer/BUILD.bazel
+++ b/src/rp2_common/hardware_timer/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 # This exists to break a dependency cycle between
@@ -7,6 +9,7 @@
     name = "hardware_timer_headers",
     hdrs = ["include/hardware/timer.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     visibility = ["//src/common/pico_time:__pkg__"],
     deps = [
         "//src/common/pico_base:pico_base_interface",
@@ -19,6 +22,7 @@
     srcs = ["timer.c"],
     hdrs = ["include/hardware/timer.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/rp2_common/hardware_claim",
diff --git a/src/rp2_common/hardware_uart/BUILD.bazel b/src/rp2_common/hardware_uart/BUILD.bazel
index a329df3..42cf900 100644
--- a/src/rp2_common/hardware_uart/BUILD.bazel
+++ b/src/rp2_common/hardware_uart/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["uart.c"],
     hdrs = ["include/hardware/uart.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_base",
diff --git a/src/rp2_common/hardware_vreg/BUILD.bazel b/src/rp2_common/hardware_vreg/BUILD.bazel
index 0a90697..626aa6c 100644
--- a/src/rp2_common/hardware_vreg/BUILD.bazel
+++ b/src/rp2_common/hardware_vreg/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["vreg.c"],
     hdrs = ["include/hardware/vreg.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/pico_platform:hardware_structs",
diff --git a/src/rp2_common/hardware_watchdog/BUILD.bazel b/src/rp2_common/hardware_watchdog/BUILD.bazel
index fad2f97..a23f3a4 100644
--- a/src/rp2_common/hardware_watchdog/BUILD.bazel
+++ b/src/rp2_common/hardware_watchdog/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["watchdog.c"],
     hdrs = ["include/hardware/watchdog.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_base:pico_platform",
diff --git a/src/rp2_common/hardware_xosc/BUILD.bazel b/src/rp2_common/hardware_xosc/BUILD.bazel
index 2ed2201..8897d4b 100644
--- a/src/rp2_common/hardware_xosc/BUILD.bazel
+++ b/src/rp2_common/hardware_xosc/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["xosc.c"],
     hdrs = ["include/hardware/xosc.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_base:pico_platform",
diff --git a/src/rp2_common/pico_async_context/BUILD.bazel b/src/rp2_common/pico_async_context/BUILD.bazel
index 577d1c8..fc7ed33 100644
--- a/src/rp2_common/pico_async_context/BUILD.bazel
+++ b/src/rp2_common/pico_async_context/BUILD.bazel
@@ -1,28 +1,63 @@
+load("//bazel:defs.bzl", "compatible_with_rp2", "incompatible_with_config")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "pico_async_context",
-    srcs = [
-        "async_context_base.c",
-        "async_context_freertos.c",
-        "async_context_poll.c",
-        "async_context_threadsafe_background.c",
-    ],
+    srcs = ["async_context_base.c"],
     hdrs = [
         "include/pico/async_context.h",
         "include/pico/async_context_base.h",
-        "include/pico/async_context_freertos.h",
-        "include/pico/async_context_poll.h",
-        "include/pico/async_context_threadsafe_background.h",
     ],
     includes = ["include"],
-    # Missing deps for:
-    #    FreeRTOS.h
-    #    semphr.h
-    #    timers.h
-    tags = ["manual"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
+        "//src/common/pico_time",
+    ],
+)
+
+cc_library(
+    name = "pico_async_context_freertos",
+    srcs = ["async_context_freertos.c"],
+    hdrs = ["include/pico/async_context_freertos.h"],
+    includes = ["include"],
+    target_compatible_with = compatible_with_rp2() + incompatible_with_config(
+        "//bazel/constraint:pico_freertos_unset",
+    ),
+    deps = [
+        ":pico_async_context",
+        "//bazel/config:PICO_FREERTOS_LIB",
+        "//src/common/pico_base",
+        "//src/common/pico_sync",
+        "//src/common/pico_time",
+        "//src/rp2_common/hardware_irq",
+    ],
+)
+
+cc_library(
+    name = "pico_async_context_poll",
+    srcs = ["async_context_poll.c"],
+    hdrs = ["include/pico/async_context_poll.h"],
+    includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        ":pico_async_context",
+        "//src/common/pico_base",
+        "//src/common/pico_sync",
+        "//src/common/pico_time",
+    ],
+)
+
+cc_library(
+    name = "pico_async_context_threadsafe_background",
+    srcs = ["async_context_threadsafe_background.c"],
+    hdrs = ["include/pico/async_context_threadsafe_background.h"],
+    includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        ":pico_async_context",
+        "//src/common/pico_base",
         "//src/common/pico_sync",
         "//src/common/pico_time",
         "//src/rp2_common/hardware_irq",
diff --git a/src/rp2_common/pico_bit_ops/BUILD.bazel b/src/rp2_common/pico_bit_ops/BUILD.bazel
index 35322a8..0daadd0 100644
--- a/src/rp2_common/pico_bit_ops/BUILD.bazel
+++ b/src/rp2_common/pico_bit_ops/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -16,6 +18,7 @@
         "-Wl,--wrap=__clzsi2",
         "-Wl,--wrap=__clzll",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_bit_ops:pico_bit_ops_headers",
         "//src/rp2_common/pico_bootrom",
diff --git a/src/rp2_common/pico_bootrom/BUILD.bazel b/src/rp2_common/pico_bootrom/BUILD.bazel
index b31378c..586ddd7 100644
--- a/src/rp2_common/pico_bootrom/BUILD.bazel
+++ b/src/rp2_common/pico_bootrom/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -8,6 +10,7 @@
         "include/pico/bootrom/sf_table.h",
     ],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_base:pico_platform",
diff --git a/src/rp2_common/pico_bootsel_via_double_reset/BUILD.bazel b/src/rp2_common/pico_bootsel_via_double_reset/BUILD.bazel
index 8e85b70..49cbf7a 100644
--- a/src/rp2_common/pico_bootsel_via_double_reset/BUILD.bazel
+++ b/src/rp2_common/pico_bootsel_via_double_reset/BUILD.bazel
@@ -1,8 +1,11 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "pico_bootsel_via_double_reset",
     srcs = ["pico_bootsel_via_double_reset.c"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_binary_info",
diff --git a/src/rp2_common/pico_btstack/BUILD.bazel b/src/rp2_common/pico_btstack/BUILD.bazel
index c3afa3e..6ae04ae 100644
--- a/src/rp2_common/pico_btstack/BUILD.bazel
+++ b/src/rp2_common/pico_btstack/BUILD.bazel
@@ -1,28 +1,73 @@
+load("//bazel:defs.bzl", "compatible_with_pico_w", "incompatible_with_config")
+
 package(default_visibility = ["//visibility:public"])
 
+# Prefer these aliases to directly referencing @btstack, as it's possible that
+# name may change.
+alias(
+    name = "pico_btstack_base",
+    actual = "@btstack//:pico_btstack_base",
+)
+
+alias(
+    name = "pico_btstack_ble",
+    actual = "@btstack//:pico_btstack_ble",
+)
+
+alias(
+    name = "pico_btstack_classic",
+    actual = "@btstack//:pico_btstack_classic",
+)
+
+alias(
+    name = "pico_btstack_sbc_encoder",
+    actual = "@btstack//:pico_btstack_classic",
+)
+
+alias(
+    name = "pico_btstack_bnep_lwip",
+    actual = "@btstack//:pico_btstack_bnep_lwip",
+)
+
+alias(
+    name = "pico_btstack_bnep_lwip_sys_freertos",
+    actual = "@btstack//:pico_btstack_bnep_lwip_sys_freertos",
+)
+
 cc_library(
-    name = "pico_btstack",
-    srcs = [
-        "btstack_flash_bank.c",
-        "btstack_run_loop_async_context.c",
-        "btstack_stdin_pico.c",
-    ],
-    hdrs = [
-        "include/pico/btstack_flash_bank.h",
-        "include/pico/btstack_run_loop_async_context.h",
-    ],
+    name = "pico_btstack_flash_bank",
+    srcs = ["btstack_flash_bank.c"],
+    hdrs = ["include/pico/btstack_flash_bank.h"],
     includes = ["include"],
-    # Missing deps for:
-    #     btstack_config.h
-    #     btstack_run_loop.h
-    #     btstack_stdin.h
-    #     hal_flash_bank.h
-    tags = ["manual"],
+    target_compatible_with = compatible_with_pico_w(),
     deps = [
+        ":pico_btstack_base",
         "//src/common/pico_base",
+        "//src/rp2_common/pico_flash",
+    ],
+)
+
+cc_library(
+    name = "btstack_run_loop_async_context",
+    srcs = ["btstack_run_loop_async_context.c"],
+    hdrs = ["include/pico/btstack_run_loop_async_context.h"],
+    includes = ["include"],
+    target_compatible_with = compatible_with_pico_w(),
+    deps = [
         "//src/rp2_common/hardware_sync",
         "//src/rp2_common/pico_async_context",
-        "//src/rp2_common/pico_flash",
+        "@btstack//:pico_btstack_base",
+    ],
+)
+
+cc_library(
+    name = "pico_btstack_stdin",
+    srcs = ["btstack_stdin_pico.c"],
+    target_compatible_with = incompatible_with_config(
+        "//bazel/constraint:pico_btstack_config_unset",
+    ) + compatible_with_pico_w(),
+    deps = [
+        "//src/common/pico_base",
         "//src/rp2_common/pico_stdio",
     ],
 )
diff --git a/src/rp2_common/pico_btstack/btstack.BUILD b/src/rp2_common/pico_btstack/btstack.BUILD
new file mode 100644
index 0000000..ac009d9
--- /dev/null
+++ b/src/rp2_common/pico_btstack/btstack.BUILD
@@ -0,0 +1,197 @@
+load("@pico-sdk//bazel:defs.bzl", "incompatible_with_config")
+
+package(default_visibility = ["//visibility:public"])
+
+_DISABLE_WARNINGS = [
+    "-Wno-cast-qual",
+    "-Wno-format",
+    "-Wno-maybe-uninitialized",
+    "-Wno-null-dereference",
+    "-Wno-sign-compare",
+    "-Wno-stringop-overflow",
+    "-Wno-suggest-attribute=format",
+    "-Wno-type-limits",
+    "-Wno-unused-parameter",
+]
+
+cc_library(
+    name = "pico_btstack_base",
+    srcs = [
+        "3rd-party/md5/md5.c",
+        "3rd-party/micro-ecc/uECC.c",
+        "3rd-party/rijndael/rijndael.c",
+        "3rd-party/segger-rtt/SEGGER_RTT.c",
+        "3rd-party/segger-rtt/SEGGER_RTT_printf.c",
+        "3rd-party/yxml/yxml.c",
+        "platform/embedded/btstack_tlv_flash_bank.c",
+        "platform/embedded/hci_dump_embedded_stdout.c",
+        "platform/embedded/hci_dump_segger_rtt_stdout.c",
+        "src/ad_parser.c",
+        "src/btstack_audio.c",
+        "src/btstack_base64_decoder.c",
+        "src/btstack_crypto.c",
+        "src/btstack_hid_parser.c",
+        "src/btstack_linked_list.c",
+        "src/btstack_memory.c",
+        "src/btstack_memory_pool.c",
+        "src/btstack_resample.c",
+        "src/btstack_ring_buffer.c",
+        "src/btstack_run_loop.c",
+        "src/btstack_run_loop_base.c",
+        "src/btstack_slip.c",
+        "src/btstack_tlv.c",
+        "src/btstack_tlv_none.c",
+        "src/btstack_util.c",
+        "src/hci.c",
+        "src/hci_cmd.c",
+        "src/hci_dump.c",
+        "src/hci_event.c",
+        "src/l2cap.c",
+        "src/l2cap_signaling.c",
+        "src/mesh/gatt-service/mesh_provisioning_service_server.c",
+        "src/mesh/gatt-service/mesh_proxy_service_server.c",
+    ],
+    hdrs = glob(["**/*.h"]),
+    copts = _DISABLE_WARNINGS,
+    includes = [
+        ".",
+        "3rd-party/md5",
+        "3rd-party/micro-ecc",
+        "3rd-party/rijndael",
+        "3rd-party/segger-rtt",
+        "3rd-party/yxml",
+        "platform/embedded",
+        "src",
+    ],
+    target_compatible_with = incompatible_with_config(
+        "@pico-sdk//bazel/constraint:pico_btstack_config_unset",
+    ),
+    deps = ["@pico-sdk//bazel/config:PICO_BTSTACK_CONFIG"],
+)
+
+cc_library(
+    name = "pico_btstack_ble",
+    srcs = [
+        "src/ble/att_db.c",
+        "src/ble/att_db_util.c",
+        "src/ble/att_dispatch.c",
+        "src/ble/att_server.c",
+        "src/ble/gatt-service/ancs_client.c",
+        "src/ble/gatt-service/battery_service_client.c",
+        "src/ble/gatt-service/battery_service_server.c",
+        "src/ble/gatt-service/cycling_power_service_server.c",
+        "src/ble/gatt-service/cycling_speed_and_cadence_service_server.c",
+        "src/ble/gatt-service/device_information_service_client.c",
+        "src/ble/gatt-service/device_information_service_server.c",
+        "src/ble/gatt-service/heart_rate_service_server.c",
+        "src/ble/gatt-service/hids_client.c",
+        "src/ble/gatt-service/hids_device.c",
+        "src/ble/gatt-service/nordic_spp_service_server.c",
+        "src/ble/gatt-service/ublox_spp_service_server.c",
+        "src/ble/gatt_client.c",
+        "src/ble/le_device_db_memory.c",
+        "src/ble/le_device_db_tlv.c",
+        "src/ble/sm.c",
+    ],
+    copts = _DISABLE_WARNINGS,
+    deps = [":pico_btstack_base"],
+)
+
+cc_library(
+    name = "pico_btstack_classic",
+    srcs = [
+        "src/classic/a2dp.c",
+        "src/classic/a2dp_sink.c",
+        "src/classic/a2dp_source.c",
+        "src/classic/avdtp.c",
+        "src/classic/avdtp_acceptor.c",
+        "src/classic/avdtp_initiator.c",
+        "src/classic/avdtp_sink.c",
+        "src/classic/avdtp_source.c",
+        "src/classic/avdtp_util.c",
+        "src/classic/avrcp.c",
+        "src/classic/avrcp_browsing.c",
+        "src/classic/avrcp_browsing_controller.c",
+        "src/classic/avrcp_browsing_target.c",
+        "src/classic/avrcp_controller.c",
+        "src/classic/avrcp_cover_art_client.c",
+        "src/classic/avrcp_media_item_iterator.c",
+        "src/classic/avrcp_target.c",
+        "src/classic/btstack_cvsd_plc.c",
+        "src/classic/btstack_link_key_db_tlv.c",
+        "src/classic/btstack_sbc_plc.c",
+        "src/classic/device_id_server.c",
+        "src/classic/gatt_sdp.c",
+        "src/classic/goep_client.c",
+        "src/classic/goep_server.c",
+        "src/classic/hfp.c",
+        "src/classic/hfp_ag.c",
+        "src/classic/hfp_gsm_model.c",
+        "src/classic/hfp_hf.c",
+        "src/classic/hfp_msbc.c",
+        "src/classic/hid_device.c",
+        "src/classic/hid_host.c",
+        "src/classic/hsp_ag.c",
+        "src/classic/hsp_hs.c",
+        "src/classic/obex_iterator.c",
+        "src/classic/obex_message_builder.c",
+        "src/classic/obex_parser.c",
+        "src/classic/pan.c",
+        "src/classic/pbap_client.c",
+        "src/classic/rfcomm.c",
+        "src/classic/sdp_client.c",
+        "src/classic/sdp_client_rfcomm.c",
+        "src/classic/sdp_server.c",
+        "src/classic/sdp_util.c",
+        "src/classic/spp_server.c",
+    ],
+    copts = _DISABLE_WARNINGS,
+    deps = [":pico_btstack_base"],
+)
+
+cc_library(
+    name = "pico_btstack_sbc_encoder",
+    srcs = [
+        "3rd-party/bluedroid/encoder/srce/sbc_analysis.c",
+        "3rd-party/bluedroid/encoder/srce/sbc_dct.c",
+        "3rd-party/bluedroid/encoder/srce/sbc_dct_coeffs.c",
+        "3rd-party/bluedroid/encoder/srce/sbc_enc_bit_alloc_mono.c",
+        "3rd-party/bluedroid/encoder/srce/sbc_enc_bit_alloc_ste.c",
+        "3rd-party/bluedroid/encoder/srce/sbc_enc_coeffs.c",
+        "3rd-party/bluedroid/encoder/srce/sbc_encoder.c",
+        "3rd-party/bluedroid/encoder/srce/sbc_packing.c",
+        "src/classic/btstack_sbc_encoder_bluedroid.c",
+    ],
+    copts = _DISABLE_WARNINGS,
+    includes = ["3rd-party/bluedroid/decoder/include"],
+    deps = [":pico_btstack_base"],
+)
+
+cc_library(
+    name = "pico_btstack_bnep_lwip",
+    srcs = [
+        "platform/lwip/bnep_lwip.c",
+        "src/classic/bnep.c",
+    ],
+    copts = _DISABLE_WARNINGS,
+    includes = ["platform/lwip"],
+    deps = [":pico_btstack_base"],
+)
+
+cc_library(
+    name = "pico_btstack_bnep_lwip_sys_freertos",
+    srcs = [
+        "platform/lwip/bnep_lwip.c",
+        "src/classic/bnep.c",
+    ],
+    copts = _DISABLE_WARNINGS,
+    defines = [
+        "LWIP_PROVIDE_ERRNO=1",
+        "PICO_LWIP_CUSTOM_LOCK_TCPIP_CORE=1",
+    ],
+    includes = [
+        "platform/freertos",
+        "platform/lwip",
+    ],
+    deps = [":pico_btstack_base"],
+)
diff --git a/src/rp2_common/pico_cxx_options/BUILD.bazel b/src/rp2_common/pico_cxx_options/BUILD.bazel
index 6fb8268..adcabd9 100644
--- a/src/rp2_common/pico_cxx_options/BUILD.bazel
+++ b/src/rp2_common/pico_cxx_options/BUILD.bazel
@@ -1,5 +1,8 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "pico_cxx_options",
+    target_compatible_with = compatible_with_rp2(),
 )
diff --git a/src/rp2_common/pico_cyw43_arch/BUILD.bazel b/src/rp2_common/pico_cyw43_arch/BUILD.bazel
index 9ce4516..a17fc2a 100644
--- a/src/rp2_common/pico_cyw43_arch/BUILD.bazel
+++ b/src/rp2_common/pico_cyw43_arch/BUILD.bazel
@@ -1,31 +1,61 @@
+load("//bazel:defs.bzl", "compatible_with_pico_w", "incompatible_with_config")
+
 package(default_visibility = ["//visibility:public"])
 
-cc_library(
-    name = "pico_cyw43_arch",
-    srcs = [
-        "cyw43_arch.c",
-        "cyw43_arch_freertos.c",
-        "cyw43_arch_poll.c",
-        "cyw43_arch_threadsafe_background.c",
-    ],
-    hdrs = [
-        "include/pico/cyw43_arch.h",
-        "include/pico/cyw43_arch/arch_freertos.h",
-        "include/pico/cyw43_arch/arch_poll.h",
-        "include/pico/cyw43_arch/arch_threadsafe_background.h",
-    ],
-    includes = ["include"],
-    # Missing deps for:
-    #     cyw43.h
-    #     cyw43_country.h
-    #     cyw43_ll.h
-    #     cyw43_stats.h
-    tags = ["manual"],
-    deps = [
-        "//src/common/pico_base",
-        "//src/rp2_common/pico_async_context",
-        "//src/rp2_common/pico_cyw43_driver",
-        "//src/rp2_common/pico_lwip",
-        "//src/rp2_common/pico_unique_id",
-    ],
+# Tuple is async_context type and whether or not lwip is enabled.
+_CONFIGURATIONS = [
+    ("freertos", False),
+    ("freertos", True),
+    ("poll", False),
+    ("poll", True),
+    ("threadsafe_background", False),
+    ("threadsafe_background", True),
+]
+
+# This produces the following labels:
+#   pico_cyw43_arch_sys_freertos
+#   pico_cyw43_arch_lwip_sys_freertos
+#   pico_cyw43_arch_poll
+#   pico_cyw43_arch_lwip_poll
+#   pico_cyw43_arch_threadsafe_background
+#   pico_cyw43_arch_lwip_threadsafe_background
+#
+# This is done rather than having intermediate libraries because the defines
+# for a given configuration must be applied to both .c files.
+[
+    cc_library(
+        name = "pico_cyw43_arch_" + ("lwip_" if use_lwip else "") + kind,
+        srcs = [
+            "cyw43_arch.c",
+            "cyw43_arch_{}.c".format(kind),
+        ],
+        hdrs = [
+            "include/pico/cyw43_arch.h",
+            "include/pico/cyw43_arch/arch_{}.h".format(kind),
+        ],
+        defines = [
+            "LIB_PICO_CYW43_ARCH=1",
+            "PICO_CYW43_ARCH_{}=1".format(kind.upper()),
+            "CYW43_LWIP={}".format(1 if use_lwip else 0),
+        ],
+        includes = ["include"],
+        target_compatible_with = compatible_with_pico_w() + (
+            incompatible_with_config("//bazel/constraint:pico_freertos_unset") if kind == "freertos" else []
+        ),
+        deps = [
+            "//src/common/pico_base",
+            "//src/rp2_common/pico_async_context:pico_async_context_{}".format(kind),
+            "//src/rp2_common/pico_cyw43_driver",
+            "//src/rp2_common/pico_lwip",
+            "//src/rp2_common/pico_unique_id",
+        ] + (
+            ["//src/rp2_common/pico_lwip:pico_lwip_freertos"] if kind == "freertos" else ["//src/rp2_common/pico_lwip:pico_lwip_nosys"]
+        ),
+    )
+    for kind, use_lwip in _CONFIGURATIONS
+]
+
+alias(
+    name = "pico_cyw43_arch_none",
+    actual = ":pico_cyw43_arch_threadsafe_background",
 )
diff --git a/src/rp2_common/pico_cyw43_driver/BUILD.bazel b/src/rp2_common/pico_cyw43_driver/BUILD.bazel
index 1d2fa13..f1286a0 100644
--- a/src/rp2_common/pico_cyw43_driver/BUILD.bazel
+++ b/src/rp2_common/pico_cyw43_driver/BUILD.bazel
@@ -1,6 +1,19 @@
+load("//bazel:defs.bzl", "compatible_with_pico_w", "pico_generate_pio_header")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
+    name = "cyw43_configport",
+    hdrs = ["include/cyw43_configport.h"],
+    includes = ["include"],
+    deps = [
+        "//src/common/pico_base",
+        "//src/common/pico_time",
+        "//src/rp2_common/hardware_gpio",
+    ],
+)
+
+cc_library(
     name = "pico_cyw43_driver",
     srcs = [
         "btstack_chipset_cyw43.c",
@@ -10,42 +23,33 @@
         "cyw43_driver.c",
     ],
     hdrs = [
-        "include/cyw43_configport.h",
         "include/pico/btstack_chipset_cyw43.h",
         "include/pico/btstack_cyw43.h",
         "include/pico/btstack_hci_transport_cyw43.h",
         "include/pico/cyw43_driver.h",
     ],
     includes = ["include"],
-    # Missing deps for:
-    #     ble/le_device_db_tlv.h
-    #     btstack_chipset.h
-    #     btstack_memory.h
-    #     btstack_tlv.h
-    #     btstack_tlv_flash_bank.h
-    #     classic/btstack_link_key_db_tlv.h
-    #     cyw43.h
-    #     cyw43_bus_pio_spi.pio.h
-    #     cyw43_debug_pins.h
-    #     cyw43_internal.h
-    #     cyw43_spi.h
-    #     hci.h
-    #     hci_dump.h
-    #     hci_dump_embedded_stdout.h
-    #     hci_dump_segger_rtt_stdout.h
-    #     hci_transport.h
-    tags = ["manual"],
+    target_compatible_with = compatible_with_pico_w(),
     deps = [
+        ":cyw43_bus_pio",
+        ":cyw43_configport",
+        "//bazel/config:PICO_BTSTACK_CONFIG",
         "//src/common/pico_base",
-        "//src/common/pico_time",
         "//src/rp2_common/hardware_clocks",
         "//src/rp2_common/hardware_dma",
-        "//src/rp2_common/hardware_gpio",
         "//src/rp2_common/hardware_irq",
         "//src/rp2_common/hardware_pio",
         "//src/rp2_common/hardware_sync",
         "//src/rp2_common/pico_async_context",
-        "//src/rp2_common/pico_btstack",
+        "//src/rp2_common/pico_btstack:btstack_run_loop_async_context",
+        "//src/rp2_common/pico_btstack:pico_btstack_base",
+        "//src/rp2_common/pico_btstack:pico_btstack_flash_bank",
         "//src/rp2_common/pico_unique_id",
+        "@cyw43-driver//:cyw43_driver",
     ],
 )
+
+pico_generate_pio_header(
+    name = "cyw43_bus_pio",
+    srcs = ["cyw43_bus_pio_spi.pio"],
+)
diff --git a/src/rp2_common/pico_cyw43_driver/cyw43-driver.BUILD b/src/rp2_common/pico_cyw43_driver/cyw43-driver.BUILD
new file mode 100644
index 0000000..99b6fae
--- /dev/null
+++ b/src/rp2_common/pico_cyw43_driver/cyw43-driver.BUILD
@@ -0,0 +1,24 @@
+load("@pico-sdk//bazel:defs.bzl", "compatible_with_pico_w")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "cyw43_driver",
+    srcs = [
+        "src/cyw43_ctrl.c",
+        "src/cyw43_ll.c",
+        "src/cyw43_lwip.c",
+        "src/cyw43_stats.c",
+    ],
+    hdrs = glob(["**/*.h"]),
+    defines = ["CYW43_ENABLE_BLUETOOTH=1"],
+    includes = [
+        "firmware",
+        "src",
+    ],
+    target_compatible_with = compatible_with_pico_w(),
+    deps = [
+        "@pico-sdk//src/rp2_common/pico_cyw43_driver:cyw43_configport",
+        "@pico-sdk//src/rp2_common/pico_lwip",
+    ],
+)
diff --git a/src/rp2_common/pico_divider/BUILD.bazel b/src/rp2_common/pico_divider/BUILD.bazel
index f35f225..074359a 100644
--- a/src/rp2_common/pico_divider/BUILD.bazel
+++ b/src/rp2_common/pico_divider/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -11,6 +13,7 @@
         "-Wl,--wrap=__aeabi_uidivmod",
         "-Wl,--wrap=__aeabi_uldivmod",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/rp2_common/hardware_divider",
         "//src/rp2_common/pico_platform",
diff --git a/src/rp2_common/pico_double/BUILD.bazel b/src/rp2_common/pico_double/BUILD.bazel
index f52e78b..c9cba57 100644
--- a/src/rp2_common/pico_double/BUILD.bazel
+++ b/src/rp2_common/pico_double/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -73,6 +75,7 @@
         "-Wl,--wrap=log1p",
         "-Wl,--wrap=fma",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_divider",
diff --git a/src/rp2_common/pico_fix/BUILD.bazel b/src/rp2_common/pico_fix/BUILD.bazel
index a1b4b2e..7909e5f 100644
--- a/src/rp2_common/pico_fix/BUILD.bazel
+++ b/src/rp2_common/pico_fix/BUILD.bazel
@@ -1,5 +1,8 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "pico_fix",
+    target_compatible_with = compatible_with_rp2(),
 )
diff --git a/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.bazel b/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.bazel
index 7b1406c..01b91c7 100644
--- a/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.bazel
+++ b/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["rp2040_usb_device_enumeration.c"],
     hdrs = ["include/pico/fix/rp2040_usb_device_enumeration.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_time",
diff --git a/src/rp2_common/pico_flash/BUILD.bazel b/src/rp2_common/pico_flash/BUILD.bazel
index d9aac18..3580498 100644
--- a/src/rp2_common/pico_flash/BUILD.bazel
+++ b/src/rp2_common/pico_flash/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,10 +7,7 @@
     srcs = ["flash.c"],
     hdrs = ["include/pico/flash.h"],
     includes = ["include"],
-    # Missing deps for:
-    #     FreeRTOS.h
-    #     task.h
-    tags = ["manual"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_time",
@@ -16,5 +15,8 @@
         "//src/rp2_common/hardware_flash",
         "//src/rp2_common/hardware_sync",
         "//src/rp2_common/pico_multicore",
-    ],
+    ] + select({
+        "//bazel/constraint:pico_freertos_unset": [],
+        "//conditions:default": ["//bazel/config:PICO_FREERTOS_LIB"],
+    }),
 )
diff --git a/src/rp2_common/pico_float/BUILD.bazel b/src/rp2_common/pico_float/BUILD.bazel
index c3cef5e..5383da1 100644
--- a/src/rp2_common/pico_float/BUILD.bazel
+++ b/src/rp2_common/pico_float/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -73,6 +75,7 @@
         "-Wl,--wrap=log1pf",
         "-Wl,--wrap=fmaf",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_divider",
diff --git a/src/rp2_common/pico_i2c_slave/BUILD.bazel b/src/rp2_common/pico_i2c_slave/BUILD.bazel
index 36e0b9b..739b331 100644
--- a/src/rp2_common/pico_i2c_slave/BUILD.bazel
+++ b/src/rp2_common/pico_i2c_slave/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["i2c_slave.c"],
     hdrs = ["include/pico/i2c_slave.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/rp2_common/hardware_i2c",
         "//src/rp2_common/hardware_irq",
diff --git a/src/rp2_common/pico_int64_ops/BUILD.bazel b/src/rp2_common/pico_int64_ops/BUILD.bazel
index 6311103..a72dc17 100644
--- a/src/rp2_common/pico_int64_ops/BUILD.bazel
+++ b/src/rp2_common/pico_int64_ops/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -6,6 +8,7 @@
     hdrs = ["include/pico/int64_ops.h"],
     includes = ["include"],
     linkopts = ["-Wl,--wrap=__aeabi_lmul"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/pico_platform",
diff --git a/src/rp2_common/pico_lwip/BUILD.bazel b/src/rp2_common/pico_lwip/BUILD.bazel
index 76799a5..8b7e9ec 100644
--- a/src/rp2_common/pico_lwip/BUILD.bazel
+++ b/src/rp2_common/pico_lwip/BUILD.bazel
@@ -1,27 +1,144 @@
+load("//bazel:defs.bzl", "compatible_with_pico_w")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
-    name = "pico_lwip",
-    srcs = [
-        "lwip_freertos.c",
-        "lwip_nosys.c",
-    ],
-    hdrs = [
-        "include/arch/cc.h",
-        "include/pico/lwip_freertos.h",
-        "include/pico/lwip_nosys.h",
-    ],
+    name = "pico_lwip_config",
+    hdrs = ["include/arch/cc.h"],
     includes = ["include"],
-    # Missing deps for:
-    #     FreeRTOS.h
-    #     lwip/tcpip.h
-    #     lwip/timeouts.h
-    #     semphr.h
-    tags = ["manual"],
     deps = [
         "//src/common/pico_base",
+        "//src/rp2_common/pico_rand",
+    ],
+)
+
+cc_library(
+    name = "pico_lwip_freertos",
+    srcs = ["lwip_freertos.c"],
+    hdrs = ["include/pico/lwip_freertos.h"],
+    includes = ["include"],
+    target_compatible_with = compatible_with_pico_w(),
+    deps = [
+        ":pico_lwip",
+        "//src/common/pico_base",
         "//src/common/pico_time",
         "//src/rp2_common/pico_async_context",
         "//src/rp2_common/pico_rand",
     ],
 )
+
+cc_library(
+    name = "pico_lwip_nosys",
+    srcs = ["lwip_nosys.c"],
+    hdrs = ["include/pico/lwip_nosys.h"],
+    target_compatible_with = compatible_with_pico_w(),
+    deps = [
+        ":pico_lwip",
+        "//src/common/pico_base",
+        "//src/common/pico_time",
+        "//src/rp2_common/pico_async_context",
+        "//src/rp2_common/pico_rand",
+    ],
+)
+
+# Prefer these aliases to directly referencing @lwip, as it's possible that
+# name may change.
+alias(
+    name = "pico_lwip",
+    actual = "@lwip//:pico_lwip",
+)
+
+alias(
+    name = "pico_lwip_core",
+    actual = "@lwip//:pico_lwip_core",
+)
+
+alias(
+    name = "pico_lwip_core4",
+    actual = "@lwip//:pico_lwip_core4",
+)
+
+alias(
+    name = "pico_lwip_core6",
+    actual = "@lwip//:pico_lwip_core6",
+)
+
+alias(
+    name = "pico_lwip_api",
+    actual = "@lwip//:pico_lwip_api",
+)
+
+alias(
+    name = "pico_lwip_netif",
+    actual = "@lwip//:pico_lwip_netif",
+)
+
+alias(
+    name = "pico_lwip_sixlowpan",
+    actual = "@lwip//:pico_lwip_sixlowpan",
+)
+
+alias(
+    name = "pico_lwip_ppp",
+    actual = "@lwip//:pico_lwip_ppp",
+)
+
+alias(
+    name = "pico_lwip_snmp",
+    actual = "@lwip//:pico_lwip_snmp",
+)
+
+alias(
+    name = "pico_lwip_http",
+    actual = "@lwip//:pico_lwip_http",
+)
+
+alias(
+    name = "pico_lwip_makefsdata",
+    actual = "@lwip//:pico_lwip_makefsdata",
+)
+
+alias(
+    name = "pico_lwip_iperf",
+    actual = "@lwip//:pico_lwip_iperf",
+)
+
+alias(
+    name = "pico_lwip_smtp",
+    actual = "@lwip//:pico_lwip_smtp",
+)
+
+alias(
+    name = "pico_lwip_sntp",
+    actual = "@lwip//:pico_lwip_sntp",
+)
+
+alias(
+    name = "pico_lwip_mdns",
+    actual = "@lwip//:pico_lwip_mdns",
+)
+
+alias(
+    name = "pico_lwip_netbios",
+    actual = "@lwip//:pico_lwip_netbios",
+)
+
+alias(
+    name = "pico_lwip_tftp",
+    actual = "@lwip//:pico_lwip_tftp",
+)
+
+alias(
+    name = "pico_lwip_mbedtls",
+    actual = "@lwip//:pico_lwip_mbedtls",
+)
+
+alias(
+    name = "pico_lwip_mqttt",
+    actual = "@lwip//:pico_lwip_mqttt",
+)
+
+alias(
+    name = "pico_lwip_contrib_freertos",
+    actual = "@lwip//:pico_lwip_contrib_freertos",
+)
diff --git a/src/rp2_common/pico_lwip/lwip.BUILD b/src/rp2_common/pico_lwip/lwip.BUILD
new file mode 100644
index 0000000..83e2be7
--- /dev/null
+++ b/src/rp2_common/pico_lwip/lwip.BUILD
@@ -0,0 +1,163 @@
+load("@pico-sdk//bazel:defs.bzl", "incompatible_with_config")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "pico_lwip_core",
+    srcs = glob(["src/core/*.c"]),
+    hdrs = glob(["**/*.h"]),
+    includes = ["src/include"],
+    target_compatible_with = incompatible_with_config(
+        "@pico-sdk//bazel/constraint:pico_lwip_config_unset",
+    ),
+    deps = [
+        "@pico-sdk//bazel/config:PICO_LWIP_CONFIG",
+        "@pico-sdk//src/rp2_common/pico_lwip:pico_lwip_config",
+    ],
+)
+
+cc_library(
+    name = "pico_lwip_core4",
+    srcs = glob(["src/core/ipv4/*.c"]),
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_core6",
+    srcs = glob(["src/core/ipv6/*.c"]),
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_api",
+    srcs = glob(["src/api/*.c"]),
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_netif",
+    srcs = [
+        "src/netif/bridgeif.c",
+        "src/netif/bridgeif_fdb.c",
+        "src/netif/ethernet.c",
+        "src/netif/slipif.c",
+    ],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_sixlowpan",
+    srcs = [
+        "src/netif/lowpan6.c",
+        "src/netif/lowpan6_ble.c",
+        "src/netif/lowpan6_common.c",
+        "src/netif/zepif.c",
+    ],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_ppp",
+    srcs = glob(["src/netif/ppp/*/*.c"]),
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_snmp",
+    srcs = glob(
+        ["src/apps/snmp/*.c"],
+        # mbedtls is provided through pico_lwip_mbedtls.
+        exclude = ["*mbedtls.c"],
+    ),
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_http",
+    srcs = glob(["src/apps/http/*.c"]),
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_makefsdata",
+    srcs = ["src/apps/http/makefsdata/makefsdata.c"],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_iperf",
+    srcs = ["src/apps/lwiperf/lwiperf.c"],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_smtp",
+    srcs = ["src/apps/smtp/smtp.c"],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_sntp",
+    srcs = ["src/apps/sntp/sntp.c"],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_mdns",
+    srcs = glob(["src/apps/mdns/*.c"]),
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_netbios",
+    srcs = ["src/apps/netbiosns/netbiosns.c"],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_tftp",
+    srcs = ["src/apps/tftp/tftp.c"],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_mbedtls",
+    srcs = [
+        "src/apps/altcp_tls/altcp_tls_mbedtls.c",
+        "src/apps/altcp_tls/altcp_tls_mbedtls_mem.c",
+        "src/apps/snmp/snmpv3_mbedtls.c",
+    ],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip_mqttt",
+    srcs = ["src/apps/mqtt/mqtt.c"],
+    deps = [":pico_lwip_core"],
+)
+
+cc_library(
+    name = "pico_lwip",
+    deps = [
+        ":pico_lwip_api",
+        ":pico_lwip_core",
+        ":pico_lwip_core4",
+        ":pico_lwip_core6",
+        ":pico_lwip_netif",
+        ":pico_lwip_ppp",
+        ":pico_lwip_sixlowpan",
+    ],
+)
+
+cc_library(
+    name = "pico_lwip_contrib_freertos",
+    srcs = ["ports/freertos/sys_arch.c"],
+    includes = ["ports/freertos/include"],
+    target_compatible_with = incompatible_with_config(
+        "@pico-sdk//bazel/constraint:pico_freertos_unset",
+    ),
+    deps = [
+        ":pico_lwip_core",
+        "//bazel/config:PICO_FREERTOS_LIB",
+    ],
+)
diff --git a/src/rp2_common/pico_malloc/BUILD.bazel b/src/rp2_common/pico_malloc/BUILD.bazel
index 7aa6bae..09e852c 100644
--- a/src/rp2_common/pico_malloc/BUILD.bazel
+++ b/src/rp2_common/pico_malloc/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -11,6 +13,7 @@
         "-Wl,--wrap=realloc",
         "-Wl,--wrap=free",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_sync",
diff --git a/src/rp2_common/pico_mbedtls/BUILD.bazel b/src/rp2_common/pico_mbedtls/BUILD.bazel
index 8f9a64b..e812524 100644
--- a/src/rp2_common/pico_mbedtls/BUILD.bazel
+++ b/src/rp2_common/pico_mbedtls/BUILD.bazel
@@ -1,8 +1,11 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "pico_mbedtls",
     srcs = ["pico_mbedtls.c"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/rp2_common/pico_platform",
         "//src/rp2_common/pico_rand",
diff --git a/src/rp2_common/pico_mem_ops/BUILD.bazel b/src/rp2_common/pico_mem_ops/BUILD.bazel
index 7d1c965..e890d8e 100644
--- a/src/rp2_common/pico_mem_ops/BUILD.bazel
+++ b/src/rp2_common/pico_mem_ops/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -18,6 +20,7 @@
         "-Wl,--wrap=__aeabi_memcpy8",
         "-Wl,--wrap=__aeabi_memset8",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/pico_bootrom",
diff --git a/src/rp2_common/pico_multicore/BUILD.bazel b/src/rp2_common/pico_multicore/BUILD.bazel
index 220edd4..73f5002 100644
--- a/src/rp2_common/pico_multicore/BUILD.bazel
+++ b/src/rp2_common/pico_multicore/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["multicore.c"],
     hdrs = ["include/pico/multicore.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_sync",
diff --git a/src/rp2_common/pico_platform/BUILD.bazel b/src/rp2_common/pico_platform/BUILD.bazel
index 0655b41..a810297 100644
--- a/src/rp2_common/pico_platform/BUILD.bazel
+++ b/src/rp2_common/pico_platform/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 alias(
@@ -5,6 +7,7 @@
     actual = select({
         "//bazel/constraint:rp2040": "//src/rp2040/hardware_regs:platform_defs",
     }),
+    target_compatible_with = compatible_with_rp2(),
 )
 
 alias(
@@ -12,6 +15,7 @@
     actual = select({
         "//bazel/constraint:rp2040": "//src/rp2040/hardware_regs:hardware_regs",
     }),
+    target_compatible_with = compatible_with_rp2(),
 )
 
 alias(
@@ -19,6 +23,7 @@
     actual = select({
         "//bazel/constraint:rp2040": "//src/rp2040/hardware_structs:hardware_structs",
     }),
+    target_compatible_with = compatible_with_rp2(),
 )
 
 cc_library(
@@ -29,6 +34,7 @@
         "include/pico/platform.h",
     ],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         ":hardware_regs",
         ":platform_defs",
@@ -36,3 +42,20 @@
         "//src/rp2_common/hardware_base",
     ],
 )
+
+cc_library(
+    name = "platform_link_deps",
+    target_compatible_with = compatible_with_rp2(),
+    deps = select({
+        # When PICO_BARE_METAL is enabled, don't automagically add link-time
+        # dependencies.
+        "//bazel/constraint:pico_baremetal_enabled": [],
+        "//conditions:default": [
+            "//bazel/config:PICO_BOOT_STAGE2_LINK_IMAGE",
+            "//bazel/config:PICO_CMSIS_PATH",
+            "//src/rp2_common/pico_bootrom",
+            "//src/rp2_common/pico_runtime",
+            "//src/rp2_common/pico_standard_link",
+        ],
+    }),
+)
diff --git a/src/rp2_common/pico_printf/BUILD.bazel b/src/rp2_common/pico_printf/BUILD.bazel
index d289845..95d1b24 100644
--- a/src/rp2_common/pico_printf/BUILD.bazel
+++ b/src/rp2_common/pico_printf/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -12,6 +14,7 @@
         "-Wl,--wrap=snprintf",
         "-Wl,--wrap=vsnprintf",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/rp2_common/pico_bootrom",
diff --git a/src/rp2_common/pico_rand/BUILD.bazel b/src/rp2_common/pico_rand/BUILD.bazel
index 3ef576c..c24e6a9 100644
--- a/src/rp2_common/pico_rand/BUILD.bazel
+++ b/src/rp2_common/pico_rand/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["rand.c"],
     hdrs = ["include/pico/rand.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_time",
diff --git a/src/rp2_common/pico_runtime/BUILD.bazel b/src/rp2_common/pico_runtime/BUILD.bazel
index f3f9bff..32e4473 100644
--- a/src/rp2_common/pico_runtime/BUILD.bazel
+++ b/src/rp2_common/pico_runtime/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["runtime.c"],
     hdrs = ["include/pico/runtime.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base:pico_base_interface",
         "//src/common/pico_sync",
@@ -17,4 +20,5 @@
         "//src/rp2_common/pico_platform:hardware_structs",
         "//src/rp2_common/pico_printf",
     ],
+    alwayslink = True,
 )
diff --git a/src/rp2_common/pico_standard_link/BUILD.bazel b/src/rp2_common/pico_standard_link/BUILD.bazel
index bfc1376..536909e 100644
--- a/src/rp2_common/pico_standard_link/BUILD.bazel
+++ b/src/rp2_common/pico_standard_link/BUILD.bazel
@@ -1,21 +1,103 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+load("//bazel/util:sdk_define.bzl", "pico_sdk_define")
+load("//src/common/pico_binary_info:binary_info.bzl", "custom_pico_binary_info")
+
 package(default_visibility = ["//visibility:public"])
 
+# This is exposed so that custom_pico_binary_info targets can find the required
+# source files.
+filegroup(
+    name = "binary_info_srcs",
+    srcs = ["binary_info.c"],
+)
+
+# PICO_BUILD_DEFINE: PICO_CXX_ENABLE_EXCEPTIONS, value of CMake var PICO_CXX_ENABLE_EXCEPTIONS, type=string, default=0, group=pico_cxx_options
+pico_sdk_define(
+    name = "PICO_CXX_ENABLE_EXCEPTIONS",
+    define_name = "PICO_CXX_ENABLE_EXCEPTIONS",
+    from_flag = "//bazel/config:PICO_CXX_ENABLE_EXCEPTIONS",
+)
+
+# PICO_BUILD_DEFINE: PICO_CMAKE_BUILD_TYPE, The type of build (e.g. Debug or Release) to embed in binary info, type=string, default=pico, group=build
+pico_sdk_define(
+    name = "PICO_BAZEL_BUILD_TYPE",
+    # WARNING: The actual define is named after CMake, so that define
+    # is reused for compatibility.
+    define_name = "PICO_CMAKE_BUILD_TYPE",
+    from_flag = "//bazel/config:PICO_BAZEL_BUILD_TYPE",
+)
+
+# With custom_pico_binary_info, it's possible to set binary info globally or
+# on a per-binary basis.
+#
+# Setting globally:
+#   * Set --@pico-sdk//bazel/config:PICO_DEFAULT_BINARY_INFO to point to your
+#     custom custom_pico_binary_info.
+#
+# Setting per-binary:
+#   * Set --@pico-sdk//bazel/config:PICO_DEFAULT_BINARY_INFO=@pico-sdk//bazel:empty_cc_lib
+#   * Copy this cc_library, and manually set the values as you wish.
+#   * Add custom_pico_binary_info to each cc_binary individually.
+custom_pico_binary_info(
+    name = "default_binary_info",
+    program_description = None,
+    program_name = None,
+    program_url = None,
+    program_version_string = None,
+)
+
+# It's possible to set linker scripts globally or on a per-binary basis.
+#
+# Setting globally:
+#   * Set --@pico-sdk//bazel/config:PICO_DEFAULT_LINKER_SCRIPT to point to your
+#     desired linker script.
+#
+# Setting per-binary:
+#   * Set --@pico-sdk//bazel/config:PICO_DEFAULT_LINKER_SCRIPT=@pico-sdk//bazel:empty_cc_lib
+#   * Manually add your desired linker script to each cc_binary.
+cc_library(
+    name = "default_linker_script",
+    linkopts = ["-T$(location memmap_default.ld)"],
+    deps = ["memmap_default.ld"],
+)
+
+# PICO_BUILD_DEFINE: PICO_USE_BLOCKED_RAM, whether this is a 'blocked_ram' build, type=bool, default=0, but dependent on CMake options, group=pico_standard_link
+cc_library(
+    name = "blocked_ram_linker_script",
+    defines = ["PICO_USE_BLOCKED_RAM=1"],
+    linkopts = ["-T$(location memmap_blocked_ram.ld)"],
+    deps = ["memmap_blocked_ram.ld"],
+)
+
+# PICO_BUILD_DEFINE: PICO_COPY_TO_RAM, whether this is a 'copy_to_ram' build, type=bool, default=0, but dependent on CMake options, group=pico_standard_link
+cc_library(
+    name = "copy_to_ram_linker_script",
+    defines = ["PICO_COPY_TO_RAM=1"],
+    linkopts = ["-T$(location memmap_copy_to_ram.ld)"],
+    deps = ["memmap_copy_to_ram.ld"],
+)
+
+# PICO_BUILD_DEFINE: PICO_NO_FLASH, whether this is a 'no_flash' build, type=bool, default=0, but dependent on CMake options, group=pico_standard_link
+cc_library(
+    name = "no_flash_linker_script",
+    defines = ["PICO_NO_FLASH=1"],
+    linkopts = ["-T$(location memmap_no_flash.ld)"],
+    deps = ["memmap_no_flash.ld"],
+)
+
 cc_library(
     name = "pico_standard_link",
     srcs = [
-        "binary_info.c",
         "crt0.S",
-        # "new_delete.cpp",  # TODO: Doesn't build yet?
+        "new_delete.cpp",
     ],
-    # TODO: Make this configurable.
-    linkopts = [
-        "-T$(location memmap_default.ld)",
-    ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
-        "memmap_default.ld",
+        ":PICO_CXX_ENABLE_EXCEPTIONS",
+        "//bazel/config:PICO_DEFAULT_BINARY_INFO",
+        "//bazel/config:PICO_DEFAULT_LINKER_SCRIPT",
         "//src/common/pico_base:pico_base_interface",
-        "//src/common/pico_binary_info",
-        "//src/rp2_common/boot_stage2:config",
+        "//src/rp2_common/cmsis:cmsis_core",
         "//src/rp2_common/pico_bootrom",
         "//src/rp2_common/pico_platform",
         "//src/rp2_common/pico_platform:hardware_regs",
diff --git a/src/rp2_common/pico_stdio/BUILD.bazel b/src/rp2_common/pico_stdio/BUILD.bazel
index d380041..1bc967f 100644
--- a/src/rp2_common/pico_stdio/BUILD.bazel
+++ b/src/rp2_common/pico_stdio/BUILD.bazel
@@ -1,5 +1,35 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+load("//bazel/util:sdk_define.bzl", "pico_sdk_define")
+
 package(default_visibility = ["//visibility:public"])
 
+pico_sdk_define(
+    name = "LIB_PICO_STDIO_UART",
+    define_name = "LIB_PICO_STDIO_UART",
+    from_flag = "//bazel/config:PICO_STDIO_UART",
+)
+
+pico_sdk_define(
+    name = "LIB_PICO_STDIO_USB",
+    define_name = "LIB_PICO_STDIO_USB",
+    from_flag = "//bazel/config:PICO_STDIO_USB",
+)
+
+pico_sdk_define(
+    name = "LIB_PICO_STDIO_SEMIHOSTING",
+    define_name = "LIB_PICO_STDIO_SEMIHOSTING",
+    from_flag = "//bazel/config:PICO_STDIO_SEMIHOSTING",
+)
+
+cc_library(
+    name = "stdio_defines",
+    deps = [
+        ":LIB_PICO_STDIO_SEMIHOSTING",
+        ":LIB_PICO_STDIO_UART",
+        ":LIB_PICO_STDIO_USB",
+    ],
+)
+
 # This exists to break dependency cycles between
 # this library and the stdio implementations.
 # Application code should always use :pico_stdio instead.
@@ -10,12 +40,14 @@
         "include/pico/stdio/driver.h",
     ],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     visibility = [
         "//src/rp2_common/pico_stdio_semihosting:__pkg__",
         "//src/rp2_common/pico_stdio_uart:__pkg__",
         "//src/rp2_common/pico_stdio_usb:__pkg__",
         "//src/rp2_common/tinyusb:__pkg__",
     ],
+    deps = [":stdio_defines"],
 )
 
 cc_library(
@@ -33,15 +65,22 @@
         "-Wl,--wrap=putchar",
         "-Wl,--wrap=getchar",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
+        ":stdio_defines",
         "//src/common/pico_base",
         "//src/common/pico_sync",
         "//src/common/pico_time",
         "//src/rp2_common/pico_printf",
     ] + select({
-        "//bazel/constraint:stdio_semihosting": ["//src/rp2_common/pico_stdio_semihosting"],
-        "//bazel/constraint:stdio_uart": ["//src/rp2_common/pico_stdio_uart"],
-        "//bazel/constraint:stdio_usb": ["//src/rp2_common/pico_stdio_usb"],
+        "//bazel/constraint:pico_stdio_semihosting_enabled": ["//src/rp2_common/pico_stdio_semihosting"],
+        "//conditions:default": [],
+    }) + select({
+        "//bazel/constraint:pico_stdio_uart_enabled": ["//src/rp2_common/pico_stdio_uart"],
+        "//conditions:default": [],
+    }) + select({
+        "//bazel/constraint:pico_stdio_usb_enabled": ["//src/rp2_common/pico_stdio_usb"],
+        "//conditions:default": [],
     }),
     alwayslink = True,  # Ensures the wrapped symbols are linked in.
 )
diff --git a/src/rp2_common/pico_stdio_semihosting/BUILD.bazel b/src/rp2_common/pico_stdio_semihosting/BUILD.bazel
index 5ce2a69..473e37a 100644
--- a/src/rp2_common/pico_stdio_semihosting/BUILD.bazel
+++ b/src/rp2_common/pico_stdio_semihosting/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["stdio_semihosting.c"],
     hdrs = ["include/pico/stdio_semihosting.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_binary_info",
         "//src/rp2_common/pico_stdio:pico_stdio_headers",
diff --git a/src/rp2_common/pico_stdio_uart/BUILD.bazel b/src/rp2_common/pico_stdio_uart/BUILD.bazel
index 3ca9968..c329935 100644
--- a/src/rp2_common/pico_stdio_uart/BUILD.bazel
+++ b/src/rp2_common/pico_stdio_uart/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["stdio_uart.c"],
     hdrs = ["include/pico/stdio_uart.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_binary_info",
         "//src/rp2_common/hardware_gpio",
diff --git a/src/rp2_common/pico_stdio_usb/BUILD.bazel b/src/rp2_common/pico_stdio_usb/BUILD.bazel
index 58c3cdb..77c5d6f 100644
--- a/src/rp2_common/pico_stdio_usb/BUILD.bazel
+++ b/src/rp2_common/pico_stdio_usb/BUILD.bazel
@@ -1,5 +1,9 @@
+load("//bazel:defs.bzl", "compatible_with_config", "compatible_with_rp2")
+load("//bazel/util:sdk_define.bzl", "pico_sdk_define")
+
 package(default_visibility = ["//visibility:public"])
 
+# Picotool requires this, so it should work on host as well as rp2.
 cc_library(
     name = "reset_interface_headers",
     hdrs = ["include/pico/stdio_usb/reset_interface.h"],
@@ -11,16 +15,25 @@
     name = "tusb_config",
     hdrs = ["include/tusb_config.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
+)
+
+pico_sdk_define(
+    name = "PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS",
+    define_name = "PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS",
+    from_flag = "//bazel/config:PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS",
 )
 
 cc_library(
     name = "pico_stdio_usb_headers",
     hdrs = ["include/pico/stdio_usb.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     visibility = [
         ":__pkg__",
         "//src/rp2_common/tinyusb:__pkg__",
     ],
+    deps = [":PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS"],
 )
 
 cc_library(
@@ -30,10 +43,14 @@
         "stdio_usb.c",
         "stdio_usb_descriptors.c",
     ],
-    target_compatible_with = ["//bazel/constraint:stdio_usb"],
+    target_compatible_with = compatible_with_config(
+        "//bazel/constraint:pico_stdio_usb_enabled",
+    ) + compatible_with_rp2(),
     deps = [
+        ":PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS",
         ":pico_stdio_usb_headers",
         ":reset_interface_headers",
+        "//bazel/config:PICO_TINYUSB_LIB",
         "//src/common/pico_binary_info",
         "//src/common/pico_sync",
         "//src/rp2_common/hardware_irq",
@@ -41,7 +58,6 @@
         "//src/rp2_common/pico_bootrom",
         "//src/rp2_common/pico_stdio:pico_stdio_headers",
         "//src/rp2_common/pico_unique_id",
-        "@tinyusb",
     ],
     # Ensure `stdio_usb_descriptors.c` isn't affected by link order.
     alwayslink = True,
diff --git a/src/rp2_common/pico_stdlib/BUILD.bazel b/src/rp2_common/pico_stdlib/BUILD.bazel
index 53b01e1..c7a7128 100644
--- a/src/rp2_common/pico_stdlib/BUILD.bazel
+++ b/src/rp2_common/pico_stdlib/BUILD.bazel
@@ -1,16 +1,24 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
     name = "pico_stdlib",
     srcs = ["stdlib.c"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_binary_info",
         "//src/common/pico_stdlib:pico_stdlib_interface",
         "//src/rp2_common/hardware_clocks",
         "//src/rp2_common/hardware_pll",
     ] + select({
-        "//bazel/constraint:stdio_semihosting": ["//src/rp2_common/pico_stdio_semihosting"],
-        "//bazel/constraint:stdio_uart": ["//src/rp2_common/pico_stdio_uart"],
-        "//bazel/constraint:stdio_usb": ["//src/rp2_common/pico_stdio_usb"],
+        "//bazel/constraint:pico_stdio_semihosting_enabled": ["//src/rp2_common/pico_stdio_semihosting"],
+        "//conditions:default": [],
+    }) + select({
+        "//bazel/constraint:pico_stdio_uart_enabled": ["//src/rp2_common/pico_stdio_uart"],
+        "//conditions:default": [],
+    }) + select({
+        "//bazel/constraint:pico_stdio_usb_enabled": ["//src/rp2_common/pico_stdio_usb"],
+        "//conditions:default": [],
     }),
 )
diff --git a/src/rp2_common/pico_unique_id/BUILD.bazel b/src/rp2_common/pico_unique_id/BUILD.bazel
index 7db977e..9c6e9f2 100644
--- a/src/rp2_common/pico_unique_id/BUILD.bazel
+++ b/src/rp2_common/pico_unique_id/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -5,6 +7,7 @@
     srcs = ["unique_id.c"],
     hdrs = ["include/pico/unique_id.h"],
     includes = ["include"],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/rp2_common/hardware_flash",
diff --git a/src/rp2_common/tinyusb/BUILD.bazel b/src/rp2_common/tinyusb/BUILD.bazel
index 0c395a7..3e397ab 100644
--- a/src/rp2_common/tinyusb/BUILD.bazel
+++ b/src/rp2_common/tinyusb/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -7,6 +9,7 @@
         "CFG_TUSB_MCU=OPT_MCU_RP2040",
         "CFG_TUSB_OS=OPT_OS_PICO",
     ],
+    target_compatible_with = compatible_with_rp2(),
     deps = [
         "//src/common/pico_base",
         "//src/common/pico_binary_info",
@@ -22,5 +25,13 @@
         "//src/rp2_common/pico_stdio:pico_stdio_headers",
         "//src/rp2_common/pico_stdio_usb:pico_stdio_usb_headers",
         "//src/rp2_common/pico_stdio_usb:tusb_config",
-    ],
+    ] + select({
+        # If enabled, these headers may need to be visible.
+        "//bazel/constraint:pico_stdio_semihosting_enabled": ["//src/rp2_common/pico_stdio_semihosting"],
+        "//conditions:default": [],
+    }) + select({
+        # If enabled, these headers may need to be visible.
+        "//bazel/constraint:pico_stdio_uart_enabled": ["//src/rp2_common/pico_stdio_uart"],
+        "//conditions:default": [],
+    }),
 )
diff --git a/test/cmsis_test/BUILD.bazel b/test/cmsis_test/BUILD.bazel
new file mode 100644
index 0000000..551aedb
--- /dev/null
+++ b/test/cmsis_test/BUILD.bazel
@@ -0,0 +1,14 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "cmsis_test",
+    testonly = True,
+    srcs = ["cmsis_test.c"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//bazel/config:PICO_CMSIS_PATH",
+        "//src/common/pico_stdlib",
+    ],
+)
diff --git a/test/hardware_irq_test/BUILD.bazel b/test/hardware_irq_test/BUILD.bazel
new file mode 100644
index 0000000..ea4697d
--- /dev/null
+++ b/test/hardware_irq_test/BUILD.bazel
@@ -0,0 +1,18 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "hardware_irq_test",
+    testonly = True,
+    srcs = ["hardware_irq_test.c"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_stdlib",
+        "//src/common/pico_time",
+        "//src/rp2_common/hardware_dma",
+        "//src/rp2_common/hardware_irq",
+        "//src/rp2_common/pico_platform:hardware_structs",
+        "//test/pico_test",
+    ],
+)
diff --git a/test/hardware_pwm_test/BUILD.bazel b/test/hardware_pwm_test/BUILD.bazel
new file mode 100644
index 0000000..700fd1a
--- /dev/null
+++ b/test/hardware_pwm_test/BUILD.bazel
@@ -0,0 +1,18 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "hardware_pwm_test",
+    testonly = True,
+    srcs = ["hardware_pwm_test.c"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_stdlib",
+        "//src/common/pico_time",
+        "//src/rp2_common/hardware_irq",
+        "//src/rp2_common/hardware_pwm",
+        "//src/rp2_common/hardware_resets",
+        "//test/pico_test",
+    ],
+)
diff --git a/test/kitchen_sink/BUILD.bazel b/test/kitchen_sink/BUILD.bazel
new file mode 100644
index 0000000..c9e6040
--- /dev/null
+++ b/test/kitchen_sink/BUILD.bazel
@@ -0,0 +1,137 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+load("//bazel/util:transition.bzl", "kitchen_sink_test_binary")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "btstack_config",
+    hdrs = ["btstack_config.h"],
+    defines = [
+        "ENABLE_CLASSIC=1",
+        "ENABLE_BLE=1",
+    ],
+    includes = ["."],
+)
+
+cc_library(
+    name = "lwip_config",
+    hdrs = ["lwipopts.h"],
+    includes = ["."],
+)
+
+cc_library(
+    name = "mbedtls_config",
+    hdrs = ["mbedtls_config.h"],
+    includes = ["."],
+)
+
+cc_library(
+    name = "kitchen_sink_common",
+    testonly = True,
+    # So that the cpp one can see the c file.
+    hdrs = ["kitchen_sink.c"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_binary_info",
+        "//src/common/pico_bit_ops",
+        "//src/common/pico_divider",
+        "//src/common/pico_stdlib",
+        "//src/common/pico_sync",
+        "//src/common/pico_time",
+        "//src/common/pico_util",
+        "//src/rp2_common/hardware_adc",
+        "//src/rp2_common/hardware_claim",
+        "//src/rp2_common/hardware_clocks",
+        "//src/rp2_common/hardware_divider",
+        "//src/rp2_common/hardware_dma",
+        "//src/rp2_common/hardware_exception",
+        "//src/rp2_common/hardware_flash",
+        "//src/rp2_common/hardware_gpio",
+        "//src/rp2_common/hardware_i2c",
+        "//src/rp2_common/hardware_interp",
+        "//src/rp2_common/hardware_irq",
+        "//src/rp2_common/hardware_pio",
+        "//src/rp2_common/hardware_pll",
+        "//src/rp2_common/hardware_pwm",
+        "//src/rp2_common/hardware_resets",
+        "//src/rp2_common/hardware_rtc",
+        "//src/rp2_common/hardware_spi",
+        "//src/rp2_common/hardware_sync",
+        "//src/rp2_common/hardware_timer",
+        "//src/rp2_common/hardware_uart",
+        "//src/rp2_common/hardware_vreg",
+        "//src/rp2_common/hardware_watchdog",
+        "//src/rp2_common/hardware_xosc",
+        "//src/rp2_common/pico_bootrom",
+        "//src/rp2_common/pico_double",
+        "//src/rp2_common/pico_fix/rp2040_usb_device_enumeration",
+        "//src/rp2_common/pico_flash",
+        "//src/rp2_common/pico_float",
+        "//src/rp2_common/pico_i2c_slave",
+        "//src/rp2_common/pico_int64_ops",
+        "//src/rp2_common/pico_malloc",
+        "//src/rp2_common/pico_mem_ops",
+        "//src/rp2_common/pico_multicore",
+        "//src/rp2_common/pico_platform",
+        "//src/rp2_common/pico_printf",
+        "//src/rp2_common/pico_rand",
+        "//src/rp2_common/pico_runtime",
+        "//src/rp2_common/pico_stdio",
+        "//src/rp2_common/pico_unique_id",
+        "//test/pico_test",
+    ],
+)
+
+cc_binary(
+    name = "kitchen_sink",
+    testonly = True,
+    srcs = ["kitchen_sink.c"],
+    deps = [":kitchen_sink_common"],
+)
+
+cc_binary(
+    name = "kitchen_sink_cpp",
+    testonly = True,
+    srcs = ["kitchen_sink_cpp.cpp"],
+    deps = [":kitchen_sink_common"],
+)
+
+cc_binary(
+    name = "kitchen_sink_lwip_poll_actual",
+    testonly = True,
+    srcs = ["kitchen_sink.c"],
+    tags = ["manual"],  # Built via kitchen_sink_lwip_poll
+    deps = [
+        ":kitchen_sink_common",
+        "//src/rp2_common/pico_cyw43_arch:pico_cyw43_arch_lwip_poll",
+    ],
+)
+
+cc_binary(
+    name = "kitchen_sink_lwip_background_actual",
+    testonly = True,
+    srcs = ["kitchen_sink.c"],
+    tags = ["manual"],  # Built via kitchen_sink_lwip_background
+    deps = [
+        ":kitchen_sink_common",
+        "//src/rp2_common/pico_cyw43_arch:pico_cyw43_arch_lwip_threadsafe_background",
+    ],
+)
+
+kitchen_sink_test_binary(
+    name = "kitchen_sink_lwip_poll",
+    testonly = True,
+    src = ":kitchen_sink_lwip_poll_actual",
+    bt_stack_config = ":btstack_config",
+    lwip_config = ":lwip_config",
+    target_compatible_with = compatible_with_rp2(),
+)
+
+kitchen_sink_test_binary(
+    name = "kitchen_sink_lwip_background",
+    testonly = True,
+    src = ":kitchen_sink_lwip_background_actual",
+    bt_stack_config = ":btstack_config",
+    lwip_config = ":lwip_config",
+    target_compatible_with = compatible_with_rp2(),
+)
diff --git a/test/pico_divider_test/BUILD.bazel b/test/pico_divider_test/BUILD.bazel
new file mode 100644
index 0000000..b349dda
--- /dev/null
+++ b/test/pico_divider_test/BUILD.bazel
@@ -0,0 +1,39 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "pico_divider_test",
+    testonly = True,
+    srcs = ["pico_divider_test.c"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_divider",
+        "//src/common/pico_stdlib",
+    ] + select({
+        "//bazel/constraint:host": [],
+        "//conditions:default": [
+            "//src/rp2_common/hardware_dma",
+            "//src/rp2_common/hardware_irq",
+            "//src/rp2_common/hardware_vreg",
+        ],
+    }),
+)
+
+cc_binary(
+    name = "pico_divider_nesting_test",
+    testonly = True,
+    srcs = ["pico_divider_nesting_test.c"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_divider",
+        "//src/common/pico_stdlib",
+    ] + select({
+        "//bazel/constraint:host": [],
+        "//conditions:default": [
+            "//src/rp2_common/hardware_dma",
+            "//src/rp2_common/hardware_irq",
+            "//src/rp2_common/hardware_vreg",
+        ],
+    }),
+)
diff --git a/test/pico_float_test/BUILD.bazel b/test/pico_float_test/BUILD.bazel
new file mode 100644
index 0000000..16e7477
--- /dev/null
+++ b/test/pico_float_test/BUILD.bazel
@@ -0,0 +1,37 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "pico_float_test",
+    testonly = True,
+    srcs = [
+        "llvm/call_apsr.S",
+        "llvm/call_apsr.h",
+        "pico_float_test.c",
+    ],
+    includes = ["llvm"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_stdlib",
+        "//src/rp2_common/pico_float",
+        "//src/rp2_common/pico_platform",
+    ],
+)
+
+cc_binary(
+    name = "pico_double_test",
+    testonly = True,
+    srcs = [
+        "llvm/call_apsr.S",
+        "llvm/call_apsr.h",
+        "pico_double_test.c",
+    ],
+    includes = ["llvm"],
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_stdlib",
+        "//src/rp2_common/pico_double",
+        "//src/rp2_common/pico_platform",
+    ],
+)
diff --git a/test/pico_sem_test/BUILD.bazel b/test/pico_sem_test/BUILD.bazel
new file mode 100644
index 0000000..c6b51c8
--- /dev/null
+++ b/test/pico_sem_test/BUILD.bazel
@@ -0,0 +1,12 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "pico_sem_test",
+    testonly = True,
+    srcs = ["pico_sem_test.c"],
+    deps = [
+        "//src/common/pico_stdlib",
+        "//src/common/pico_sync",
+        "//test/pico_test",
+    ],
+)
diff --git a/test/pico_stdio_test/BUILD.bazel b/test/pico_stdio_test/BUILD.bazel
new file mode 100644
index 0000000..eeffa06
--- /dev/null
+++ b/test/pico_stdio_test/BUILD.bazel
@@ -0,0 +1,16 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "pico_stdio_test",
+    testonly = True,
+    srcs = ["pico_stdio_test.c"],
+    # Host doesn't support PICO_TIME_NO_ALARM_SUPPORT without pico_host_sdl.
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_stdlib",
+        "//src/rp2_common/pico_multicore",
+        "//test/pico_test",
+    ],
+)
diff --git a/test/pico_stdlib_test/BUILD.bazel b/test/pico_stdlib_test/BUILD.bazel
new file mode 100644
index 0000000..edce64f
--- /dev/null
+++ b/test/pico_stdlib_test/BUILD.bazel
@@ -0,0 +1,11 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "pico_stdlib_test",
+    testonly = True,
+    srcs = ["pico_stdlib_test.c"],
+    deps = [
+        "//src/common/pico_bit_ops",
+        "//src/common/pico_stdlib",
+    ],
+)
diff --git a/test/pico_test/BUILD.bazel b/test/pico_test/BUILD.bazel
new file mode 100644
index 0000000..cbcd874
--- /dev/null
+++ b/test/pico_test/BUILD.bazel
@@ -0,0 +1,9 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "pico_test",
+    testonly = True,
+    hdrs = ["include/pico/test.h"],
+    includes = ["include"],
+    deps = ["//src/common/pico_base"],
+)
diff --git a/test/pico_time_test/BUILD.bazel b/test/pico_time_test/BUILD.bazel
new file mode 100644
index 0000000..582e426
--- /dev/null
+++ b/test/pico_time_test/BUILD.bazel
@@ -0,0 +1,26 @@
+load("//bazel:defs.bzl", "compatible_with_rp2")
+load("//bazel/util:transition.bzl", "extra_copts_for_all_deps")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "pico_time_test_actual",
+    testonly = True,
+    srcs = ["pico_time_test.c"],
+    tags = ["manual"],  # Built via pico_time_test.
+    # Doesn't appear to work on host builds yet.
+    target_compatible_with = compatible_with_rp2(),
+    deps = [
+        "//src/common/pico_stdlib",
+        "//test/pico_test",
+    ],
+)
+
+extra_copts_for_all_deps(
+    name = "pico_time_test",
+    testonly = True,
+    src = ":pico_time_test_actual",
+    extra_copts = ["-DPICO_TIME_DEFAULT_ALARM_POOL_MAX_TIMERS=250"],
+    # Host doesn't support PICO_TIME_NO_ALARM_SUPPORT without pico_host_sdl.
+    target_compatible_with = compatible_with_rp2(),
+)
diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel
new file mode 100644
index 0000000..ffd0fb0
--- /dev/null
+++ b/tools/BUILD.bazel
@@ -0,0 +1 @@
+package(default_visibility = ["//visibility:public"])
diff --git a/tools/compare_build_systems.py b/tools/compare_build_systems.py
new file mode 100644
index 0000000..d691e50
--- /dev/null
+++ b/tools/compare_build_systems.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2024 Raspberry Pi (Trading) Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#
+# A script to ensure that all declared configuration options match across both
+# CMake and Bazel.
+#
+# Usage:
+#
+# Run from anywhere.
+
+from dataclasses import dataclass
+import glob
+import os
+import re
+import subprocess
+import sys
+
+CMAKE_FILE_TYPES = (
+    "**/CMakeLists.txt",
+    "**/*.cmake",
+)
+
+BAZEL_FILE_TYPES = (
+    "**/BUILD.bazel",
+    "**/*.bzl",
+    "**/*.BUILD",
+)
+
+SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
+
+SDK_ROOT = subprocess.run(
+    (
+        "git",
+        "rev-parse",
+        "--show-toplevel",
+    ),
+    cwd=SCRIPT_DIR,
+    text=True,
+    check=True,
+    capture_output=True,
+).stdout.strip()
+
+ATTR_REGEX = re.compile(r',?\s*(?P<key>[^=]+)=(?P<value>[^,]+)')
+
+# Sometimes the build systems are supposed to be implemented differently. This
+# allowlist permits the descriptions to differ between CMake and Bazel.
+BUILD_SYSTEM_DESCRIPTION_DIFFERENCE_ALLOWLIST = (
+    # Minor semantic differences in Bazel.
+    "PICO_DEFAULT_BOOT_STAGE2_FILE",
+    # In Bazel, not overridable by user environment variables (only flags).
+    "PICO_BOARD",
+    # In Bazel, it's a build label rather than a path.
+    "PICO_CMSIS_PATH",
+    # In Bazel, the semantics of embedded binary info are slightly different.
+    "PICO_PROGRAM_NAME",
+    "PICO_PROGRAM_DESCRIPTION",
+    "PICO_PROGRAM_URL",
+    "PICO_PROGRAM_VERSION_STRING",
+    "PICO_TARGET_NAME",
+)
+
+CMAKE_ONLY_ALLOWLIST = (
+    # Not relevant to Bazel: toolchain is fetched dynamically, and can be
+    # overridden with native Bazel features.
+    "PICO_TOOLCHAIN_PATH",
+    # Bazel uses native --platforms mechanics.
+    "PICO_PLATFORM",
+    # TODO: No built-in, pre-configured clang offering for Bazel yet.
+    "PICO_COMPILER",
+    # Entirely irrelevant to Bazel, use Bazel platforms:
+    #     https://bazel.build/extending/platforms
+    "PICO_CMAKE_PRELOAD_PLATFORM_FILE",
+    # Both of these are marked as TODO and not actually set up in CMake.
+    "PICO_CMSIS_VENDOR",
+    "PICO_CMSIS_DEVICE",
+    # Bazel build uses PICO_CONFIG_EXTRA_HEADER and PICO_CONFIG_PLATFORM_HEADER
+    # instead.
+    "PICO_CONFIG_HEADER_FILES",
+    "PICO_RP2040_CONFIG_HEADER_FILES",
+    "PICO_HOST_CONFIG_HEADER_FILES",
+    # Bazel uses PICO_CONFIG_HEADER.
+    "PICO_BOARD_CMAKE_DIRS",
+    "PICO_BOARD_HEADER_FILE",
+    "PICO_BOARD_HEADER_DIRS",
+    # Bazel supports this differently.
+    # TODO: Provide a helper rule for explicitly generating a UF2 so users don't
+    # have to write out a bespoke run_binary.
+    "PICO_NO_UF2",
+    # Bazel will not provide a default for this.
+    # TODO: Provide handy rules for PIOASM so users don't have to write out a
+    # bespoke run_binary.
+    "PICO_DEFAULT_PIOASM_OUTPUT_FORMAT",
+)
+
+BAZEL_ONLY_ALLOWLIST = (
+    # Allows users to fully replace the final image for boot_stage2.
+    "PICO_BOOT_STAGE2_LINK_IMAGE",
+    # Allows users to inject an alternative TinyUSB library since TinyUSB
+    # doesn't have native Bazel support.
+    "PICO_TINYUSB_LIB",
+    # Bazel can't do pico_set_* for the binary info defines, so there's a
+    # different mechanism.
+    "PICO_DEFAULT_BINARY_INFO",
+    # Bazel analogue for PICO_CMAKE_BUILD_TYPE.
+    "PICO_BAZEL_BUILD_TYPE",
+    # Different mechanism for setting a linker script that is less complex.
+    "PICO_DEFAULT_LINKER_SCRIPT",
+    # Not yet documented in CMake (but probably should be):
+    "PICO_CMAKE_BUILD_TYPE",
+    # Replaces PICO_RP2040_CONFIG_HEADER_FILES and
+    # PICO_HOST_CONFIG_HEADER_FILES.
+    "PICO_CONFIG_EXTRA_HEADER",
+    "PICO_CONFIG_PLATFORM_HEADER",
+    # Effectively replaces:
+    # - PICO_BOARD_CMAKE_DIRS
+    # - PICO_BOARD_HEADER_FILE
+    # - PICO_BOARD_HEADER_DIRS
+    "PICO_CONFIG_HEADER",
+    # Bazel configuration for 3p deps.
+    "PICO_BTSTACK_CONFIG",
+    "PICO_LWIP_CONFIG",
+    "PICO_FREERTOS_LIB",
+)
+
+@dataclass
+class Option:
+    name: str
+    description: str
+    attrs: dict[str, str]
+
+    def matches(self, other):
+        matches = (self.name == other.name) and (self.attrs == other.attrs)
+        if not self.name in BUILD_SYSTEM_DESCRIPTION_DIFFERENCE_ALLOWLIST:
+            matches = matches and (self.description == other.description)
+        return matches
+
+
+def FindKnownOptions(option_pattern_matcher, file_paths):
+    pattern = re.compile(
+        option_pattern_matcher +
+        r':\s+(?P<name>\w+),\s+(?P<description>[^,]+)(?:,\s+(?P<attrs>.*))?$')
+    options = {}
+    for p in file_paths:
+        with open(p, 'r') as f:
+            for line in f:
+                match = re.search(pattern, line)
+                if not match:
+                    continue
+
+                attrs = {
+                    m.group('key'): m.group('value')
+                    for m in re.finditer(ATTR_REGEX, match.group('attrs'))
+                }
+
+                options[match.group('name')] = Option(
+                    match.group('name'),
+                    match.group('description'),
+                    attrs,
+                )
+    return options
+
+
+def OptionsAreEqual(bazel_option, cmake_option):
+    if bazel_option is None:
+        if cmake_option.name in CMAKE_ONLY_ALLOWLIST:
+            return True
+        print(f"    {cmake_option.name} does not exist in Bazel")
+        return False
+    elif cmake_option is None:
+        if bazel_option.name in BAZEL_ONLY_ALLOWLIST:
+            return True
+        print(f"    {bazel_option.name} does not exist in CMake")
+        return False
+    elif not bazel_option.matches(cmake_option):
+        print("    Bazel and CMAKE definitions do not match:")
+        print(f"    [CMAKE]    {bazel_option}")
+        print(f"    [BAZEL]    {cmake_option}")
+        return False
+
+    return True
+
+
+def CompareOptions(bazel_pattern, bazel_files, cmake_pattern, cmake_files):
+    bazel_options = FindKnownOptions(bazel_pattern, bazel_files)
+    cmake_options = FindKnownOptions(cmake_pattern, cmake_files)
+
+    are_equal = True
+    both = {}
+    both.update(bazel_options)
+    both.update(cmake_options)
+    for k in both.keys():
+        if not OptionsAreEqual(bazel_options.get(k, None),
+                               cmake_options.get(k, None)):
+            are_equal = False
+    return are_equal
+
+
+cmake_files = [
+    f for p in CMAKE_FILE_TYPES
+    for f in glob.glob(p, root_dir=SDK_ROOT, recursive=True)
+]
+bazel_files = [
+    f for p in BAZEL_FILE_TYPES
+    for f in glob.glob(p, root_dir=SDK_ROOT, recursive=True)
+]
+
+print('[1/2] Checking build system configuration flags...')
+build_options_ok = CompareOptions("PICO_BAZEL_CONFIG", bazel_files,
+                                  "PICO_CMAKE_CONFIG", cmake_files)
+
+print('[2/2] Checking build system defines...')
+build_defines_ok = CompareOptions("PICO_BUILD_DEFINE", bazel_files,
+                                  "PICO_BUILD_DEFINE", cmake_files)
+
+if build_options_ok and build_defines_ok:
+    print("OK")
+    sys.exit(0)
+
+sys.exit(1)
diff --git a/tools/elf2uf2/BUILD.bazel b/tools/elf2uf2/BUILD.bazel
new file mode 100644
index 0000000..bb2e8ed
--- /dev/null
+++ b/tools/elf2uf2/BUILD.bazel
@@ -0,0 +1,19 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_binary(
+    name = "elf2uf2",
+    srcs = [
+        "elf.h",
+        "main.cpp",
+    ],
+    copts = select({
+        "@platforms//os:windows": [],
+        "//conditions:default": [
+            "-Wno-unused-function",
+            "-Wno-reorder-ctor",
+            "-Wno-unused-variable",
+        ],
+    }),
+    target_compatible_with = ["//bazel/constraint:host"],
+    deps = ["//src/common/boot_uf2"],
+)
diff --git a/tools/pioasm/BUILD.bazel b/tools/pioasm/BUILD.bazel
new file mode 100644
index 0000000..c9fd601
--- /dev/null
+++ b/tools/pioasm/BUILD.bazel
@@ -0,0 +1,66 @@
+package(default_visibility = ["//visibility:public"])
+
+# TODO: No support for building the parser.
+
+cc_library(
+    name = "pioasm_core",
+    srcs = [
+        "gen/lexer.cpp",
+        "gen/location.h",
+        "gen/parser.cpp",
+        "gen/parser.hpp",
+        "go_output.cpp",
+        "json_output.cpp",
+        "main.cpp",
+        "output_format.h",
+        "pio_assembler.cpp",
+        "pio_assembler.h",
+        "pio_disassembler.cpp",
+        "pio_disassembler.h",
+        "pio_types.h",
+    ],
+    includes = [
+        ".",
+        "gen",
+    ],
+    target_compatible_with = ["//bazel/constraint:host"],
+)
+
+cc_library(
+    name = "c_sdk_output",
+    srcs = ["c_sdk_output.cpp"],
+    deps = [":pioasm_core"],
+    alwayslink = True,
+)
+
+cc_library(
+    name = "python_output",
+    srcs = ["python_output.cpp"],
+    deps = [":pioasm_core"],
+    alwayslink = True,
+)
+
+cc_library(
+    name = "hex_output",
+    srcs = ["hex_output.cpp"],
+    deps = [":pioasm_core"],
+    alwayslink = True,
+)
+
+cc_library(
+    name = "ada_output",
+    srcs = ["ada_output.cpp"],
+    deps = [":pioasm_core"],
+    alwayslink = True,
+)
+
+cc_binary(
+    name = "pioasm",
+    deps = [
+        ":ada_output",
+        ":c_sdk_output",
+        ":hex_output",
+        ":pioasm_core",
+        ":python_output",
+    ],
+)
diff --git a/tools/uf2_aspect.bzl b/tools/uf2_aspect.bzl
new file mode 100644
index 0000000..961fcbc
--- /dev/null
+++ b/tools/uf2_aspect.bzl
@@ -0,0 +1,63 @@
+# TODO: Default to a list of known compatible rules until the toolchain emits
+# firmware images with a .elf extension. When binaries have a .elf suffix,
+# this can change to ["*"] and another attribute that allows extension-based
+# filtering can be added to more easily support a wider array of file types.
+_SUPPORTED_BINARY_TYPES = ",".join([
+    "cc_binary",
+    "cc_test",
+])
+
+def _pico_uf2_aspect_impl(target, ctx):
+    allowed_types = ctx.attr.from_rules.split(",")
+    if ctx.rule.kind not in allowed_types and "*" not in allowed_types:
+        return []
+
+    binary_to_convert = target[DefaultInfo].files_to_run.executable
+    uf2_output = ctx.actions.declare_file(binary_to_convert.basename + ".uf2")
+    ctx.actions.run(
+        outputs = [uf2_output],
+        inputs = [binary_to_convert],
+        tools = [ctx.executable._elf2uf2_tool],
+        executable = ctx.executable._elf2uf2_tool,
+        arguments = [
+            binary_to_convert.path,
+            uf2_output.path,
+        ],
+    )
+    return [
+        OutputGroupInfo(
+            pico_uf2_files = depset([uf2_output]),
+        ),
+    ]
+    return []
+
+pico_uf2_aspect = aspect(
+    implementation = _pico_uf2_aspect_impl,
+    doc = """An aspect for generating UF2 images from ELF binaries.
+
+Normally with Bazel, a cc_binary or other rule cannot be "extended" to emit
+additional outputs. However, this aspect may be used as a secondary, adjacent
+step that generates UF2 images from all ELF artifacts.
+
+This can be used from a build to produce UF2 files alongside the regular
+outputs:
+
+```
+bazel build --platforms=@pico-sdk//bazel/platform:rp2040 \\
+    --aspects @pico-sdk//tools:uf2_aspect.bzl%pico_uf2_aspect \\
+    --output_groups=+pico_uf2_files \\
+    //...
+```
+
+It's also possible to use this aspect within a custom macro (e.g. my_cc_binary)
+to produce UF2 images alongside ELF files. However, with that method UF2 images
+will only be produced when you explicitly use your custom macro.
+""",
+    attrs = {
+        "from_rules": attr.string(
+            default = _SUPPORTED_BINARY_TYPES,
+            doc = "A comma-separated list of rule kinds to apply the UF2 aspect to",
+        ),
+        "_elf2uf2_tool": attr.label(default = "//tools/elf2uf2:elf2uf2", executable = True, cfg = "exec"),
+    },
+)