Chapter 12: ROS/ROS2 Foundations (Practical Intro)
Lesson 3: Packages, Workspaces, and Launch Files
This lesson introduces the software-organization core of ROS/ROS2: packages as atomic reusable units, workspaces as build and overlay environments, and launch files as formal orchestration scripts for multi-node robot systems. We connect these ideas to dependency graphs, build-order correctness, and reproducible deployment.
1. Conceptual Overview
In ROS/ROS2, everything is organized into packages. A package is the smallest unit of distribution, compilation, and runtime discovery. Packages live inside a workspace, which is the directory tree that colcon builds and installs. When your robot application needs several nodes started with parameters and remappings, you use a launch file to declare the system configuration once and run it consistently.
flowchart TD
A["Write code in a package"] --> B["Place packages in workspace/src"]
B --> C["Declare deps in package.xml"]
C --> D["Build workspace with colcon"]
D --> E["Source install/setup.bash"]
E --> F["Run nodes manually or via launch file"]
F --> G["Robot application executes"]
By the end of this lesson you should be able to:
- Explain the ROS package as a dependency-aware module.
- Build and overlay workspaces with colcon.
- Write and install launch files to run multi-node graphs.
2. Packages as Dependency-Graph Modules
Let \( \mathcal{P} = \{p_1,\dots,p_n\} \) be the set of packages in a workspace. Dependencies form a directed graph \( G = (\mathcal{P}, E) \), where \( (p_i, p_j) \in E \) means “package \( p_i \) depends on \( p_j \)” (build, exec, or test dependency). The build system must choose an order that respects this relation.
A build order is a permutation \( \pi \) of packages such that for every edge \( (p_i,p_j) \), \( \pi(p_j) < \pi(p_i) \). This is exactly a topological ordering.
Proposition 1 (Topological build order exists iff no cycles). A dependency graph \( G \) admits a valid build order if and only if \( G \) is a DAG (directed acyclic graph).
Proof.
(Only if) Suppose a valid build order \( \pi \) exists. If \( G \) had a directed cycle \( p_{i_1} \rightarrow p_{i_2} \rightarrow \cdots \rightarrow p_{i_k} \rightarrow p_{i_1} \), then the ordering constraints imply \( \pi(p_{i_1}) < \pi(p_{i_k}) < \cdots < \pi(p_{i_1}) \), a contradiction. Therefore no cycles exist.
(If) If \( G \) is acyclic, Kahn’s algorithm repeatedly removes any node with indegree zero, appending it to the order. A DAG always has at least one indegree-zero node; otherwise following incoming edges forever would create a cycle. Hence the procedure terminates after \( n \) removals producing a topological order. ∎
ROS2 encodes edges of \( G \) in each package’s
manifest package.xml (and in build scripts). The manifest
also declares metadata, licensing, and build type (e.g.,
ament_cmake or ament_python).
Minimal ROS2 package manifest (illustrative).
<?xml version="1.0" ?>
<package format="3">
<name>my_robot_pkg</name>
<version>0.0.1</version>
<description>Intro example package</description>
<maintainer email="you@uni.edu">Your Name</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
Interpreting the manifest as graph data, \( \deg^{-}(p_i) \) is the number of dependencies imported by \( p_i \). A clean ROS workspace aims to keep indegrees small to preserve modularity and avoid tight coupling.
3. Workspaces and Build Semantics
A ROS2 workspace typically contains four main directories:
src/ (source packages), build/ (intermediate
build products), install/ (install tree used by runtime
discovery), and log/. Building is done at the workspace
root with: colcon build (often with
--symlink-install for fast iteration).
Formally, let a workspace be a tuple \( W = (\mathcal{P}_W, \preceq_W) \) where \( \mathcal{P}_W \) is its package set and \( \preceq_W \) is the dependency partial order induced by edges in \( G \): \( p_j \preceq_W p_i \iff p_i \) depends on \( p_j \) by some path.
Colcon computes a topological order \( \pi_W \) extending \( \preceq_W \), and then builds packages in that order. The correctness follows directly from Proposition 1.
Overlays. You can “layer” workspaces: \( W_1 \triangleright W_0 \) means “overlay \( W_1 \) on top of base \( W_0 \)”. Runtime lookup then resolves a package name to the highest-priority definition:
\[ R(p) = \arg\max_{W \in \{W_1, W_0\}} \mathbf{1}[p \in \mathcal{P}_W], \]
where \( R(p) \) returns the version of package \( p \) in the topmost workspace that contains it. This enables safe experimentation: you can override a dependency locally without modifying the system installation.
4. Creating and Structuring Packages
In ROS2, packages are created inside workspace/src using
the CLI. Two common build types are:
ament_cmakefor C/C++ packages.ament_pythonfor Python packages.
The command is:
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake my_cpp_pkg
ros2 pkg create --build-type ament_python my_py_pkg
This generates the correct minimum files (e.g.,
CMakeLists.txt, package.xml,
setup.py, setup.cfg).
A typical C++ package layout is:
my_cpp_pkg/
CMakeLists.txt
package.xml
include/my_cpp_pkg/...
src/...
launch/...
A typical Python package layout is:
my_py_pkg/
package.xml
setup.py
setup.cfg
resource/my_py_pkg
my_py_pkg/__init__.py
my_py_pkg/...
launch/...
5. Multi-language Examples in One Workspace
Below are small “hello-robot” style examples showing how packages in different languages coexist in the same workspace and are built by colcon. The runtime graph (topics/services) is covered in Lesson 2; here we focus on packaging.
5.1 C++ Package (ament_cmake)
File: my_cpp_pkg/src/simple_publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class SimplePublisher : public rclcpp::Node {
public:
SimplePublisher() : Node("simple_publisher"), count_(0) {
pub_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
timer_ = this->create_wall_timer(
std::chrono::milliseconds(500),
std::bind(&SimplePublisher::tick, this)
);
}
private:
void tick() {
std_msgs::msg::String msg;
msg.data = "hello " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: %s", msg.data.c_str());
pub_->publish(msg);
}
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
int count_;
};
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<SimplePublisher>());
rclcpp::shutdown();
return 0;
}
CMake snippet: my_cpp_pkg/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(my_cpp_pkg)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(simple_publisher src/simple_publisher.cpp)
ament_target_dependencies(simple_publisher rclcpp std_msgs)
install(TARGETS simple_publisher
DESTINATION lib/${PROJECT_NAME})
ament_package()
5.2 Python Package (ament_python)
File:
my_py_pkg/my_py_pkg/simple_subscriber.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class SimpleSubscriber(Node):
def __init__(self):
super().__init__('simple_subscriber')
self.sub = self.create_subscription(
String, 'chatter', self.cb, 10)
def cb(self, msg):
self.get_logger().info(f"Received: {msg.data}")
def main():
rclpy.init()
rclpy.spin(SimpleSubscriber())
rclpy.shutdown()
if __name__ == "__main__":
main()
setup.py entry point:
from setuptools import setup
package_name = 'my_py_pkg'
setup(
name=package_name,
version='0.0.1',
packages=[package_name],
install_requires=['setuptools'],
zip_safe=True,
entry_points={
'console_scripts': [
'simple_subscriber = my_py_pkg.simple_subscriber:main',
],
},
)
5.3 Java Package (conceptual, rcljava)
ROS2 has an official Java client library (rcljava). The
packaging steps mirror other languages, but you use Gradle/Maven. The
code below is a minimal publisher.
import org.ros2.rcljava.RCLJava;
import org.ros2.rcljava.node.Node;
import org.ros2.rcljava.publisher.Publisher;
import std_msgs.msg.String;
public class SimplePublisherJava {
public static void main(String[] args) {
RCLJava.rclJavaInit();
Node node = RCLJava.createNode("simple_publisher_java");
Publisher<String> pub = node.createPublisher(String.class, "chatter");
int count = 0;
while (RCLJava.ok()) {
String msg = new String();
msg.setData("hello " + count++);
pub.publish(msg);
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
RCLJava.shutdown();
}
}
5.4 MATLAB / Simulink (ROS Toolbox)
MATLAB’s ROS Toolbox provides ROS2 interfaces. A minimal MATLAB publisher:
% Requires Robotics System Toolbox / ROS Toolbox
node = ros2node("/matlab_node");
pub = ros2publisher(node, "/chatter", "std_msgs/String");
msg = ros2message("std_msgs/String");
for k = 1:10
msg.data = "hello " + string(k);
send(pub, msg);
pause(0.5);
end
clear node pub
In Simulink, the equivalent uses a “ROS 2 Publisher” block configured
for topic /chatter and message type
std_msgs/String; the block is placed inside a subsystem
representing a packaged node.
6. Launch Files: Formal Multi-node Orchestration
Launch files specify which executables to run, their
namespaces, parameters, remappings, and sometimes conditional logic.
ROS2 supports launch frontends in Python, XML, and YAML and executes
them with ros2 launch.
We can model a launch file as a mapping:
\[ \mathcal{L} : \mathcal{C} \rightarrow (\mathcal{E}, \Theta, \rho), \]
where configuration space \( \mathcal{C} \) includes launch arguments, \( \mathcal{E} \) is the set of executables to start, \( \Theta \) is a parameter assignment function, and \( \rho \) is a remapping relation. A correct launch must satisfy: for every executable \( e \in \mathcal{E} \), all package dependencies required by \( e \) appear in the transitive closure of the workspace dependency graph.
Example launch in Python. Put this into
my_robot_pkg/launch/demo.launch.py:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
pub_node = Node(
package='my_cpp_pkg',
executable='simple_publisher',
name='pub',
namespace='demo',
parameters=[{'rate_hz': 2.0}],
remappings=[('chatter', 'demo_chatter')]
)
sub_node = Node(
package='my_py_pkg',
executable='simple_subscriber',
name='sub',
namespace='demo',
remappings=[('chatter', 'demo_chatter')]
)
return LaunchDescription([pub_node, sub_node])
To install launch files in a CMake package, add:
install(DIRECTORY launch
DESTINATION share/${PROJECT_NAME})
Then rebuild and run:
colcon build --symlink-install
source install/setup.bash
ros2 launch my_robot_pkg demo.launch.py
flowchart LR
L["Launch description"] --> A1["Action: start publisher"]
L --> A2["Action: start subscriber"]
A1 --> P1["Process pub"]
A2 --> P2["Process sub"]
P1 --> T["Topic demo_chatter"]
P2 --> T
7. Practical Guidelines
- One concept per package. Keep packages cohesive: drivers, algorithms, and demos in separate packages.
- Declare dependencies minimally. Every extra edge in \( G \) reduces reuse and raises rebuild cost.
- Prefer launch for system runs. If you need >1 terminal, you need a launch file.
- Use overlays for experiments. Overlay only what you are modifying, leaving base stable.
- Version-control the workspace. Your robot is reproducible only if its package set is.
8. Problems and Solutions
Problem 1 (Dependency cycles): Consider packages \( A,B,C \) with dependencies \( A \rightarrow B \), \( B \rightarrow C \), \( C \rightarrow A \). Show that no valid build order exists.
Solution:
The dependencies form a directed cycle. By Proposition 1, a topological order exists iff the graph is acyclic. Because \( A \prec B \prec C \prec A \) would be required, we obtain a contradiction. Hence colcon must refuse the build or you must remove an edge (split functionality or refactor shared code into a fourth package).
Problem 2 (Compute a build order): Let dependencies be \( D \rightarrow B \), \( D \rightarrow C \), \( B \rightarrow A \), \( C \rightarrow A \). Provide one valid build order.
Solution:
Packages with indegree zero: \( A \). After building \( A \), both \( B \) and \( C \) become indegree zero. One valid order: \( A, B, C, D \). Another is \( A, C, B, D \).
Problem 3 (Overlay resolution): Suppose base workspace \( W_0 \) contains packages \( \{x,y\} \), and overlay workspace \( W_1 \) contains \( \{y,z\} \). Using the resolution function in Section 3, what does \( R(y) \) return?
Solution:
Because \( y \in \mathcal{P}_{W_1} \), the argmax selects \( W_1 \). Hence \( R(y) \) resolves to the overlay version, shadowing the base.
Problem 4 (Launch correctness): A launch file starts executables from packages \( p_i \). Give a sufficient condition on the dependency graph so that launch is guaranteed to find all executables at runtime.
Solution:
A sufficient condition is: for each started package \( p_i \), every runtime dependency package is in the transitive closure of the workspace:
\[ \forall p_i \in \mathcal{E},\; \operatorname{Deps}(p_i) \subseteq \mathcal{P}_W . \]
Since colcon installs executables for all packages in
\( \mathcal{P}_W \), and the environment is sourced
from install/, runtime discovery succeeds.
9. Summary
Packages are the modular building blocks of ROS/ROS2; their manifests induce a dependency DAG that determines build order. Workspaces are environments that colcon builds into install trees, and overlays provide priority-based shadowing for safe experimentation. Launch files are formal system configurations, mapping launch arguments to executable sets, parameters, and remappings for reproducible multi-node runs.
10. References
- Quigley, M., Conley, K., Gerkey, B., Faust, J., Foote, T., Leibs, J., Wheeler, R., & Ng, A. (2009). ROS: An open-source Robot Operating System. ICRA Workshop on Open Source Software, 3(3), 1–6.
- Koubaa, A. (2017). Robot Operating System (ROS): The Complete Reference (Vol. 1–3). Springer Tracts in Advanced Robotics.
- Gerkey, B., & Mataric, M.J. (2004). A formal analysis and taxonomy of task allocation in multi-robot systems. International Journal of Robotics Research, 23(9), 939–954.
- Brooks, R.A. (1986). A robust layered control system for a mobile robot. IEEE Journal of Robotics and Automation, 2(1), 14–23.
- Colledanchise, M., & Ögren, P. (2018). Behavior Trees in Robotics and AI: An Introduction. Annual Review of Control, Robotics, and Autonomous Systems, 1, 355–381.
- Brugali, D., Scandurra, P., & Nieri, M. (2019). Component-based robotic engineering (CBSE): a survey. Robotics and Autonomous Systems, 117, 39–55.