Introduction

CMake is the most widely used build system tool in the C++ programming landscape. Troubleshooting its build errors can be frustrating for engineers new to the ecosystem, especially those in robotics using ROS middleware, the most popular framework used by robotics engineers in the field. A common challenge arises when integrating third-party CMake projects, only to encounter cryptic errors that take hours to resolve. This is partly due to CMake’s official documentation, which, while detailed, often lacks clear, practical examples.

I’ve been both a victim and a culprit of this problem! In this post, I’ll walk through a structured approach to troubleshooting CMake errors by making the best use of its modern features.

Example Overview

We’ll use a repository that integrates Boston Dynamics Spot API with the MuJoCo physics engine in a ROS 2 environment. The issue? A missing header file (cv_bridge.h).

alt

Note: This example is chosen solely to illustrate a common CMake build error. The repository only supports ROS 2 Humble version, but we’re attempting to build it in ROS 2 Jazzy version (and beyond), where cv_bridge package was updated, modifying its include directory structure.

Troubleshooting Steps

1. Think of CMake Like OOP

I like to think of targets in modern CMake as similar to classes in an object-oriented programming (OOP) language with properties and access specifiers. A target’s dependencies, such as libraries and header files, function like inherited properties in OOP, affecting how they link to executables. This article explains the concept well. As with any programming language, when build or configuration errors arise, the best first step is usually to add relevant print/debug statements to gain more insight into the issue. In this example, the error message directly points to the problem, i.e., failure in a find_package command.

2. Understanding find_package

The error occurs because CMake can’t locate cv_bridge.h, despite the package being listed as a dependency.

The official documentation of find_package mentions that this command searches for package-specific details (libraries, include files, etc.).

The typical search modes, as listed in the official documentation, are given below. This is loosely analogous to polymorphism in OOP; the mechanism for finding a package can be seen as a common interface, with the two modes acting as classes with different functionality.

  • Config mode: Uses a package-specific configuration file, providing more flexibility to the target in how the found package’s configuration is used or even modified.
  • Module mode: Searches CMake’s built-in modules and paths in CMAKE_MODULE_PATH.

3. Finding Where CMake Looks for Packages

We now know that CMake may find cv_bridge package via either mode. So, we need to determine its source considering both modes of search:

  • Config mode: The <PackageName>_DIR variable in CMakeCache.txt shows where a package was found. Alternatively, print <PackageName>_CONFIG variable after the find_package statement. This is shown in the below steps.

    • Patch applied to simulation/mujoco’s CMakeLists.txt to print cv_bridge_CONFIG if it was set.

      diff --git a/simulation/mujoco/CMakeLists.txt b/simulation/mujoco/CMakeLists.txt
      index 8c98978..d90d9f1 100644
      --- a/simulation/mujoco/CMakeLists.txt
      +++ b/simulation/mujoco/CMakeLists.txt
      @@ -31,6 +31,8 @@ find_package(tf2 REQUIRED)
       find_package(tf2_ros REQUIRED)
       find_package(nav_msgs REQUIRED)
       find_package(cv_bridge REQUIRED)
      +message(DEBUG "cv_bridge config: ${cv_bridge_CONFIG}")
      +
       # BLASFEO Option
       if(NOT TARGET mujoco)
         add_library(mujoco SHARED IMPORTED)
      
    • Colcon build command enabling printing of CMake debug statements (applies to a ROS2 workspace only):

      colcon build --symlink-install --event-handlers console_direct+ --cmake-clean-first --cmake-args " --log-level=Debug"
      
      • Note: In a normal CMake project, the command would be of the form:
        cmake --build . --log-level=Debug
        
  • Module mode: Print CMAKE_MODULE_PATH to see where CMake looks. It selects the first matching path.

4. Resolving the Issue

On my system, CMake finds cv_bridgeConfig.cmake at /opt/ros/jazzy/share/cv_bridge/cmake.

Checking the root installation path (/opt/ros/jazzy) for a relevant include directory (i.e., /opt/ros/jazzy/include/cv_bridge), we see cv_bridge.h was deprecated and replaced with cv_bridge.hpp. Hence, updating the include statement in simulation/mujoco/src/MuJoCoMessageHandler.cpp resolves the issue, allowing the project to build successfully in a ROS 2 Jazzy workspace.

Summary

The key takeaways from this example are:

  • Understand CMake’s dependency resolution: Treat CMake scripts like an object-oriented programming language. Targets inherit dependencies, and commands like find_package work in different modes, similar to OOP polymorphism, affecting where libraries and include files are sourced from.
  • Diagnose before fixing: Always check where CMake finds packages/modules/libraries. Inspect CMakeCache.txt or CMAKE_MODULE_PATH, and liberally add print statements (message(DEBUG ...)) to fix dependency issues. Extend this principle to other (yes, bigger) build issues.