This article explores the challenges and strategies for adding support for CMake’s find_package() mechanism to packages that depend on the Raspberry Pi Pico C/C++ SDK. While the Pico SDK is designed for direct inclusion and does not export its library targets thereby barring directly adding find_package support, I want to support modular, reusable components that abstract away platform-specific setup. The article analyzes common problems—including missing export targets and toolchain configuration constraints—and considers three implementation approaches for downstream dependency handling. The selected solution uses generator expressions to defer linking dependencies until the build stage, enabling find_package() compatibility without duplicating toolchain logic or bloating downstream CMakeLists files. This is demonstrated through a real-world GARP Motor Controller HAL, with a flexible multi-toolchain build process via ExternalProject_Add.
Introduction
If you use CMake to define a target (say, a HAL) that links library targets from the Raspberry Pi Pico SDK and then attempt to export that HAL target to support CMake’s find_package, CMake issues an error during the build stage:
[build] CMake Error: install(EXPORT "garp_mc_hal-rp2350Targets" ...) includes target "garp_mc_hal-rp2350" which requires target "pico_stdlib" that is not in any export set.
The issue is that the Pico SDK does not export it’s library targets as it expects users to have the SDK installed locally and available for linking, which isn’t entirely unreasonable given that compiling for the platform likely means you are actively using the ~$5 hardware. These Pico targets do conveniently register the include directories necessary for target use, and so would be nice to reuse if possible. Unfortunately, the SDK also does not support find_package due to the way it configures the toolchain. As an alternative to prescribing the --toolchain argument at the command line, the SDK provides a .cmake file that can be included to configure the toolchain, but then requires some CMake instructions introduced ahead of the project() command. This .cmake file has remained relatively stable since 2021, but should be expected to change in the future. Given these factors, the typical workflow for building a project targeting the Pico hardware begins with installing the SDK onto the host system, specifying the PICO_SDK_PATH environment variable. The project’s CMakeLists.txt file then includes the pico_sdk_import.cmake file, followed by the project() command, and then the pico_sdk_init() command. Downstream packages then would repeat this content in their CMakeLists.txt files configuring the toolchain for each artifact. When using a HAL intended to abstract away the dependence on the SDK, this need to include the file and repeat these contents are less than ideal, and more importantly, the need to track which Pico library targets are used and explicitly add them to link lists is error-prone. As I’ve been developing a HAL and a series of motor controller applications, this latter has been particularly cumbersome.
Use Case
Ideally, using the HAL would entail an opaque interface using find_package and no other modifications to access the dependencies. More specifically, if writing an application A that leverages library L, in turn leveraging the HAL, the workflow would look something like this:
- Application A’s
CMakeLists.txtwould usefind_packageto import the L library project - This would automatically introduce the L library’s linkages and other properties like header files
- This, in turn, would automatically introduce the HAL’s dependencies, including the Pico SDK
The key elements here are the “automatic” steps that release me from trying to remember all of the appropriate dependencies two+ layers of dependencies back, and would make modularity significantly easier. The ability to build for multiple platforms (i.e. support multiple toolchains) from a single CMake entrypoint would also be a boon to simplify managing several independent but otherwise parallel directory structures.
Implementation
To realize this pipe dream, a few options came to mind:
- Use FetchContent or ExternalProject to automate dependency download and build for each application (effectively abandoning hope of
find_packagesupport). - Link all Pico libraries used by the HAL into the HAL static library directly
- Register the link dependencies in the HAL static library, but prevent CMake from looking for those dependencies outside of the build stage
In parallel, to add multiple toolchain support, ExternalProject appears to be the best option. Using ExternalProject_Add from a root “superbuild”, an entirely independent CMake project with its own toolchain, targets, &c. can be configured and kicked off. This introduces a couple wrinkles however: One, the subproject does not share target definitions with the superbuild, so any installs, packaging, &c. must occur in the subbuild, and two, the entire CMake lifecycle of configure, build, &c. stages is executed during the superbuild’s build stage, so subbuild artifacts are not available to the build stage of the superbuild (unless dependencies are registered). ExternalProject_Add does include a number of the features of FetchContent like downloading git repositories and specifying git tags, or can point to a local filesystem location. This then enables building a mock library artifact for the host system, while also building artifacts for other platforms, all from a single repository.
First considering the FetchContent or ExternalProject dependency download for each application, this was an effective design, albeit slow and tedious to maintain. Each clean build encompassed downloading the dependencies, compiling them, compiling the application and then linking. By the third layer (i.e. Application > Library L > HAL) build times were multiple minutes for a simple application. Perhaps most painfully, when multiple parallel libraries required the same dependency, it was common to download, build, and link multiple copies of the dependency due to concurrency in CMake.
The second option to link all Pico SDK dependencies into a singular HAL static library was (and is) somewhat appealing. There’s a risk that the library file would be large-ish, but since I’m not shipping the library file around, that’s not a terribly concerning factor. My primary concern with this approach is the process of linking the Pico’s targets into the HAL. CMake won’t pull in symbol definitions when building a static library automatically. This means the following:
add_library(hal_lib STATIC)
target_link_libraries(hal_lib PRIVATE pico_stdlib)
wouldn’t link in the symbol definitions from pico_stdlib until hal_lib was linked into the application code. To resolve this, I’ll need to produce object libraries for all of the used Pico SDK library targets, which isn’t in and of itself too difficult, but makes the CMake configuration of the rapidly evolving HAL cumbersome. An alternative to the object library targets is to explicitly specific Pico SDK .a files and explicitly identifying include directories, which adds similar overhead when adding features to the HAL (albeit only once).
The last option, and the one currently in use is to identify, but not link, Pico SDK dependencies, and carry them downstream. In this case, the HAL’s CMakeLists.txt (and only the HAL’s) explicitly lists the Pico SDK link targets, but wraps them in a $<BUILD_INTERFACE:...> generator expression so that the HAL’s static library is tracking the dependency, but does not seek out the linked target’s resources during the configure, install, &c. stages. Downstream projects then include the HAL target via find_package and collect its include directories via linking against the target, but again do not seek out the HAL’s dependencies until building the application code. This can then be chained, so the Application can depend on Library A which depends of Library B which depends on the HAL without the application or any Library needing to specify Pico SDK link targets not needed by them directly. This DOES however, require the Pico SDK be installed when building the downstream application, as this is when the build system will seek out those dependencies, but this is reasonable given the previous argument.
There are likely many other approaches, but in the time-honored tradition of finding my solution in the last place I’ve looked…
Example
While I may use the second option in the future once HAL development settles, the third is currently being used because it makes changes simple and I have the Pico SDK readily available/installed on my host machine. Walking through the implementation of the GARP Motor Controller HAL as an example, the build is organized as a top-level superbuild with a CMakeLists.txt in the root of the repository, and subbuild CMakeLists.txt files in the src/mock and src/rp2350 directories with the respective interface implementations. The superbuild uses ExternalProject_Add commands to kick of each of the mock and RP2350 platform-dependent library builds:
ExternalProject_Add(
ep-${PROJECT_NAME}-mock
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/mock
...
)
...
ExternalProject_Add(
ep-${PROJECT_NAME}-rp2350
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/rp2350
...
)
where the -mock target builds with the host toolchain, and the -rp2350 target builds with the Pico SDK-supplied toolchain. To make the RP2350 variant exportable/support find_package, the CMakeLists.txt in the src/rp2350 directory includes an amalgam of the typical Pico SDK application, approach three above, and the typical project export:
# Typical Pico SDK usage
set(PICO_BOARD pico2)
set(PICO_PLATFORM rp2350-arm-s)
include(pico_sdk_import.cmake)
...
project(garp_mc_hal-rp2350)
...
pico_sdk_init()
...
add_library(${PROJECT_NAME} STATIC
hal_core_rp2350.c
...
)
...
# Build stage link target registration
target_link_libraries(${PROJECT_NAME}
PRIVATE
$<BUILD_INTERFACE:pico_stdlib>
$<BUILD_INTERFACE:pico_stdio>
$<BUILD_INTERFACE:pico_multicore>
$<BUILD_INTERFACE:pico_sync>
$<BUILD_INTERFACE:hardware_irq>
$<BUILD_INTERFACE:hardware_pwm>
$<BUILD_INTERFACE:hardware_spi>
)
...
# Typical find_package() support
install(
TARGETS
${PROJECT_NAME}
EXPORT
${PROJECT_NAME}Targets
ARCHIVE
DESTINATION ${INSTALL_LIBDIR}
COMPONENT lib
PUBLIC_HEADER
DESTINATION ${INSTALL_INCLUDEDIR}
COMPONENT dev
)
...
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
configure_package_config_file(
${SUPERBUILD_ROOT}/cmake/${PROJECT_NAME}Config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
INSTALL_DESTINATION ${INSTALL_CMAKEDIR}
)
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
DESTINATION
${INSTALL_CMAKEDIR}
)
install(
EXPORT
${PROJECT_NAME}Targets
DESTINATION
${INSTALL_CMAKEDIR}
COMPONENT
dev
)
Not shown is the pico_enable_stdio_usb(...) command added since the HAL uses USB CDC for logging support, and GNUInstallDirs to define common install locations. A pair of standard ${PROJECT_NAME}Config.cmake.in files are also created the cmake directory to support CMake’s CMakePackageConfigHelpers.
Specific GARP CMake Practices
Within the GARP portfolio, I use ${HOME}/.local/ as a default install prefix (i.e. CMAKE_INSTALL_PREFIX) so I don’t need elevated privileges to run installs. ExternalProject will run the install step by default, and this is in fact required for downstream projects to find the artifacts built by ExternalProject, but I want to avoid polluting my host system with artifacts every time I build a given project. In particular when a project is installed directly with make install I’d like it to install to my host system, but when built as an automated dependency I’d like it installed to the build tree. To realize this, the GARP portfolio sets a default value for CMAKE_INSTALL_PREFIX:
set(home_dir_ "$ENV{HOME}/.local")
set(CMAKE_INSTALL_PREFIX
${home_dir_}
CACHE PATH "Installation root directory")
set(CMAKE_PREFIX_PATH
"${CMAKE_INSTALL_PREFIX}"
CACHE PATH "Prefix path for installed packages")
which can be overridden with the -DCMAKE_INSTALL_PREFIX argument. This argument is then supplied when building dependencies to direct the installation into the build tree. Considering the GARP Motor Controller Interfaces package, the HAL library dependency is built with:
ExternalProject_Add(
ep-garp_mc_hal-mock
...
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/external
...
)
while the end artifact is built with:
ExternalProject_Add(
ep-${PROJECT_NAME}-mock
...
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}
...
)
Summary
To support modular and reusable CMake packages that rely on the Raspberry Pi Pico SDK, this article details a strategy for enabling find_package() compatibility despite the SDK’s lack of exported targets or native package support. It evaluates three approaches for managing downstream dependencies and selects one that leverages generator expressions to defer linking until the build stage. This avoids duplicating toolchain logic and simplifies integration for applications and libraries further downstream. The solution is implemented in the GARP Motor Controller HAL using a multi-toolchain superbuild structure with ExternalProject_Add, enabling clean, scalable integration across platforms.