YARP
Yet Another Robot Platform
Using CMake

CMake lets you express the structure of your software in a portable way, that can then let you or others compile it with lots of different tools.

So, for example, you don't have to maintain Makefiles and Visual Studio projects, you can have a common "source" that can generate either.

It is possible to use YARP without CMake. But it isn't as much fun. So we give a basic tutorial on it here.

We gave instructions on installing CMake and getting started with a basic project here:

Now we're going to go into CMake usage in a little more depth. But let's start simple, with a basic "hello world" example.

Hello World Example

Let's use CMake to compile the usual "hello world" program.

Create a directory "cmake_tutorial" to work in. Save the following as "main.cpp" within that directory:

#include <stdio.h>
int main() {
printf("hello world.\n");
return 0;
}

Then save the following as "CMakeLists.txt" within the same directory:

    cmake_minimum_required(VERSION 3.12)
    project(MyProject)
    add_executable(hello)
    target_sources(hello PRIVATE main.cpp)

The first line gives a CMake version number. CMake is evolving with time, so it is useful to warn a user if they have a version of CMake too old to work on your project. More importantly, by giving a version number CMake can support your project indefinitely into the future as its language changes.

The second line gives a name for our project. If we compile using Makefiles, this won't do anything, but if we compile with something like Visual Studio, the project name will be used for our workspace/solution.

The third line says we want to generate a program called "hello" (with whatever extension is common on the operating system used, ".exe" for Windows, nothing for Linux) and that to generate it we use the source file "main.cpp".

Now all we need to do is run CMake and we are ready to compile. In what follows, we describe running CMake on the command line, since it is easier to present. For instructions on using the CMake GUI on Windows, see:

From the command-line, you can see all the options cmake takes by running it without any arguments.

we usually run cmake with a single argument that specifies the directory the source code is in. We could also specify what kind of tool we will want to build with (Unix Makefiles, CodeBlocks, Eclipse, KDevelop3, ...). By default, CMake will produce Unix Makefiles on systems where that make sense. If we want to compile in the same directory as our source code, that directory is ".", and here's what we see when we run cmake:

$ cmake .
-- The C compiler identification is GNU
-- The CXX compiler identification is GNU
-- Check for working C compiler: /usr/bin/gcc
-- Check for working C compiler: /usr/bin/gcc -- works
-- Detecting C compiler info
-- Detecting C compiler info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler info
-- Detecting CXX compiler info - done
-- Configuring done
-- Generating done
-- Build files have been written to: ...

We can then compile; on Windows we would click on the workspace/solution file that CMake has just created, or in Linux we do "make":

$ make
Scanning dependencies of target hello
[100%] Building CXX object CMakeFiles/hello.dir/main.cpp.o
Linking CXX executable hello
[100%] Built target hello

Our program is now ready to run:

$ ./hello
hello world.

Success! But what was the point of all this? The point is that with our source code in C/C++, and our project description in CMake, our code can be compiled easily by users of a vast range of operating systems and tools. This makes collaborating with others a whole lot easier.

Out-of-source Builds

CMake generates a lot of helper files for compilation. In our "hello world" example, here are the files and directories we see after CMake has run:

$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  CMakeLists.txt
main.cpp        Makefile

Normally, we don't actually want all this junk mixed in with our source code. It is very common with CMake to do "out-of-source" builds, where compiling happens in a different directory to the source. So let's try doing this for our "hello world" example. First, let's clean up:

$ rm -rf CMakeCache.txt CMakeFiles cmake_install.cmake Makefile
$ ls
CMakeLists.txt  main.cpp

On Windows, just click on all files other than the two we wrote (CMakeLists.txt and main.cpp) and delete them. Now, let's build our program, but this time in a subdirectory called "build" (or whatever you like):

$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./hello

(On Windows, make a subdirectory "build", then run CMake with the source directory set to where "main.cpp" is and the binary directory set to the "build" directory - then go through the compilation procedure again).

Now if we want to clean up, we can just remove the build directory.

The benefit of this method is that we can compile our code in lots of different ways side by side. This can be useful if you share a file system across several computers running different operating systems or compilers, or if your program takes a set of different compile options that you need to test.

Building a library

We've seen an example of how to build a program. Now let's make a library as well. The library will be called "namer" and it will have a single function "getWorld" that returns the name of the nearest planet.

We choose to put this library in a subdirectory called "namer" - it doesn't really matter where we put it, this is just an example.

Here is a header file "namer/world.h":

#include <string>
std::string getWorld();

And here is the corresponding implementation "namer/world.cpp":

#include <namer/world.h>
std::string getWorld() { return "Earth"; }

And we update our "main.cpp" program to make use of the "getWorld" function:

