{"id":284,"date":"2025-07-24T00:57:55","date_gmt":"2025-07-24T00:57:55","guid":{"rendered":"https:\/\/michael-stinger.com\/?p=284"},"modified":"2025-07-24T14:20:55","modified_gmt":"2025-07-24T14:20:55","slug":"a-cmake-strategy-for-the-pico-c-c-sdk","status":"publish","type":"post","link":"https:\/\/michael-stinger.com\/index.php\/2025\/07\/24\/a-cmake-strategy-for-the-pico-c-c-sdk\/","title":{"rendered":"A CMake Strategy for the Pico C\/C++ SDK"},"content":{"rendered":"\n<p>This article explores the challenges and strategies for adding support for CMake&#8217;s <code>find_package()<\/code> 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 <code>find_package<\/code> support, I want to support modular, reusable components that abstract away platform-specific setup. The article analyzes common problems\u2014including missing export targets and toolchain configuration constraints\u2014and considers three implementation approaches for downstream dependency handling. The selected solution uses generator expressions to defer linking dependencies until the build stage, enabling <code>find_package()<\/code> 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 <code>ExternalProject_Add<\/code>.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Introduction<\/h2>\n\n\n\n<p>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&#8217;s <code>find_package<\/code>, CMake issues an error during the build stage:<\/p>\n\n\n\n<p class=\"has-white-color has-black-background-color has-text-color has-background has-link-color wp-elements-9de3351fca610d940c2027018afa67ef\"><code>[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.<\/code><\/p>\n\n\n\n<p>The issue is that the Pico SDK does not export it&#8217;s library targets as it expects users to have the SDK installed locally and available for linking, which isn&#8217;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, <a href=\"https:\/\/github.com\/raspberrypi\/pico-sdk\/issues\/1476\" title=\"\">the SDK also does not support <code>find_package<\/code> due to the way it configures the toolchain<\/a>. As an alternative to prescribing the <code>--toolchain<\/code> argument at the command line, the SDK provides <a href=\"https:\/\/github.com\/raspberrypi\/pico-sdk#unix-command-line\" title=\"\">a .cmake file<\/a> that can be included to configure the toolchain, but then requires some CMake instructions introduced ahead of the <code>project()<\/code> command. This <code>.cmake<\/code> file has <a href=\"https:\/\/github.com\/raspberrypi\/pico-sdk\/commits\/master\/external\/pico_sdk_import.cmake\" title=\"\">remained relatively stable since 2021<\/a>, 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 <code>PICO_SDK_PATH<\/code> environment variable. The project&#8217;s <code>CMakeLists.txt<\/code> file then includes the <code>pico_sdk_import.cmake<\/code> file, followed by the <code>project()<\/code> command, and then the <code>pico_sdk_init()<\/code> command. Downstream packages then would repeat this content in their <code>CMakeLists.txt<\/code> 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&#8217;ve been developing a HAL and a series of motor controller applications, this latter has been particularly cumbersome.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Use Case<\/h2>\n\n\n\n<p>Ideally, using the HAL would entail an opaque interface using <code>find_package<\/code> 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:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Application A&#8217;s <code>CMakeLists.txt<\/code> would use <code>find_package<\/code> to import the L library project<\/li>\n\n\n\n<li>This would automatically introduce the L library&#8217;s linkages and other properties like header files<\/li>\n\n\n\n<li>This, in turn, would automatically introduce the HAL&#8217;s dependencies, including the Pico SDK<\/li>\n<\/ol>\n\n\n\n<p>The key elements here are the &#8220;automatic&#8221; 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Implementation<\/h2>\n\n\n\n<p>To realize this pipe dream, a few options came to mind:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use FetchContent or ExternalProject to automate dependency download and build for each application (effectively abandoning hope of <code>find_package<\/code> support).<\/li>\n\n\n\n<li>Link all Pico libraries used by the HAL into the HAL static library directly<\/li>\n\n\n\n<li>Register the link dependencies in the HAL static library, but prevent CMake from looking for those dependencies outside of the build stage<\/li>\n<\/ul>\n\n\n\n<p>In parallel, to add multiple toolchain support, ExternalProject appears to be the best option. Using <a href=\"https:\/\/cmake.org\/cmake\/help\/latest\/module\/ExternalProject.html\" title=\"\"><code>ExternalProject_Add<\/code><\/a> from a root &#8220;superbuild&#8221;, an entirely independent CMake project with its own toolchain, targets, &amp;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, &amp;c. must occur in the subbuild, and two, the entire CMake lifecycle of configure, build, &amp;c. stages is executed during the superbuild&#8217;s <em>build<\/em> 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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>The second option to link all Pico SDK dependencies into a singular HAL static library was (and is) somewhat appealing. There&#8217;s a risk that the library file would be large-ish, but since I&#8217;m not shipping the library file around, that&#8217;s not a terribly concerning factor. My primary concern with this approach is the process of linking the Pico&#8217;s targets into the HAL. CMake won&#8217;t pull in symbol definitions when building a static library automatically. This means the following:<\/p>\n\n\n\n<pre class=\"wp-block-code has-background\" style=\"background-color:#dddddd\"><code>add_library(hal_lib STATIC)\ntarget_link_libraries(hal_lib PRIVATE pico_stdlib)<\/code><\/pre>\n\n\n\n<p>wouldn&#8217;t link in the symbol definitions from <code>pico_stdlib<\/code> until <code>hal_lib<\/code> was linked into the application code. To resolve this, I&#8217;ll need to produce object libraries for all of the used Pico SDK library targets, which isn&#8217;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).<\/p>\n\n\n\n<p>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&#8217;s CMakeLists.txt (and only the HAL&#8217;s) explicitly lists the Pico SDK link targets, but wraps them in a <code>$&lt;BUILD_INTERFACE:...><\/code> generator expression so that the HAL&#8217;s static library is tracking the dependency, but does not seek out the linked target&#8217;s resources during the configure, install, &amp;c. stages. Downstream projects then include the HAL target via <code>find_package<\/code> and collect its include directories via linking against the target, but again do not seek out the HAL&#8217;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.<\/p>\n\n\n\n<p>There are likely many other approaches, but in the time-honored tradition of finding my solution in the last place I&#8217;ve looked&#8230;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Example<\/h2>\n\n\n\n<p>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 <code>CMakeLists.txt<\/code> in the root of the repository, and subbuild <code>CMakeLists.txt<\/code> files in the <code>src\/mock<\/code> and <code>src\/rp2350<\/code> directories with the respective interface implementations. The superbuild uses <code>ExternalProject_Add<\/code> commands to kick of each of the mock and RP2350 platform-dependent library builds:<\/p>\n\n\n\n<pre class=\"wp-block-code has-background\" style=\"background-color:#dddddd\"><code>ExternalProject_Add(\n  ep-${PROJECT_NAME}-mock\n  SOURCE_DIR    ${CMAKE_CURRENT_SOURCE_DIR}\/src\/mock\n    ...\n)\n  ...\nExternalProject_Add(\n  ep-${PROJECT_NAME}-rp2350\n  SOURCE_DIR    ${CMAKE_CURRENT_SOURCE_DIR}\/src\/rp2350\n    ...\n)<\/code><\/pre>\n\n\n\n<p>where the <code>-mock<\/code> target builds with the host toolchain, and the <code>-rp2350<\/code> target builds with the Pico SDK-supplied toolchain. To make the RP2350 variant exportable\/support <code>find_package<\/code>, the CMakeLists.txt in the <code>src\/rp2350<\/code> directory includes an amalgam of the typical Pico SDK application, approach three above, and the typical project export:<\/p>\n\n\n\n<pre class=\"wp-block-code has-background\" style=\"background-color:#dddddd\"><code># Typical Pico SDK usage\nset(PICO_BOARD pico2)\nset(PICO_PLATFORM rp2350-arm-s)\ninclude(pico_sdk_import.cmake)\n  ...\nproject(garp_mc_hal-rp2350)\n  ...\npico_sdk_init()\n  ...\nadd_library(${PROJECT_NAME} STATIC\n  hal_core_rp2350.c\n    ...\n)\n  ...\n# Build stage link target registration\ntarget_link_libraries(${PROJECT_NAME}\n  PRIVATE\n    $&lt;BUILD_INTERFACE:pico_stdlib>\n    $&lt;BUILD_INTERFACE:pico_stdio>\n    $&lt;BUILD_INTERFACE:pico_multicore>\n    $&lt;BUILD_INTERFACE:pico_sync>\n    $&lt;BUILD_INTERFACE:hardware_irq>\n    $&lt;BUILD_INTERFACE:hardware_pwm>\n    $&lt;BUILD_INTERFACE:hardware_spi>\n)\n  ...\n# Typical find_package() support\ninstall(\n  TARGETS\n    ${PROJECT_NAME}\n  EXPORT\n    ${PROJECT_NAME}Targets\n  ARCHIVE\n    DESTINATION ${INSTALL_LIBDIR}\n    COMPONENT lib\n  PUBLIC_HEADER\n    DESTINATION ${INSTALL_INCLUDEDIR}\n    COMPONENT dev\n)\n  ...\ninclude(CMakePackageConfigHelpers)\nwrite_basic_package_version_file(\n  ${CMAKE_CURRENT_BINARY_DIR}\/${PROJECT_NAME}ConfigVersion.cmake\n  VERSION ${PROJECT_VERSION}\n  COMPATIBILITY SameMajorVersion\n)\n\nconfigure_package_config_file(\n  ${SUPERBUILD_ROOT}\/cmake\/${PROJECT_NAME}Config.cmake.in\n  ${CMAKE_CURRENT_BINARY_DIR}\/${PROJECT_NAME}Config.cmake\n  INSTALL_DESTINATION ${INSTALL_CMAKEDIR}\n)\n\ninstall(\n  FILES\n    ${CMAKE_CURRENT_BINARY_DIR}\/${PROJECT_NAME}Config.cmake\n    ${CMAKE_CURRENT_BINARY_DIR}\/${PROJECT_NAME}ConfigVersion.cmake\n  DESTINATION\n    ${INSTALL_CMAKEDIR}\n)\n\ninstall(\n  EXPORT\n    ${PROJECT_NAME}Targets\n  DESTINATION\n    ${INSTALL_CMAKEDIR}\n  COMPONENT\n    dev\n)<\/code><\/pre>\n\n\n\n<p>Not shown is the <code>pico_enable_stdio_usb(...)<\/code> command added since the HAL uses USB CDC for logging support, and <code><a href=\"https:\/\/cmake.org\/cmake\/help\/latest\/module\/GNUInstallDirs.html\" title=\"\">GNUInstallDirs<\/a><\/code> to define common install locations. A pair of standard <code>${PROJECT_NAME}Config.cmake.in<\/code> files are also created the <code>cmake<\/code> directory to support CMake&#8217;s <code><a href=\"https:\/\/cmake.org\/cmake\/help\/latest\/module\/CMakePackageConfigHelpers.html#module:CMakePackageConfigHelpers\" title=\"\">CMakePackageConfigHelpers<\/a><\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Specific GARP CMake Practices<\/h2>\n\n\n\n<p>Within the GARP portfolio, I use <code>${HOME}\/.local\/<\/code> as a default install prefix (i.e. <code>CMAKE_INSTALL_PREFIX<\/code>) so I don&#8217;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 <code>make install<\/code> I&#8217;d like it to install to my host system, but when built as an automated dependency I&#8217;d like it installed to the build tree. To realize this, the GARP portfolio sets a default value for <code>CMAKE_INSTALL_PREFIX<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code has-background\" style=\"background-color:#dddddd\"><code>set(home_dir_ \"$ENV{HOME}\/.local\")\nset(CMAKE_INSTALL_PREFIX\n  ${home_dir_}\n  CACHE PATH \"Installation root directory\")\nset(CMAKE_PREFIX_PATH\n  \"${CMAKE_INSTALL_PREFIX}\"\n  CACHE PATH \"Prefix path for installed packages\")<\/code><\/pre>\n\n\n\n<p>which can be overridden with the <code>-DCMAKE_INSTALL_PREFIX<\/code> 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:<\/p>\n\n\n\n<pre class=\"wp-block-code has-background\" style=\"background-color:#dddddd\"><code>ExternalProject_Add(\n  ep-garp_mc_hal-mock\n    ...\n  CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}\/external\n    ...\n)<\/code><\/pre>\n\n\n\n<p>while the end artifact is built with:<\/p>\n\n\n\n<pre class=\"wp-block-code has-background\" style=\"background-color:#dddddd\"><code>ExternalProject_Add(\n  ep-${PROJECT_NAME}-mock\n    ...\n  CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}\n    ...\n)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p>To support modular and reusable CMake packages that rely on the Raspberry Pi Pico SDK, this article details a strategy for enabling <code>find_package()<\/code> compatibility despite the SDK\u2019s 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 <code>ExternalProject_Add<\/code>, enabling clean, scalable integration across platforms.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This article explores the challenges and strategies for adding support for CMake&#8217;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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":293,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[5,4],"tags":[],"class_list":["post-284","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-pico","category-garp"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/posts\/284","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/comments?post=284"}],"version-history":[{"count":9,"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/posts\/284\/revisions"}],"predecessor-version":[{"id":294,"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/posts\/284\/revisions\/294"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/media\/293"}],"wp:attachment":[{"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/media?parent=284"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/categories?post=284"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/michael-stinger.com\/index.php\/wp-json\/wp\/v2\/tags?post=284"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}