comprehensive guide to managing dependencies - Uvic-Robotics-Club/uvic-rover-2027 GitHub Wiki

Adding Dependencies to the uvic_rover Workspace (ROS 2 Jazzy)

This guide covers everything needed to correctly add dependencies across the uvic_rover multi-package ROS 2 workspace. It addresses workspace layout, inter-package dependencies, both Python and C++ nodes, custom messages/services, and the colcon build system.


Workspace Structure

The repository is the ROS 2 workspace. Each subsystem lives in its own package under src/. The top-level layout should look like this:

uvic_rover/                        ← git repo root AND workspace root
├── src/
│   ├── rover_bringup/             ← launch files, top-level config
│   ├── rover_interfaces/          ← shared custom messages/services/actions
│   ├── rover_drive/               ← drive subsystem
│   └── rover_arm/                 ← arm subsystem
├── install/                       ← generated by colcon (do not commit)
├── build/                         ← generated by colcon (do not commit)
├── log/                           ← generated by colcon (do not commit)
└── .gitignore

.gitignore must exclude build artifacts:

/build/
/install/
/log/

colcon build is always run from the workspace root (uvic_rover/), not from inside src/ or an individual package.


Key Design Principle: One Package Per Subsystem

Each subsystem package is independent and self-contained. Shared types (messages, services, actions) that cross subsystem boundaries belong in a dedicated interfaces package (e.g., rover_interfaces) rather than inside any one subsystem package. This prevents circular dependencies and makes the build order unambiguous.


Dependency Types Quick Reference

Dependency Type package.xml CMakeLists.txt Custom Interfaces
Standard ROS 2 C++ packages ✅ ✅ If interface-related ✅
Standard ROS 2 Python packages ✅ ✅ (install rule) ❌
C++ system/vendor libraries ✅ (<depend>) ✅ (find_package + target_link_libraries) ❌
Custom .msg / .srv / .action ✅ ✅ ✅
Sibling package in this repo ✅ (same as any dep) ✅ (same as any dep) If consuming interfaces ✅
Python-only pip packages System install or requirements.txt ❌ ❌

Step-by-Step Process

Step 0: Creating a New Subsystem Package

When adding a new subsystem, create the package inside src/ using the appropriate generator so the boilerplate is correct from the start:

cd uvic_rover/src
 
# C++ package
ros2 pkg create --build-type ament_cmake rover_drive \
  --dependencies rclcpp std_msgs
 
# Python package
ros2 pkg create --build-type ament_python rover_drive_py \
  --dependencies rclpy std_msgs
 
# Mixed C++ and Python (start from ament_cmake and add Python manually)
ros2 pkg create --build-type ament_cmake rover_drive \
  --dependencies rclcpp rclpy std_msgs

This creates the correct package.xml, CMakeLists.txt, and directory skeleton. You then follow the steps below to add further dependencies.


Step 1: Update package.xml

package.xml uses format 3 in ROS 2. The simplest declaration for most dependencies is <depend>, covering both build and execution time.

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd"
            schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>rover_drive</name>
  <version>0.1.0</version>
  <description>Drive subsystem for the UVic Rover</description>
  <maintainer email="[email protected]">UVicRobotics</maintainer>
  <license>Apache-2.0</license>
 
  <!-- Build system -->
  <buildtool_depend>ament_cmake</buildtool_depend>
 
  <!-- Core ROS 2 C++ runtime -->
  <depend>rclcpp</depend>
  <depend>std_msgs</depend>
  <depend>sensor_msgs</depend>
 
  <!-- Core ROS 2 Python runtime -->
  <depend>rclpy</depend>
 
  <!-- Sibling package dependency (same repo) — declared identically to any other dep -->
  <depend>rover_interfaces</depend>
 
  <!-- Add additional dependencies here -->
  <!-- <depend>your_dependency</depend> -->
 
  <!-- Custom message generation (only if THIS package defines interfaces) -->
  <!-- <build_depend>rosidl_default_generators</build_depend>  -->
  <!-- <exec_depend>rosidl_default_runtime</exec_depend>       -->
  <!-- <member_of_group>rosidl_interface_packages</member_of_group> -->
 
  <!-- Testing -->
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>
 
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Key rules:

  • Replace <buildtool_depend>catkin</buildtool_depend> with <buildtool_depend>ament_cmake</buildtool_depend>.
  • Replace <run_depend> with <exec_depend> (or the combined <depend>).
  • Sibling packages in the same repo (e.g., rover_interfaces) are declared identically to any external ROS 2 package — colcon resolves the build order automatically from these declarations.
  • Only add the rosidl_* entries if this specific package defines .msg/.srv/.action files. Packages that only consume interfaces from rover_interfaces do not need them.

