ROS2 Workspace Structure - Uvic-Robotics-Club/uvic-rover-2027 GitHub Wiki

This workspace is the root directory that contains the entire codebase. All environment configuration, firmware, and ROS2 packages live here. ROS2 follows a strict structure that must be respected for the build system to work correctly.

rover_ws/                          # Workspace root
├── src/                           # All ROS2 packages live here
│   ├── drive_package/
│   ├── arm_package/
│   ├── comms_package/
│   ├── perception_package/
│   ├── odometry_package/
│   └── rover_interfaces/          # Shared custom messages, services, actions
├── build/                         # Generated by colcon — do not touch
├── install/                       # Generated by colcon — do not touch
├── log/                           # Build and runtime logs — do not touch
├── docker/                        # Docker environment configuration
├── .gitignore
└── README.md

The build/, install/, and log/ directories are generated automatically when you build the workspace. You never edit these manually and they are gitignored.

Packages

A package is the basic unit of organization in ROS2. Each subsystem is its own package. Packages can also exist purely to share things across other packages. An example is the rover_interfaces package which holds all custom message, service, and action definitions so every other package can use these WITHOUT duplicating code.

There are two types of packages:

  • ament_cmake: C++ packages. Uses CMakeLists.txt as the build file.
  • ament_python: Python packages. Uses setup.py as the build file.

A package can also be a mix of both but this adds complexity so only do this where necessary.

To create a new package, run one of these from inside src/:

# C++ package
ros2 pkg create --build-type ament_cmake <package_name>

# Python package
ros2 pkg create --build-type ament_python <package_name>

This scaffolds the required files and folders automatically. Do not create packages manually.

Package Structure

C++ Package

drive_package/
├── include/
│   └── drive_package/
│       └── drive_node.hpp         # Header files — declare classes and functions
├── src/
│   └── drive_node.cpp             # Implementation — the actual node code
├── launch/
│   └── drive_launch.py
├── config/
│   └── params.yaml                # Parameter files
├── test/
│   └── test_drive_node.cpp        # GTest files
├── CMakeLists.txt                 # Build configuration
└── package.xml                    # Package metadata and dependencies

Python Package

perception_package/
├── perception_package/
│   ├── __init__.py
│   └── camera_node.py             # Node code lives here
├── launch/
│   └── perception_launch.py
├── config/
│   └── params.yaml
├── test/
│   └── test_camera_node.py
├── setup.py                       # Build configuration
├── setup.cfg
└── package.xml                    # Package metadata and dependencies

Interfaces Package

rover_interfaces/
├── action/
│   └── MoveArm.action
├── msg/
│   └── RoverStatus.msg
├── srv/
│   └── EnableArm.srv
├── CMakeLists.txt
└── package.xml

Custom messages, services, and actions always live here. This keeps them accessible to every package without creating circular dependencies.

Key Files

package.xml

This is the package manifest. It tells ROS2 what the package is and what it depends on. Every dependency your package uses must be declared here. If it isn't, the build system won't know to fetch it and your package will fail to build on a fresh install.

<?xml version="1.0"?>
<package format="3">
  <name>drive_package</name>
  <version>0.0.1</version>
  <description>Drive subsystem for the rover</description>
  <maintainer email="[email protected]">Your Name</maintainer>
  <license>MIT</license>

  <!-- Build tool — always required for ament_cmake packages -->
  <buildtool_depend>ament_cmake</buildtool_depend>

  <!-- ROS2 core dependency -->
  <depend>rclcpp</depend>

  <!-- Message type dependencies -->
  <depend>std_msgs</depend>
  <depend>geometry_msgs</depend>

  <!-- Dependency on our shared interfaces package -->
  <depend>rover_interfaces</depend>

  <!-- Test dependencies — only needed for building tests -->
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

A common mistake is adding a dependency in code but forgetting to declare it here. The package may build locally if the dependency happens to be installed but will fail on another machine.

CMakeLists.txt

This is the build configuration file. It tells the build system how to compile the package, what to link against, and what to install. This is the most complex file in the package and the one most likely to cause confusing build errors if something is missing.

cmake_minimum_required(VERSION 3.8)
project(drive_package)

# Find ROS2 core and any packages declared in package.xml
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rover_interfaces REQUIRED)

# Add the executable — maps the node name to its source file
add_executable(drive_node src/drive_node.cpp)

# Tell the compiler where to find header files for this target
target_include_directories(drive_node PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>
)

# Link the node against its dependencies
ament_target_dependencies(drive_node
  rclcpp
  std_msgs
  rover_interfaces
)

# Install the compiled executable so ros2 run can find it
install(TARGETS drive_node
  DESTINATION lib/${PROJECT_NAME}
)

# Install launch files so ros2 launch can find them
install(DIRECTORY launch/
  DESTINATION share/${PROJECT_NAME}/launch
)

# Install config files so they are accessible at runtime
install(DIRECTORY config/
  DESTINATION share/${PROJECT_NAME}/config
)

ament_package()

The most common mistakes here are:

  • Adding a new source file but forgetting to add it to add_executable()
  • Adding a new dependency but forgetting to add it to both find_package and ament_target_dependencies
  • Forgetting to install a directory (launch, config) and then wondering why ros2 launch can't find the file

setup.py (Python packages)

The Python equivalent of CMakeLists.txt. Simpler but still requires attention when adding new nodes or data files.

from setuptools import setup
import os
from glob import glob

package_name = 'perception_package'

setup(
    name=package_name,
    version='0.0.1',
    packages=[package_name],
    data_files=[
        # Required — registers the package with ROS2
        ('share/ament_index/resource_index/packages', ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),

        # Install launch files
        (os.path.join('share', package_name, 'launch'), glob('launch/*_launch.py')),

        # Install config files
        (os.path.join('share', package_name, 'config'), glob('config/*.yaml')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    entry_points={
        # This is how ROS2 knows about your nodes
        # Format: 'node_name = package_name.module_name:main_function'
        'console_scripts': [
            'camera_node = perception_package.camera_node:main',
        ],
    },
)

Every new Python node must be registered under console_scripts, without this, ros2 run cannot find it.

Building the Workspace

All build commands are run from the workspace root, never from inside a package.

# Build all packages
colcon build

# Build a specific package only — faster during development
colcon build --packages-select drive_package

# Build a package and everything that depends on it
colcon build --packages-up-to drive_package

# Source the workspace after building — required to use the built packages
source install/setup.bash

source install/setup.bash is required to run in every new terminal to successfully build and utilize ROS2. However, sourcing is handled automatically by the Docker setup, so this should never need to run manually.

Build Order and Dependencies

colcon resolves build order automatically based on package.xml dependencies. If package A depends on package B, colcon builds B first. This means rover_interfaces always builds before any subsystem package since every package depends on it.

If you see a build error saying a package cannot be found, check:

  1. Is it declared in package.xml?
  2. Is it declared in CMakeLists.txt under find_package?
  3. Had the dependency package itself been built successfully?
⚠️ **GitHub.com Fallback** ⚠️