#include <stdio.h>
#include <namer/world.h>
int main() {
printf("Hello %s\n", getWorld().c_str());
return 0;
}

Note the new include directive, for "world.h" (we could have said "namer/world.h" instead, it is up to us how we want to organize things).

Now we update the "CMakeLists.txt" to tell it about the library and to make sure the header file can be found by both the library and by "main.cpp":

    cmake_minimum_required(VERSION 3.12)
    project(MyProject)
    add_library(namer)
    target_sources(namer PRIVATE namer/world.cpp
                                 namer/world.h)
    target_include_directories(namer PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                                            $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)

    add_executable(hello)
    target_sources(hello PRIVATE main.cpp)
    target_link_libraries(hello PRIVATE namer)

The target_include_directories command makes sure namer/world.h can be found when included in source code. We supply the path relative to CMAKE_CURRENT_SOURCE_DIR, which is a variable pointing to our source directory.

The add_library command tells CMake that we want to make a library called namer (namer.lib on Windows, libnamer.a or libnamer.a on Linux, ...) using the supplied source code. Note that header files should be included in this list (although it will work without them). There are extra commands you can use to organize code into different groups for presentation in IDEs like visual studio, but we'll skip over that.

The target_link_libraries says that program hello needs library namer.

And that's all! CMake will figure out all the details needed for your operating system and compiler to compile the library and link it to your program.

Building a library and program separately

We've just seen how to build a library and a program all together in one project. In real life, libraries and programs often are written by different people working on different projects. Let's simulate that by breaking up our library and program into two separate projects.

Add the following CMake file for the library in "namer/CMakeLists.txt":

    cmake_minimum_required(VERSION 3.12)
    project(Namer)
    add_library(namer)
    target_sources(namer PRIVATE world.cpp world.h)
    target_include_directories(namer PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                                            $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)

This is very similar to what we had before, except with our program pruned out.

Make a subdirectory "hello" and move "main.cpp" into it. Then add "hello/CMakeLists.txt":

    cmake_minimum_required(VERSION 3.12)
    project(hello)
    find_package(Namer REQUIRED)
    add_executable(hello)
    target_sources(hello PRIVATE main.cpp)
    target_include_directories(hello PRIVATE "${Namer_INCLUDE_DIRS}")
    target_link_libraries(hello PRIVATE ${Namer_LIBRARIES})

This is also similar to what we had before, except we now use the "find_package" command. This command is used to find external material that your project needs.

If you try configuring and compiling the library, it will work fine. But the executable will not work yet. You will get an error like this (depends on CMake version):

find_package could not find module FindNamer.cmake or a configuration
file for package Namer.  Adjust CMAKE_MODULE_PATH to find
FindNamer.cmake or set Namer_DIR to the directory containing a CMake
configuration file for Namer.  The file will have one of the following
names:
  NamerConfig.cmake
  namer-config.cmake

Basically, we haven't specified how to find the package Namer yet. CMake tried some ways it knows but none of them worked.

One way we can help CMake find the Namer package (which will be our namer library) is by writing a helper script called FindNamer.cmake. This is just another file written in the CMake language that pokes around in all the places our library might be hiding. Here is an example (put this in "hello/FindNamer.cmake"):

  find_path(Namer_INCLUDE_DIRS world.h /usr/include "$ENV{NAMER_ROOT}")

  find_library(Namer_LIBRARIES namer /usr/lib "$ENV{NAMER_ROOT}")

  set(Namer_FOUND TRUE)

  if (NOT Namer_INCLUDE_DIRS)
    set(Namer_FOUND FALSE)
  endif (NOT Namer_INCLUDE_DIRS)

  if (NOT Namer_LIBRARIES)
    set(Namer_FOUND FALSE)
  endif (NOT Namer_LIBRARIES)

The important parts here are the "find_path" and "find_library" commands, which look for the header file world.h and the namer library. I wrote them to check the Unix standard locations first, then a directory specified by an environment variable.

Now modify "hello/CMakeLists.txt" so that it can find this script:

    cmake_minimum_required(VERSION 3.12)
    set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}")
    find_package(Namer REQUIRED)
    include_directories("${Namer_INCLUDE_DIRS}")
    add_executable(hello)
    target_sources(hello PRIVATE main.cpp)
    target_link_libraries(hello PRIVATE ${Namer_LIBRARIES})

If we try again, configuration will still fail since the search path we gave for "find_path" and "find_library" doesn't actually include the needed files. We could copy them, or have added a hard-coded directory to find_path and find_library pointing to where the files are on our hard drive - but better, in the CMake GUI on windows or by running "ccmake ." on Linux, we can just fill in the directories there.

main
int main(int argc, char *argv[])
Definition: yarpros.cpp:261