Getting Started with CMake - 2 - Manage large project
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:
- Source file:
executable.c
func.c
- Header file:
func.h
- Static lib and header file from same project:
libxxx.a
xxx.h
- 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 buildinglibxxx
, if we are buildinglibyyy
, 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
add_executable
- Creates a target of type executable, add source files needed to build this target
target_include_directories
- For a specific target, add path to search header files from
target_link_libraries
- For a specific target, add libraries to link with. Libraries could be internally created, like
libxxx
, or external ones, likepthread
- For a specific target, add libraries to link with. Libraries could be internally created, like
Note, there is also a target-less version of these commands:
include_directories
andlink_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.