Getting Started with CMake - 2 - Manage large project

Jeff posted on  (updated on )

Table of Contents

Build large project manually

Let's take a look at life without CMake, imagine we are building an executable in C, called executable, from the following files:

  1. Source file: executable.c func.c
  2. Header file: func.h
  3. Static lib and header file from same project: libxxx.a xxx.h
  4. External lib that are installed on the system like pthread

To manually build the final executable, we must tell the compiler exactly what file to compile, what library to link, and where to search. The command line would look like this:

clang -o executable executable.c src/func.c libxxx/libxxx.a -Iinclude/ -Ilibxxx/include

The CMake counterpart would look like this:

cmake_minimum_required(VERSION 3.15.0)
project(playground VERSION 0.1.0 LANGUAGES C)

add_subdirectory(libxxx)

add_executable(executable executable.c src/func.c)
target_include_directories(executable PUBLIC "include")
target_link_libraries(executable PUBLIC libxxx)

We will use an example below to show how to achieve each goal in CMake.

Demo

Create library

A final executable (or library) can depend on many other libraries, so let's first see how to declare and build a library in CMake. Let's create a library called libxxx

libxxx
├── CMakeLists.txt
├── include
│   └── libxxx.h
└── libxxx.c

1. Source files

For demo purposes, let's just add a method to print Hello World for our lib.

// .h
#pragma once

void libxxx_print();

// .c
#include "libxxx.h"

#include <stdio.h>
void libxxx_print() {
    printf("From libxxx: Hello, World!\n");
}

Note that we deliberately use #include "libxxx.h even though the relative path should be include/libxxx.h. This is to make sure we properly set the CMake search path manually (so it could fail if we made the wrong assumption about include path).

2. CMakeLists.txt

add_library(libxxx libxxx.c)
target_include_directories(libxxx PUBLIC "include/") // Note it can be a relative path

Let's understand what's happening here.

add_library creates a target of library type. Official document here. The first parameter becomes the name of the library, we will refer to this name if later in our project we want to use it. What follows are source files that are needed to build this library, note we don't specify header files here.

Now obviously the source file uses #include "libxxx.h", we need to let the compiler know where to look for this header file, so next we use target_include_directories. Official document here. As the name implies, this command adds provided directories to the #include search path for the provided target ONLY.

In our example, that means when building libxxx, "include/" will be searched for header files. This search path is only in effect when building libxxx, if we are building libyyy, this search path will not be added.

Create executable

Now let's create an executable that uses libxxx, the project structure would look like this

├── CMakeLists.txt
├── executable.c
├── include
│   └── func.h
├── libxxx   <---- this is the library from above section
│   ├── CMakeLists.txt
│   ├── include
│   │   └── libxxx.h
│   └── libxxx.c
└── src
    └── func.c

Again just create func.h/c with a method to print hello world, for demo purpose.

// func.h
#pragma once
void func_print();

// func.c
#include "func.h"

#include <stdio.h>
void func_print() {
    printf("From func.c: Hello, World!\n");
}

// executable.c
#include "func.h"
#include "libxxx.h"

int main()
{
    func_print();
    libxxx_print();
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.15.0)
project(playground VERSION 0.1.0 LANGUAGES C)

add_subdirectory(libxxx)

add_executable(executable executable.c src/func.c)
target_include_directories(executable PUBLIC "include")
target_link_libraries(executable PUBLIC libxxx)

Let's explore each command here

cmake_minimum_required(VERSION 3.15.0)
project(playground VERSION 0.1.0 LANGUAGES C)

Should be self-explanatory, declare minimum cmake version required, and create a top level project.

add_subdirectory(libxxx)

By default CMake will only look for CMakeLists.txt at the current(root) directory, this tells CMake to also search "libxxx/" folder. So we can use the libxxx library in our project.

add_executable(executable executable.c src/func.c)
target_include_directories(executable PUBLIC "include")
target_link_libraries(executable PUBLIC libxxx)

These are the 3 most common and important commands. It abstracts the 3 most important components of building a C program: 1) source files 2) header files 3) libraries

  1. add_executable
    1. Creates a target of type executable, add source files needed to build this target
  2. target_include_directories
    1. For a specific target, add path to search header files from
  3. target_link_libraries
    1. For a specific target, add libraries to link with. Libraries could be internally created, like libxxx, or external ones, like pthread

Note, there is also a target-less version of these commands: include_directories and link_libraries, but these apply to ALL targets after them, so should generally be avoided.
It's better to include minimal dependencies only for a target that uses them.

That's it. For large projects, there might be a lot more source files/libraries/etc, but the core principle stays the same.