Introduction
This tutorial introduces the Catkin1 build tool used by ROS2 to a user with little to average experience with the CMake3 buildsystem. The goal of this tutorial is to gently introduce how to build ROS programs in a robust way without overloading the reader with unjustified complexity too quickly.
This tutorial begins with some pretty basic concepts, but ends with the building of collections of ROS packages with a standard Catkin workspace model used in nearly all of the post-hydro ROS tutorials. In-between, the user is guided from the bottom up through each layer of abstraction with some justification for why it needs to exist.
NOTE: This tutorial was written for the ROS Hydro Distribution. Assuming the commands are still accurate, if you wish to follow this tutorial with a different distribution of ROS, any time
hydro
is mentioned, simply replace it with the shortname for that distribution.
Pre-Requisites
- A computer running a recent Ubuntu Linix4 LTS (long-term support) installation
- Minimal experience with the Linux and the command-line interface
- Minimal experience with compiling C++ code
Tools Used
- Ubuntu Linux4
- The bash shell5
- C++6
- The GNU Compiler Collection (GCC)7
- CMake3
- Catkin1
- Git8
- Any plain-text editor (I like vim9).
ROS Packages Used
Number of Windows Needed
- Browser for these instructions
- Window for your text editor
- Terminal to navigate the filesystem and execute build commands
Contents
- Introduction
- Install ROS (If not installed)
- Compiling by Hand Makes it Harder to Build Complex Software
- Defining Build Rules with GNU Makefiles
- Enter Pkg-Config
- CMake: A Cross-Platform, High-Level Buildsystem
- When CMake Alone Isn’t Enough
- The Catkin Develspace
- Using the Catkin-Generated Setup Files
- Out-of-Source Building with Catkin
- The Standard Catkin Workspace and The catkin_make Tool
- Conclusion
Install ROS (If not installed)
For Ubuntu Linux, you can follow the following instructions, for other Linux platforms, see the main ROS installation instructions. As of the writing of this tutorial, ROS packages are only built with the Debian package management system10. This makes it easy to install on debian-based Linux distributions like Ubuntu.
Add the ROS Binary Package Repository
First, add the binary package repository hosted on ros.org to your sysmtem. This will allow you to locate pre-compiled ROS packages, and only needs to be done once, but is idempotent:
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -cs) main" > /etc/apt/sources.list.d/ros-latest.list'
Next, get the ros.org PGP public key. This also only needs to be done once and is also idempotent.This will let you verify that your ROS packages are actually coming from ros.org and not some malicious middle-man. This is done automatically whenever you install a package from ros.org.
wget http://packages.ros.org/ros.key -O - | sudo apt-key add -
Install the Base ROS Packages
First, update the binary package index. This should be done whenever you want to make sure your system knows about the latest versions of binary packages available:
sudo apt-get update
Finally, install the base ROS packages from the ROS “Hydromedusa” distribution:
sudo apt-get install ros-hydro-ros-base
There are lots of other ROS packages available to install, but for this tutorial you only need a few of the “core” packages. To see the list of currently available binary packags, their versions, and build status, you can see the ROS debian package build status page.
Compiling by Hand Makes it Harder to Build Complex Software
Open-Source Software (OSS) development patterns are powerful in part because of the ease with which they re-use existing solutions to problems in order to solve new problems. Open-source robotics software is no different: many robotics applications require many of the same problems to be solved, and many applied systems can be decomposed into subsystems which are very similar.
Organizational challenges arise, however, when you start trying to compile code which has deep trees of dependencies. Each dependency might introduce its own headers, libraries, compile flags, linker flags, or other compile-time utilities needed for testing or code-generation.
In the ROS C++ Hello World Tutorial, an
extremely simple ROS node with the following code was compiled from the source
file hello_world_node.cpp
by directly invoking g++
as shown below:
hello_world_node.cpp
g++ hello_world_node.cpp -o hello_world_node -I/opt/ros/hydro/include -L/opt/ros/hydro/lib -Wl,-rpath,/opt/ros/hydro/lib -lroscpp -lrosconsole
It shouldn’t be surprising that building software directly like the above
example doesn’t scale well. In this case, we only had two dependencies:
roscpp
and rosconsole
. If we had more, we’d need to know the locations of
the headers for each dependency, the names of the libraries, the locations of
the libraries, and other various flags. Not only that, but we would even need
to know the proper order in which the libraries need to be listed.
Not only this, but the above command would fail if it was run on a system where ROS was installed to some place other than the default location. In summary, it makes the following limiting assumptions:
- The user wants to use
g++
to compile the program - ROS is installed to the default location in
/opt/ros
- ROS Hydro is installed (and is the distribution of choice)
- The only library dependencies are
libroscpp
andlibrosconsole
- The source code is in the current directory
- All of the necessary dependencies are installed
Defining Build Rules with GNU Makefiles
If calling g++
by hand like in the previous section is walking, then writing
GNU Makefiles is like riding a bike. You still have to put effort into it, but
you’re getting the mechanical advantage of a tool. In the past, it was
acceptable for small or even medium-sized projects to use GNU Makefiles.
A Makefile
defines high-level “targets” which then make numerous calls to the
compiler to build a collection of source files into binary object files and
link them together. Makefiles don’t have an extension, they are just called
Makefile
and are read by the GNU Make program.
Makefiles, however, do more than just store the build command shown above. GNU Make has its own language which was designed for specifying rules to compile and link code.
The GNU Makefile
For example, the “hello world” example code11 could be built
similarly with the following file named Makefile
. When put in the same
directory as hello_world_node.cpp
, this will allow someone to easily build the
hello_world_node
program even if he or she doesn’t know what the exact
compilation command is.
NOTE: Makefile syntax is very pedantic about tabs vs spaces. If you copy this code, make sure you indent the definitions of the rules with a single tab
\t
character instead of some number of spaces.
Makefile
Building with GNU Make
Then, to build hello_world_node
you can run the make
command in the same
directory as hello_world_node.cpp
and Makefile
:
make hello_world_node
For introspection, make
will write the expanded commands that it executes to
the console:
g++ -c -o hello_world_node.o hello_world_node.cpp -I/opt/ros/hydro/include
g++ -o hello_world_node hello_world_node.o -L/opt/ros/hydro/lib -Wl,-rpath,/opt/ros/hydro/lib -lroscpp -lrosconsole
Unlike our single-line build command, our Makefile
precipitates two commands:
the first to compile the hello_world_node.cpp
source file and the second to link
the hello_world_node
binary object file into the hello_world_node
executable. This
separation allows GNU Make to compile each .cpp
file in your project
individually so that changing one file doesn’t require recompilation of the
entire project.
Whether or not the above commands succeed or fail depends on whether or not ROS
Hydro is installed in /opt/ros/hydro
.
Despite the fact that this has the same exact result as the one-line command given in the previous section, this Makefile is already increasing the robustness of the program in the following ways:
- The
Makefile
makes it clear that thehello_world_node
program can be built by simply invokingmake hello_world_node
in the source directory. - The
Makefile
makes apparent which libraries are needed and where they are expected to be. - If additional
.cpp
files are added to the project, they will be compiled more efficiently since this Makefile separates the compilation and linking steps into two inter-dependent rules. - If the source file
hello_world_node.cpp
hasn’t changed, than subsequent calls to GNU Make won’t waste time recompiling it.
Limitations
Unfortunately, it still suffers from all of the same assumptions as the single-line build command given in the previous section.
Enter Pkg-Config
Some of the assumptions made about the location of the headers and libraries
can be relaxed by using the pkg-config
utility. Many libraries (including
pre-compiled ROS packages) provide .pc
files which describe their include
directories, link directories, libraries, and other required flags. This means
that you don’t need to know the location of each dependency, you just need to be able
to find the .pc
file.
For example, on a standard ROS system install on Linux, the ROS C++ APIs are
defined in the roscpp
package, and the file
/opt/ros/hydro/lib/pkgconfig/roscpp.pc
has the following contents:
prefix=/opt/ros/hydro
Name: roscpp
Description: Description of roscpp
Version: 1.9.50
Cflags: -I/opt/ros/hydro/include -I/usr/include
Libs: -L/opt/ros/hydro/lib -lroscpp -lpthread -l:/usr/lib/libboost_signals-mt.so -l:/usr/lib/libboost_filesystem-mt.so -l:/usr/lib/libboost_system-mt.so
Requires: cpp_common message_runtime rosconsole roscpp_serialization roscpp_traits rosgraph_msgs rostime std_msgs xmlrpcpp
Note that not only does this have the necessary compile flags for roscpp
, but
it also lists rosconsole
as a dependency under the Requires:
field! This
means that if we request the flags for roscpp
we will also get the flags for
rosconsole
(and its other dependencies) for free!
A GNU Makefile that Uses Pkg-Config
Now we can update the Makefile
to use the pkg-config
command-line program
to get these flags so that we can build our ROS C++ program without specifying
the dependencies explicitly:
Makefile
Building with GNU Make and Pkg-Config
Now that Makefile
is using pkg-config
to determine the build flags, we need
to make sure pkg-config
can find the packages. The normal mechanism for this
is the $PKG_CONFIG_PATH
environment variable. The ROS setup files will set
this automatically to include /opt/ros/hydro/lib/pkgconfig
if you source
/opt/ros/hydro/setup.bash
, but you can also set it explicitly like so:
export PKG_CONFIG_PATH=/opt/ros/hydro/lib/pkgconfig:$PKG_CONFIG_PATH
Now you can build hello_world_node
with the Makefile
just like in the
previous section, and GNU Make will display the commands that it executes in
the console:
make hello_world_node
Compared to the output from make
in the previous section, you should see a
new, longer build command which includes all of the dependencies for roscpp
in addition to the features that we know our program is using:
g++ -c -o hello_world_node.o hello_world_node.cpp -I/opt/ros/hydro/include
g++ -o hello_world_node hello_world_node.o -L/opt/ros/hydro/lib -lroscpp -l:/usr/lib/libboost_signals-mt.so -l:/usr/lib/libboost_filesystem-mt.so -lrosconsole -l:/usr/lib/libboost_regex-mt.so -l:/usr/lib/liblog4cxx.so -lxmlrpcpp -lroscpp_serialization -lrostime -l:/usr/lib/libboost_date_time-mt.so -l:/usr/lib/libboost_system-mt.so -l:/usr/lib/libboost_thread-mt.so -lpthread -lcpp_common
Limitations
Unfortunately, while we’ve relaxed several of the assumptions made by both the
initial single-line build method and the GNU Make method, this Makefile
still
has several limitations.
Linux/UNIX Required:
Despite relying only on $PKG_CONFIG_PATH
to find roscpp
, we’re still
assuming this is running on a Linux-like system where GNU Make can be used and
where pkg-config
can be called from the command-line.
Spaghetti Build Files: As we add more source files to this project, we will need to manage the dependencies between different targets manually, or with awkward mechanisms which have been retrofitted into GNU Make over the years.
Mucking Around in the Weeds: Despite creating a high-level interface in the form of a Makefile for the end-user who wants to compile this code, the developer is still required to manually add build rules for executables, libraries, and other targets.
CMake: A Cross-Platform, High-Level Buildsystem
If GNU Makefiles are like riding a bike, then using CMake is like riding a motor scooter: you’re still riding a vehicle, but now you don’t have to turn the gears manually.
On Linux/UNIX platforms by default, CMake literally writes the GNU Makefiles for you. This can be strange to get at first, but the truth is that CMake is just better at it than you are. As such, CMake has its own language in which you declare build targets for which it should generate Makefiles.
What makes CMake really powerful, however, is that this additional layer of abstraction enables it to be used to generate not just GNU Makefiles, but also Mac OS XCode Project Files, Microsoft Visual Studio Project files, and other platform-specific buildystems.
The CMakeLists.txt Fille
In a given directory, CMake reads a file named CMakeLists.txt
which
specifies all of the build rules for that directory. A CMakeLists.txt
file
which does the same thing as the GNU Makefile
of the previous section is as
follows:
CMakeLists.txt
Just like with pkg-config
when we use the CMake find_package(...)
command
with roscpp
as an argument, we will get not only the build flags for
roscpp
, but also all the flags for its dependencies. This greatly simplifies
incorporating dependencies into our programs.
Unlike pkg-config
, however, the find_package(...)
command looks for
CMake config files which contain semantically identical information to
Pkg-Config .pc
files. Also unlike Pkg-Config, this CMake command is not
platform-specific.
For roscpp
, the CMake config file is
/opt/ros/hydro/share/roscpp/cmake/roscppConfig.cmake
and isn’t worth
displaying here, but you can view it easily with the less
command:
less /opt/ros/hydro/share/roscpp/cmake/roscppConfig.cmake
Running find_package(roscpp)
, automatically defines several CMake variables,
including but not limited to ${roscpp_INCLUDE_DIRS}
,
${roscpp_LIBRARY_DIRS}
, and ${roscpp_LIBRARIES}
. These variables follow a
standard naming convention used by many CMake configuration files. These
variables can then be passed into standard CMake commands as shown in the
CMakeLists.txt
file above.
Furthermore, by adding the optional REQUIRED
argument to find_package(...)
,
CMake will report a human-readable error if the package isn’t found.
Building with CMake
Since CMake generates GNU Makefiles, it’s common practice to keep all of the generated
products in a directory called build
. Doing so keeps your source tree tidy
and allows you to clear away the build and easily re-set it if you screw
something up. Create this directory so that your project directory contains the
following two files and single directory:
.
├── build
├── CMakeLists.txt
└── hello_world_node.cpp
Just like with pkg-config
and $PKG_CONFIG_PATH
in the previous section,
CMake needs to know where to look to find packages. There are a few places
where CMake searches but the way these packages are exposed to CMake is via the
$CMAKE_PREFIX_PATH
environment variable. For ROS libraries, you can (again)
make sure you’ve sourced /opt/ros/hydro/setup.bash
, or alternatively, you can
set the path explicitly:
export CMAKE_PREFIX_PATH=/opt/ros/hydro:$CMAKE_PREFIX_PATH
Once your path is set up properly, you can invoke cmake
from the build
directory to generate the GNU Makefiles there. Calling cmake
like so performs
the CMake configuration and generation steps:
cd build
cmake ..
If this succeeds, it should report that the “Build files have been written” and you should have the following files in your project directory:
.
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ └── Makefile
├── CMakeLists.txt
└── hello_world_node.cpp
Now you can invoke GNU Make just like before, but this time, with the
auto-generated Makefile
in the build directory:
make hello_world_node
Just like our hand-written Makefile
, CMake’s generated Makefile
outputs
status information to the screen about what it’s doing. When you run this
make
command, you’ll see something similar to the following:
Scanning dependencies of target hello_world_node
[100%] Building CXX object CMakeFiles/hello_world_node.dir/hello_world_node.cpp.o
Linking CXX executable hello_world_node
[100%] Built target hello_world_node
This additional step might seem onerous at first, but the relative high-level
nature of CMake saves an enormous amount of time when compared to managing GNU
Makefiles manually. Also, by containing all of the side-products of the build
in the build
directory, it keeps your sources organized while you do a lot of
building.
Since CMake is meant to be used a higher level, the generated Makefile
hides
nominal output like the lower-level calls to g++
. If you wish to see these
details, you can temporarily set the VERBOSE
environment variable, and re-run
make
:
VERBOSE=true make hello_world_node
Limitations
Many modern open-source projects rely on CMake because of it’s cross-platform
abstraction and the simplicity with which it lets someone specify complex build
configurations. For any single project whose dependencies are all available as
binary packages at the system level, CMake is a great option. CMakes
find_package(...)
and similar commands provide a great infrastructure for
incorporating system dependencies.
Unfortunately, robotics application code and mid-level robotics software written by researchers isn’t necessarily going to be stable enough or broadly-distributed enough to justify building and hosting as binary packages. The normal result is that a given project will be developed as a single monolithic CMake project with numerous switches depending on which components the user wants to build.
Such large CMake projects tend to develop their own collection of CMake functions and macros and then each part of the project is very heavily connected to every other part. This tends to make refactoring more difficult and makes it harder to integrate different projects developed at different institutions or even just different research groups!
When CMake Alone Isn’t Enough
The goal Catkin is to make local collections of source-code-only packages behave more like system installations of binary packages. This is done not by creating an entirely new buildsystem, but by defining some catkin-specific CMake macros and functions which configure CMake in a special way. As such, you can continue to use CMake in a “standard” fashion, even if it’s being configured in a non-standard way.
Using Catkin in your CMake project
The simplest way to start using Catkin is by declaring the tutorial project is
a “catkin package”. This is done with the catkin_package()
CMake function
(documented
here.
This function becomes available after finding the catkin
package via CMake’s
standard find_package()
mechanism. The CMakeLists.txt
from the previous
section is shown below with these modifications:
CMakeLists.txt
Of the two lines added, find_package(catkin REQUIRED)
makes the Catkin CMake
macros available, and defines some variables, but does nothing else. Calling
catkin_package()
however, will re-configure how CMake builds your code, as
seen in the next section.
Configuring your CMake Project as a Catkin Package (and failing)
Since we’re still just using CMake, you can navigate to the build
directory,
and re-generate the GNU Makefiles to build the project:
cmake ..
This command will fail with several CMake Errors messages similar to those below, surrounded by a bunch of noise and stack traces (note that some absolute paths might be different on your computer depending on where you’ve put the project directory):
CMake Error: File /tmp/hello_world_tutorial/package.xml does not exist.
CMake Error at /opt/ros/hydro/share/catkin/cmake/stamp.cmake:10 (configure_file):
configure_file Problem configuring file
Call Stack (most recent call first):
/opt/ros/hydro/share/catkin/cmake/catkin_package_xml.cmake:61 (stamp)
/opt/ros/hydro/share/catkin/cmake/catkin_package_xml.cmake:39 (_catkin_package_xml)
/opt/ros/hydro/share/catkin/cmake/catkin_package.cmake:95 (catkin_package_xml)
CMakeLists.txt:6 (catkin_package)
CMake Error at /opt/ros/hydro/share/catkin/cmake/catkin_package.cmake:112 (message):
catkin_package() 'catkin' must be listed as a buildtool dependency in the
package.xml
Call Stack (most recent call first):
/opt/ros/hydro/share/catkin/cmake/catkin_package.cmake:98 (_catkin_package)
CMakeLists.txt:6 (catkin_package)
-- Configuring incomplete, errors occurred!
In this case, the first and second error messages are pretty straight-forward
and explain that there’s a missing required file called package.xml
which
should be in the project root right next to CMakeLists.txt.:
CMake Error: File /tmp/hello_world_tutorial/package.xml does not exist.
The third error message, which is actually a separate error from the first, is describing a problem with “the package.xml” which is a non-existent file:
CMake Error at /opt/ros/hydro/share/catkin/cmake/catkin_package.cmake:112 (message):
catkin_package() 'catkin' must be listed as a buildtool dependency in the
package.xml
In the above console output, CMake reports at the bottom that something went wrong, but you need to scroll up to determine what went wrong. Just like when debugging compilation errors, it’s important to start by analyzing the first error that’s reported, because subsequent errors are often the result of trying to configure with erroneous information.
The ability to parse and understand these error messages is an extremely useful skill and if read correctly, they can help you quickly find problems and resolve them.
Adding a package.xml Manifest File
In order for a CMake project to be a valid Catkin package on which other
packages can depend, it needs a package.xml
manifest. This XML file contains
metadata about the package including who’s responsible for it, its version,
under which license it’s released, and its dependencies on other system
packages and catkin packages.
For our simple package, the following package.xml
has all of the required
fields as well as a description of how we depend on the roscpp
package:
package.xml
In the current version of Catkin, different types of dependendencies are
declared explicitly. In this case, we require the roscpp
package both at
“build-time” and at “run-time.” In the context of Catkin, “build-time” refers
to when this package is being built, and “run-time” refers to any time
after this package is built. The subtleties of this will be explained in
another tutorial.
Configuring and Building your CMake Project as a Catkin Package (and succeeding)
Now that we have a valid package.xml
file and it contains all the required
dependencies, we can configure and build the project as a catkin package:
cd build
cmake ..
make
This time, if you read the output of make
, you’ll notice that
hello_world_node
isn’t built in the root of the build
directory. Instead,
it’s built in the build/devel/lib/hello_world_tutorial
directory:
Scanning dependencies of target hello_world_node
[100%] Building CXX object CMakeFiles/hello_world_node.dir/hello_world_node.cpp.o
Linking CXX executable devel/lib/hello_world_tutorial/hello_world_node
[100%] Built target hello_world_node
This is the first meaningful affect of calling catkin_package()
in your
project’s CMakeLists.txt
file and it’s a critically important feature of
how catkin works.
The Catkin Develspace
Remember that the goal of Catkin is to make it so that we can treat a collection of local source packages like a collection of installed binary packages. The mechanism through which this happens involves a directory tree that Catkin builds called the “develspace.”
The develspace is a directory, normally called devel
, which is generated at
compile time, and this is where all final build products (executables,
libraries, etc.) are written. This includes our program hello_world_node
.
If you look at the tree of files in the default develspace,
build/devel
, you see that the develspace has a structure which is very
similar to the root of a UNIX-based filesytem and adheres to the Filesystem
Hierarchy Standard
(FHS). This means
that it has conventionally-named directories like bin
, lib
, share
, etc
etc. (haha…)
build
└── devel
├── env.sh
├── etc
│ └── catkin
│ └── ...
├── lib
│ ├── hello_world_tutorial
│ │ └── hello_world_node
│ └── pkgconfig
│ └── hello_world_tutorial.pc
├── setup.bash
├── setup.sh
├── _setup_util.py
├── setup.zsh
└── share
└── hello_world_tutorial
└── cmake
├── hello_world_tutorialConfig.cmake
└── hello_world_tutorialConfig-version.cmake
There are a few features of the Catkin develspace which stand out or prompt further explanation.
Catkin Generates More Setup Files!
The setup.sh
, setup.bash
, and setup.zsh
files should look familiar
because these files serve the same purpose as the ones in /opt/ros/hydro
. In
fact, if you list the contents of /opt/ros/hydro
it will look almost
identical to our devel
directory. This is because they are both just
Catkin workspaces!
Catkin Generates Pkg-Config and CMake Config Files!
Just like roscpp
and rosconsole
have Pkg-Config and CMake Config files, by
calling catkin_package()
in your project’s CMakeLists.txt
, Catkin has
automatically generated these configuration files for hello_world_tutorial
as
well! This project doesn’t define any header files or libraries, but you can
examine the contents of these files like so:
less devel/lib/pkgconfig/hello_world_tutorial.pc
less devel/share/hello_world_tutorial/cmake/hello_world_tutorialConfig.cmake
less devel/share/hello_world_tutorial/cmake/hello_world_tutorialConfig-version.cmake
Just like before, if devel/lib/pkgconfig
is added to $PKG_CONFIG_PATH
, then
your package will be found when pkg-config
is queried. Similarly, if devel
is
added to $CMAKE_PREFIX_PATH
, then your package will also be found if someone
tries to find it with the CMake find_package()
command.
Unsurprisingly, setting these environment variables like so is exactly what
the aforementioned setup files do. This means that if you source said setup
files, code elsewhere on your system will be able to locate the products of the
hello_world_tutorial
package.
Catkin Puts Executables in the “lib” Directory?
Finally, one surprising feature of Catkin is that it puts executable products
beneath the lib
directory. Specifically, it will put executables from a given
package in a subdirectory of lib
with the name of that package. This is why
hello_world_node
is located at build/devel/lib/hello_world_tutorial/hello_world_node
.
This enables package-relative scoping of executables, and an execute-from-anywhere behavior when using this develspace’s setup files. Package-relative scoping means that a single workspace can have numerous packages’ binaries while keeping them separate in case any two binaries share the same name.
Using the Catkin-Generated Setup Files
Now that you’ve built a complete Catkin develspace, you can source the
catkin-generated setup.bash
and observe how it modifies your environment.
Before doing this, however, you should examine all of the environment variables
which are set by sourcing /opt/ros/hydro/setup.bash
by running the following command:
source /opt/ros/hydro/setup.bash
env | grep "/opt/ros/hydro"
LD_LIBRARY_PATH=/opt/ros/hydro/lib
PATH=/opt/ros/hydro/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
ROS_ROOT=/opt/ros/hydro/share/ros
CPATH=/opt/ros/hydro/include
CMAKE_PREFIX_PATH=/opt/ros/hydro
PYTHONPATH=/opt/ros/hydro/lib/python2.7/dist-packages
PKG_CONFIG_PATH=/opt/ros/hydro/lib/pkgconfig
ROS_PACKAGE_PATH=/opt/ros/hydro/share:/opt/ros/hydro/stacks
ROS_ETC_DIR=/opt/ros/hydro/etc/ros
Next, source the setup file in the develspace generated by Catkin, and re-examine the environment variables to see how they’re changed:
source devel/setup.bash
env | grep "hello_world_tutorial"
LD_LIBRARY_PATH=/tmp/hello_world_tutorial/build/devel/lib:/opt/ros/hydro/lib
PATH=/tmp/hello_world_tutorial/build/devel/bin:/opt/ros/hydro/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
CPATH=/tmp/hello_world_tutorial/build/devel/include:/opt/ros/hydro/include
CMAKE_PREFIX_PATH=/tmp/hello_world_tutorial/build/devel:/opt/ros/hydro
PYTHONPATH=/tmp/hello_world_tutorial/build/devel/lib/python2.7/dist-packages:/opt/ros/hydro/lib/python2.7/dist-packages
ROSLISP_PACKAGE_DIRECTORIES=/tmp/hello_world_tutorial/build/devel/share/common-lisp
PKG_CONFIG_PATH=/tmp/hello_world_tutorial/build/devel/lib/pkgconfig:/opt/ros/hydro/lib/pkgconfig
ROS_PACKAGE_PATH=/tmp/hello_world_tutorial:/opt/ros/hydro/share:/opt/ros/hydro/stacks
As you can see above, the develspace setup files don’t override the ROS system setup files. Instead, they just extend the environment to include the resources in the develspace as well.
Furthermore, you can even source the setup files in the develspace with a
completely clean environment (i.e. without having previously sourced
/opt/ros/hydro/setup.bash
). This works as long as you had the correct
environment set up when you initially built the package.
This because when the setup files are generated at compile time, they capture
whatever is in your $CMAKE_PREFIX_PATH
and “chain” this new workspace off of
whichever workspaces were already in your environment.
NOTE: If things go haywire and you ever need to completely re-set your shell, you can do so by making a new shell and make sure that your
.bashrc
file isn’t sourcing any workspace setup files. Then you can source the system setup file as usual:source /opt/ros/hydro/setup.bash
Referring to Resources in the Develspace
After having sourced devel/setup.bash
ROS command-line tools will be able
to find resources in packages located in the develspace.
For example, the rospack
command line tool should be able to locate the
source directory for the hello_world_tutorial
package from anywhere in
the filesystem. You can see this by running the following command:
rospack find hello_world_tutorial
/tmp/hello_world_tutorial
Furthermore, you can run executables built in the develspace with rosrun
from
anywhere in the filesystem as well. You can try this with hello_world_node
(which may produce an error if you’re not also running a ROS Master):
rosrun hello_world_tutorial hello_world_node
These resource location-resolution tools become really powerful when you begin to have numerous source workspaces with many packages on your system.
Out-of-Source Building with Catkin
In the previous section, we built the hello_world_tutorial
package in the
build
directory in the project’s source directory. A more common practice
both with standard CMake as well as Catkin, however, is to use what’s called an
“out-of-source” build. In this case, our build directory is simply not beneath
our source directory. This way, we can easily distinguish between files which
need to be saved and files which can always be generated or built.
Set Up The Out-of-Source Build
Create a new directory for the out-of-source build, and create the following
directory structure where CMakeLists.txt
, hello_world_node.cpp
, and
package.xml
are identical to the files used previously:
.
├── build
├── devel
└── src
└── hello_world_tutorial
├── CMakeLists.txt
├── hello_world_node.cpp
└── package.xml
This keeps our intermediate build products in build
, our final build products
in devel
and all of our source code in src
.
Configure and Build an Out-of-Source Workspace
Building out-of-source is almost as simple as before when we told CMake to read
the CMakeLists.txt
file one directory up. Now, we still give the path to the
location of the CMakeLists.txt
file, but we also have to tell Catkin which directory
to use as the develspace.
After moving into the build directory, this is done by passing the
CATKIN_DEVEL_PREFIX
CMake variable on the command-line:
cd build
cmake ../src/hello_world_tutorial -DCATKIN_DEVEL_PREFIX=../devel
make
Just like before, you can make a new shell and source one of the setup files in the develspace and use this workspace:
source ../devel/setup.bash
The Standard Catkin Workspace and The catkin_make Tool
Now that you’ve seen how to build your node manually, with GNU Makefiles,
Pkg-Config, and CMake, and seen how Catkin reconfigures CMake’s normal behavior,
you can understand the entire stack involved in the “standard” Catkin workspace
layout used by the catkin_make
program and most ROS tutorials. This standard
workspace layout is similar to the out-of-source build in the previous section,
and is described below..
In the previous sections, we focused on a single Catkin package:
hello_world_tutorial
. The whole goal of Catkin, however, is to make it easy
to use numerous packages in a source-only workspace. In general, you will use
Catkin in this multi-package mode.
Building Multiple Packages in a Single Workspace
Suppose you also wanted to build the robot_state_publisher
package from source in your workspace.
To do this, you could clone the robot_state_publisher
Git repository into your src
directory, like so:
cd src
git clone https://github.com/ros/robot_state_publisher.git -b hydro-devel
This results in the new directory structure:
.
├── build
├── devel
└── src
├── hello_world_tutorial
│ ├── build
│ ├── CMakeLists.txt
│ ├── Makefile
│ ├── minimal_node.cpp
│ └── package.xml
└── robot_state_publisher
├── include
├── src
├── test
├── CMakeLists.txt
├── doc.dox
└── package.xml
At this point, you have two packages that you want to build in the same
workspace, but each of them has a specific CMakeLists.txt
, so you can’t
simply invoke cmake
in order to build them both. You need to have a
unifying CMakeLists.txt
to act as the root of the source tree.
Fortunately, Catkin provides such a CMakeLists.txt
which can be
symbolically-linked into your src
directory. You can either do this
manually, or you can do it with the catkin_init_workspace
command (which does
exactly the same thing):
cd src
ln -s /opt/ros/hydro/share/catkin/cmake/toplevel.cmake CMakeLists.txt
catkin_init_workspace src
At this point your workspace should now have the “standard” layout for a multi-package Catkin workspace:
.
├── build
├── devel
└── src
├── CMakeLists.txt -> /opt/ros/hydro/share/catkin/cmake/toplevel.cmake
├── hello_world_tutorial
└── robot_state_publisher
Building the Standard Catkin Workspace
Now that our src
directory has a “top-level” CMakeLists.txt
, we can use
it to configure a multiple-package CMake build! You can continue to call
cmake
and make
manually, like in the previous section:
cd build
cmake ../src -DCATKIN_DEVEL_PREFIX=../devel
make
Alternatively, you can combine all of these steps and use the catkin_make
tool.
This is a convenience program which, when run from the root of a Catkin workspace
will both configure and build all targets in all packages. To use it, simply
run the following from the same directory that contains build
, devel
, and src
:
catkin_make
The cakin_make tool has numerous command-line options for specifying CMake arguments,
specific targets to compile and many other configuration options. You can run
catkin_make -h
for more information.
Standard Practice VS Catkin Default Behavior
At this point, the motivation of Catkin’s default behavior might be confusing since
the “standard” use with the catkin_make
script requires configuring CMake
with a bunch of additional arguments. This is because what’s actually convenient
is not CMake’s default behavior, and without additional arguments, Catkin shouldn’t
generate any files outside of the build
directory used by CMake. This is the
most likely motivation behind the separation in behavior between Catkin and the
catkin_make
tool.
Conclusion
This tutorial has demonstrated the “full stack” of utilities and libraries that are involved when using the Catkin build tool. This has been by no means a comprehensive picture of all of the features or configuration modes of Catkin, but it has shown all of the stages involved to build both a simple ROS package at each level of abstraction. This has hopefully elucidated what goes on under “the hood” of Catkin when building collections of ROS code.
At the level of abstraction reached in the last section on the Standard Catkin
Workspace, as long as a CMake project uses the catkin_pckage()
CMake function
and has a valid package.xml
file, all you need to do to build it is add it to
your workspace’s src
directory and run catkin_make
! Catkin will find all of
the CMake projects and build their products will be built in a common
develspace, setup files and all!