A Gentle Introduction to Catkin

This tutorial introduces the Catkin build tool used by ROS to a user with little to average experience with the CMake buildsystem.
Wed, Feb 12, 2014

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 Linux5
  • The bash shell6
  • C++7
  • The GNU Compiler Collection (GCC)8
  • CMake9
  • Catkin10
  • Git11
  • Any plain-text editor (I like vim12).

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

    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 system13. 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:

    {% include hello_world_node.cpp.md %}

    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 and librosconsole
    • 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 code14 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
    # Declare our preferred compiler
    CC=g++
    # Declare the compile flags
    CFLAGS=-I/opt/ros/hydro/include
    # Declare the linker flags
    LDFLAGS=-L/opt/ros/hydro/lib -Wl,-rpath,/opt/ros/hydro/lib -lroscpp -lrosconsole
    
    # A rule which satisfies any dependencies with the ".o" extension by compiling
    # (but not linking) the corresponding ".cpp" file with the same basename
    %.o: %.cpp
      $(CC) -c -o $@ $< $(CFLAGS)
    
    # A rule to build the hello_world_node program
    hello_world_node: hello_world_node.o 
      $(CC) -o hello_world_node hello_world_node.o $(LDFLAGS)
    

    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 the hello_world_node program can be built by simply invoking make 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:

    {% include code_caption.html filename="Makefile" %}

    # Declare our preferred compiler
    CC=g++
    # Declare the compile flags
    CFLAGS=$(shell pkg-config --cflags roscpp)
    # Declare the linker flags
    LDFLAGS=$(shell pkg-config --libs roscpp)
    
    # A rule which satisfies any dependencies with the ".o" extension by compiling
    # (but not linking) the corresponding ".cpp" file with the same basename
    %.o: %.cpp
      $(CC) -c -o $@ $< $(CFLAGS)
    
    # A rule to build the hello_world_node program
    hello_world_node: hello_world_node.o 
      $(CC) -o hello_world_node hello_world_node.o $(LDFLAGS)
    

    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:

    {% include code_caption.html filename="CMakeLists.txt" %}

    # Declare the version of the CMake API for forward-compatibility
    cmake_minimum_required(VERSION 2.8)
    
    # Declare the name of the CMake Project
    project(hello_world_tutorial)
    
    # Find and get all the information about the roscpp package
    find_package(roscpp REQUIRED)
    
    # Add the headers from roscpp
    include_directories(${roscpp_INCLUDE_DIRS})
    # Define an execuable target called hello_world_node 
    add_executable(hello_world_node hello_world_node.cpp)
    # Link the hello_world_node target against the libraries used by roscpp
    target_link_libraries(hello_world_node ${roscpp_LIBRARIES})
    

    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 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:

    {% include code_caption.html filename="CMakeLists.txt" %}

    # Declare the version of the CMake API for forward-compatibility
    cmake_minimum_required(VERSION 2.8)
    
    # Declare the name of the CMake Project
    project(hello_world_tutorial)
    
    # Find Catkin
    find_package(catkin REQUIRED)
    # Declare this project as a catkin package
    catkin_package()
    
    # Find and get all the information about the roscpp package
    find_package(roscpp REQUIRED)
    
    # Add the headers from roscpp
    include_directories(${roscpp_INCLUDE_DIRS})
    # Define an execuable target called hello_world_node 
    add_executable(hello_world_node hello_world_node.cpp)
    # Link the hello_world_node target against the libraries used by roscpp
    target_link_libraries(hello_world_node ${roscpp_LIBRARIES})
    

    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:

    {% include code_caption.html filename="package.xml" %}

    <package>
      <!-- Package Metadata -->
      <name>hello_world_tutorial</name>
      <maintainer email="you@example.com">Your Name</maintainer>
      <description>
        A ROS tutorial.
      </description>
      <version>0.0.0</version>
      <license>BSD</license>
    
      <!-- Required by Catkin -->
      <buildtool_depend>catkin</buildtool_depend>
    
      <!-- Package Dependencies -->
      <build_depend>roscpp</build_depend>
      <run_depend>roscpp</run_depend>
    </package>
    

    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!

    References