Comprehensive Guide to Managing Dependencies - Uvic-Robotics-Club/uvic-rover-2027 GitHub Wiki
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.
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.
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 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
|
❌ | ❌ |
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_msgsThis creates the correct package.xml, CMakeLists.txt, and directory skeleton. You then follow the steps below to add further dependencies.
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/.actionfiles. Packages that only consume interfaces fromrover_interfacesdo not need them.
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)# ── 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. Reservetarget_link_libraries()for non-ROS system libraries like Eigen, PCL, or OpenCV.
# ── 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()Custom messages, services, and actions shared across subsystems belong in a dedicated interfaces package. No node code lives here — only interface definitions.
src/rover_interfaces/
├── package.xml
├── CMakeLists.txt
├── msg/
│ ├── DriveCommand.msg
│ └── RoverStatus.msg
├── srv/
│ └── SetDriveMode.srv
└── action/
└── Navigate.action
<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>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()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.
For third-party libraries, use standard CMake find_package and target_link_libraries.
find_package(Eigen3 REQUIRED)
target_link_libraries(drive_node Eigen3::Eigen)package.xml: <depend>libeigen3-dev</depend>
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>
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>
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_interfacesCommon error messages:
-
Could not find package '<name>'→ missingfind_package()inCMakeLists.txtor<depend>inpackage.xml -
ament_target_dependenciesnot found → missingfind_package(ament_cmake REQUIRED) - Interface not found at runtime → missing
<exec_depend>rosidl_default_runtime</exec_depend>inrover_interfaces - Build order issue (sibling package not found) → make sure
<depend>rover_interfaces</depend>is in the consuming package'spackage.xmlso colcon knows the order
# 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/DriveCommandBe 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 pushFull 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()- Running
colcon buildfrom insidesrc/or a package directory — always run from the workspace root - Committing
build/,install/, orlog/directories — add them to.gitignore - Putting node code inside
rover_interfaces— interfaces packages should contain only.msg/.srv/.actionfiles - Adding
rosidl_default_generatorsto a subsystem package that only uses interfaces — only the package that defines them needs it - Still using
catkin_package()→ replace withament_package() - Using
target_link_librariesfor ROS packages → useament_target_dependencies()instead - Using
generate_messages()→ replace withrosidl_generate_interfaces() - Sourcing
devel/setup.bash→ sourceinstall/setup.bash -
ament_package()is not the last line inCMakeLists.txt - Missing
<member_of_group>rosidl_interface_packages</member_of_group>inrover_interfaces/package.xml - Forgetting
<depend>rover_msgs</depend>in a subsystem'spackage.xml— colcon won't know to build it first