Step 2: Update CMakeLists.txt

2a. Minimum boilerplate

cmake_minimum_required(VERSION 3.8)
project(rover_drive)
 
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()
 
# ── 1. Find ament and all dependencies ────────────────────────────────────────
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(rover_interfaces REQUIRED)   # sibling package — colcon ensures it's built first
# find_package(your_dependency REQUIRED)

2b. C++ executables

# ── 2. C++ Executables ────────────────────────────────────────────────────────
add_executable(drive_node src/drive_node.cpp)
 
ament_target_dependencies(drive_node
  rclcpp
  std_msgs
  sensor_msgs
  rover_interfaces        # gives access to rover_interfaces message headers
)
 
# For non-ROS system/vendor libraries:
# find_package(Eigen3 REQUIRED)
# target_link_libraries(drive_node Eigen3::Eigen)
 
install(TARGETS drive_node
  DESTINATION lib/${PROJECT_NAME}
)

Note: Use ament_target_dependencies() for all ROS packages (including sibling packages). It correctly propagates include directories and link libraries. Reserve target_link_libraries() for non-ROS system libraries like Eigen, PCL, or OpenCV.

2c. Python nodes

# ── 3. Python Nodes ───────────────────────────────────────────────────────────
ament_python_install_package(${PROJECT_NAME})
 
install(PROGRAMS
  scripts/drive_controller.py
  DESTINATION lib/${PROJECT_NAME}
)

2d. Shared resources

# ── 4. Shared Resources ───────────────────────────────────────────────────────
install(DIRECTORY
  launch
  config
  DESTINATION share/${PROJECT_NAME}
)

2e. Testing and finalize

# ── 5. Testing ────────────────────────────────────────────────────────────────
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
endif()
 
# ── 6. Finalize — must be last ────────────────────────────────────────────────
ament_package()

Step 3: Shared Interfaces Package (rover_interfaces)

Custom messages, services, and actions shared across subsystems belong in a dedicated interfaces package. No node code lives here — only interface definitions.

Recommended structure

src/rover_interfaces/
├── package.xml
├── CMakeLists.txt
├── msg/
│   ├── DriveCommand.msg
│   └── RoverStatus.msg
├── srv/
│   └── SetDriveMode.srv
└── action/
    └── Navigate.action

rover_interfaces/package.xml

<package format="3">
  <name>rover_interfaces</name>
  <version>0.1.0</version>
  <description>Shared ROS 2 interfaces for the UVic Rover</description>
  <maintainer email="[email protected]">UVicRobotics</maintainer>
  <license>Apache-2.0</license>
 
  <buildtool_depend>ament_cmake</buildtool_depend>
 
  <build_depend>rosidl_default_generators</build_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
 
  <!-- Any ROS message types your .msg/.srv files reference -->
  <depend>std_msgs</depend>
  <depend>geometry_msgs</depend>
 
  <member_of_group>rosidl_interface_packages</member_of_group>
 
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

rover_interfaces/CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(rover_interfaces)
 
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
 
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/DriveCommand.msg"
  "msg/RoverStatus.msg"
  "srv/SetDriveMode.srv"
  "action/Navigate.action"
  DEPENDENCIES
    std_msgs
    geometry_msgs
)
 
ament_package()

Using rover_interfaces in a sibling C++ package

After declaring <depend>rover_interfaces</depend> in package.xml and find_package(rover_interfaces REQUIRED) in CMakeLists.txt, include the generated headers normally:

#include "rover_interfaces/msg/drive_command.hpp"
#include "rover_interfaces/srv/set_drive_mode.hpp"

colcon guarantees rover_interfaces is built before any package that depends on it.


Step 4: C++ System Library Dependencies (Non-ROS)

For third-party libraries, use standard CMake find_package and target_link_libraries.

Eigen3

find_package(Eigen3 REQUIRED)
target_link_libraries(drive_node Eigen3::Eigen)

package.xml: <depend>libeigen3-dev</depend>

OpenCV

find_package(OpenCV REQUIRED)
target_link_libraries(drive_node ${OpenCV_LIBS})
target_include_directories(drive_node PRIVATE ${OpenCV_INCLUDE_DIRS})

package.xml: <depend>libopencv-dev</depend>

PCL

find_package(PCL REQUIRED COMPONENTS common io filters)
target_link_libraries(drive_node ${PCL_LIBRARIES})
target_include_directories(drive_node PRIVATE ${PCL_INCLUDE_DIRS})

package.xml: <depend>libpcl-all-dev</depend>


Step 5: Build the Workspace

Always build from the workspace root, not from inside src/ or a package subdirectory.

cd ~/uvic_rover   # workspace root
 
# Build everything
colcon build
 
# Build one subsystem only (colcon automatically builds its dependencies first)
colcon build --packages-select rover_drive
 
# Build a subsystem and everything it depends on
colcon build --packages-up-to rover_drive
 
# Build with symlinks — Python scripts update without a rebuild
colcon build --symlink-install
 
# Build only packages that changed since the last build
colcon build --packages-select-by-dep rover_interfaces   # rebuild dependents of rover_interfaces

Common error messages:

  • Could not find package '<name>' → missing find_package() in CMakeLists.txt or <depend> in package.xml
  • ament_target_dependencies not found → missing find_package(ament_cmake REQUIRED)
  • Interface not found at runtime → missing <exec_depend>rosidl_default_runtime</exec_depend> in rover_interfaces
  • Build order issue (sibling package not found) → make sure <depend>rover_interfaces</depend> is in the consuming package's package.xml so colcon knows the order

Step 6: Source and Test

# Source from workspace root — not package directory
source install/setup.bash
 
# Run nodes
ros2 run rover_drive drive_node
ros2 run rover_drive drive_controller.py
 
# Launch
ros2 launch rover_bringup full_system.launch.py
 
# Verify a message type from rover_interfaces is visible
ros2 interface show rover_interfaces/msg/DriveCommand

Step 7: Commit Changes

Be specific about which package changed:

git add src/rover_drive/package.xml src/rover_drive/CMakeLists.txt
git commit -m "for rover_drive: added rover_interfaces dependency"
git push
 
# When adding a new package entirely:
git add src/rover_arm/
git commit -m "added rover_arm subsystem package"
git push

Complete CMakeLists.txt Template (Subsystem Package)

Full template for a subsystem package with C++, Python, and a dependency on rover_interfaces:

cmake_minimum_required(VERSION 3.8)
project(rover_drive)
 
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()
 
# ── 1. Dependencies ────────────────────────────────────────────────────────────
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclpy REQUIRED)
find_package(std_msgs REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(rover_interfaces REQUIRED)          # shared interfaces sibling package
# find_package(your_dependency REQUIRED)
 
# ── 2. C++ Executables ────────────────────────────────────────────────────────
add_executable(drive_node src/drive_node.cpp)
ament_target_dependencies(drive_node
  rclcpp
  std_msgs
  sensor_msgs
  rover_interfaces
)
install(TARGETS drive_node DESTINATION lib/${PROJECT_NAME})
 
# ── 3. Python Nodes ───────────────────────────────────────────────────────────
ament_python_install_package(${PROJECT_NAME})
install(PROGRAMS scripts/drive_controller.py DESTINATION lib/${PROJECT_NAME})
 
# ── 4. Shared Resources ───────────────────────────────────────────────────────
install(DIRECTORY launch config DESTINATION share/${PROJECT_NAME})
 
# ── 5. Testing ────────────────────────────────────────────────────────────────
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
endif()
 
# ── 6. Finalize — must be last ────────────────────────────────────────────────
ament_package()

Common Mistakes Checklist

  • Running colcon build from inside src/ or a package directory — always run from the workspace root
  • Committing build/, install/, or log/ directories — add them to .gitignore
  • Putting node code inside rover_interfaces — interfaces packages should contain only .msg/.srv/.action files
  • Adding rosidl_default_generators to a subsystem package that only uses interfaces — only the package that defines them needs it
  • Still using catkin_package() → replace with ament_package()
  • Using target_link_libraries for ROS packages → use ament_target_dependencies() instead
  • Using generate_messages() → replace with rosidl_generate_interfaces()
  • Sourcing devel/setup.bash → source install/setup.bash
  • ament_package() is not the last line in CMakeLists.txt
  • Missing <member_of_group>rosidl_interface_packages</member_of_group> in rover_interfaces/package.xml
  • Forgetting <depend>rover_msgs</depend> in a subsystem's package.xml — colcon won't know to build it first
⚠️ **GitHub.com Fallback** ⚠️