A Complete Guide to Using CMake

Scan to FollowLearn Embedded Together, Learn Together, Grow Together

A Complete Guide to Using CMake

Hello everyone, I previously shared some articles related to makefiles:

Make Command Tutorial | A Hands-On Guide to Makefile
Linux Kernel Makefile Execution Process

Some General Makefile Templates

Today, I am sharing an article about CMake, which can generate local Makefiles without having to write complex Makefiles.

Introduction

CMake is an open-source, cross-platform build tool that allows us to generate local Makefiles by writing simple configuration files. These configuration files are independent of the runtime platform and compiler, so we don’t have to write Makefiles ourselves, and they can be used on other platforms without modification, which is very convenient.

This article mainly discusses how to use CMake to compile our programs on Linux.

1. Environment Setup

I am using Ubuntu 18.04, and to install CMake, use the following command:

sudo apt install cmake

After installation, enter the following in the terminal:

cmake -version

to check the CMake version.

A Complete Guide to Using CMake

Now CMake is installed.

2. Getting Started

Let’s start with the simplest code to experience how CMake works.

2.1 Project Structure

A Complete Guide to Using CMake

2.2 Example Source Code

Open the terminal and enter:

touch main.c CMakeLists.txt

Write the following in main.c:

#include <stdio.h>

int main(void)
{
	printf("Hello World\n");
	return 0;
}

Then, in the same directory as main.c, write the following in CMakeLists.txt:

cmake_minimum_required (VERSION 2.8)

project (demo)

add_executable(main main.c)

2.3 Running and Checking

Change to the directory where main.c is located in the terminal, then enter the following command to run CMake:

cmake .

The output will be as follows:

A Complete Guide to Using CMake

Note: At this point, pay attention to the files generated in this folder.

A Complete Guide to Using CMake

We can see that a Makefile was successfully generated, along with some files automatically generated by CMake during runtime.

Then enter make in the terminal:

A Complete Guide to Using CMake

You can see the progress of the Makefile generated by CMake displayed with color. Now let’s check the files in the directory:

A Complete Guide to Using CMake

We can see that the executable file main has been successfully generated!

Now run main:

A Complete Guide to Using CMake

Run successful!

PS: If you want to regenerate main, just enter make clean to delete the main file. Then just make again. Please note: I hope you pay special attention to the files generated in this folder at this time.

3. Compiling Multiple Source Files

3.1 Having Multiple Source Files in the Same Directory

3.1.1 Simple Version

Now let’s move on to a slightly more complex example: having multiple source files in the same directory.

3.1.1.1 Project Structure

A Complete Guide to Using CMake

3.1.1.2 Example Code

First, delete the previous files:

rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile main

Add two files, testFunc.c and testFunc.h, in the previous directory:

touch testFunc.c testFunc.h

After adding, the overall file structure is as follows:

A Complete Guide to Using CMake

testFunc.c

/*
** testFunc.c
*/

#include <stdio.h>
#include "testFunc.h"

void func(int data)
{
	printf("data is %d\n", data);
}

testFunc.h

/*
** testFunc.h
*/

#ifndef _TEST_FUNC_H_
#define _TEST_FUNC_H_

void func(int data);

#endif

Modify main.c to call the function func() declared in testFunc.h:

main.c

#include <stdio.h>

#include "testFunc.h"

int main(void)
{
	func(100);
	return 0;
}

Modify CMakeLists.txt to add testFunc.c in the parameters of add_executable:

CMakeLists.txt

cmake_minimum_required (VERSION 2.8)

project (demo)

add_executable(main main.c testFunc.c)

3.1.1.3 Running and Checking

cmake .
make

A Complete Guide to Using CMake

Then run and check:

A Complete Guide to Using CMake

Run successful!

It can be inferred that if there are multiple source files in the same directory, just add all the source files in add_executable.

However, if there are a hundred source files, doing it this way can be a bit cumbersome and does not reflect the advantages of CMake. Therefore, CMake provides a command to store all source files in a specified directory in a variable, and this command is:

aux_source_directory(dir var)

The first parameter dir is the specified directory, and the second parameter var is the variable used to store the source file list.

Next, let’s write an advanced version of the demo using this variable.

3.1.2 Advanced Version

3.1.2.1 Project Structure

A Complete Guide to Using CMake

3.1.2.2 Example Code

Delete unrelated files

rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake  main Makefile

Create files:

touch testFunc1.c testFunc1.h

testFunc1.c

/*
** testFunc1.c
*/

#include <stdio.h>
#include "testFunc1.h"

void func1(int data)
{
	printf("data is %d\n", data);
}

testFunc1.h

/*
** testFunc1.h
*/

#ifndef _TEST_FUNC1_H_
#define _TEST_FUNC1_H_

void func1(int data);

#endif

Next, modify main.c to call the function func1() declared in testFunc1.h:

main.c

#include <stdio.h>

#include "testFunc.h"
#include "testFunc1.h"

int main(void)
{
	func(100);
	func1(200);

	return 0;
}

Modify CMakeLists.txt:

CMakeLists.txt

cmake_minimum_required (VERSION 2.8)

project (demo)

aux_source_directory(. SRC_LIST)

add_executable(main ${SRC_LIST})

Use aux_source_directory to store the source files in the current directory into the variable SRC_LIST;

Then call SRC_LIST in add_executable (note the syntax when calling the variable).

3.1.2.3 Running and Checking

Run cmake and make again, and run main:

A Complete Guide to Using CMake

It can be seen that it ran successfully.

aux_source_directory() also has drawbacks; it will add all source files in the specified directory, which may include some files we don’t need. In this case, we can use the set command to create a variable to store the required source files, as follows:

cmake_minimum_required (VERSION 2.8)

project (demo)

set( SRC_LIST
	 ./main.c
	 ./testFunc1.c
	 ./testFunc.c)

add_executable(main ${SRC_LIST})

3.2 Having Multiple Source Files in Different Directories

Generally speaking, when there are many program files, we will manage them categorically, placing code in different directories based on functionality for easier searching. So how do we write CMakeLists.txt in this case?

3.2.1 Project Structure

We will organize the previous source files (create two new directories test_func and test_func1):

rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake  main Makefile

After organizing, the overall file structure is as follows:

A Complete Guide to Using CMake

Move the previous testFunc.c and testFunc.h into the test_func directory, and testFunc1.c and testFunc1.h into the test_func1 directory.

3.2.2 Example Code

Here, CMakeLists.txt and main.c are in the same directory, and the content is modified as follows:

cmake_minimum_required (VERSION 2.8)

project (demo)

include_directories (test_func test_func1)

aux_source_directory (test_func SRC_LIST)
aux_source_directory (test_func1 SRC_LIST1)

add_executable (main main.c ${SRC_LIST} ${SRC_LIST1})

A new command appears here: include_directories.

This command is used to add multiple specified header file search paths to the project, with paths separated by spaces.

Since main.c includes testFunc.h and testFunc1.h, if this command is not used to specify the location of the header files, it will not compile. Of course, you can also specify the path in main.c like this:

#include "test_func/testFunc.h"
#include "test_func1/testFunc1.h"

This way of writing is not very elegant. Additionally, we used aux_source_directory twice because the source files are distributed in two directories, so we added it twice.

3.2.2 Running and Checking

A Complete Guide to Using CMake

4. Project-Level Organizational Structure

Generally speaking, we usually place source files in the src directory, header files in the include directory, generated object files in the build directory, and the final output executable program files in the bin directory, making the entire structure clearer.

4.1 Project Structure

Let’s reorganize the previous files:

A Complete Guide to Using CMake

4.2 Example Code

Modify CMakeLists.txt:

CMakeLists.txt

cmake_minimum_required (VERSION 2.8)

project (demo)

add_subdirectory (src)

add_subdirectory: This statement adds a subdirectory for compilation. Its basic syntax is:

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

There are three parameters in total, with the latter two being optional.

  • source_dir: Source code directory

Specifies a directory containing CMakeLists.txt and code files, which can be an absolute or relative path. For the latter, the starting point of the relative path is CMAKE_CURRENT_SOURCE_DIR. Additionally, if the subdirectory contains its own CMakeLists.txt, it will continue processing the inner CMakeLists.txt rather than continuing with the current source code.

  • binary_dir: Binary code directory

This directory is optional; if specified, the output files after executing the cmake command will be stored here. If not specified, the default is the path of source_dir before relative path calculation, which is CMAKE_BINARY_DIR.

  • EXCLUDE_FROM_ALL flag

This flag is optional; if this parameter is passed, the newly added subdirectory will be excluded from the ALL directory (possibly from make all in the make system?), indicating that this directory will be excluded from the IDE project. Users must explicitly compile this target in the subdirectory (manually cmake, etc.). Specifying this folder indicates that it is independent of the source project; these functions are useful but not necessary, such as a series of examples.

add_subdirectory is used to add source file subdirectories, and it can also specify intermediate binary and target binary generation paths. EXCLUDE_FROM_ALL will exclude this directory from compilation, as examples in the project need to wait for other compilations to complete before being compiled separately. Typically, subdirectories should contain their own project() command so that the entire compilation command generates its own target files. If we compare CMakeLists.txt with VS IDE, the top-level CMakeLists.txt is equivalent to a solution, while the sub-CMakeLists.txt is equivalent to a project file under that solution. Another point to note is that if the parent CMakeLists depends on source files in the sub-CMakeLists when compiling, this flag will be overridden (i.e., it will also be processed) to satisfy the compilation task.

Here, we specify that the source files are stored in the src directory. When executing cmake, it will go into the src directory to find the CMakeLists.txt there, so also create a CMakeLists.txt in the src directory with the following content:

src/CMakeLists.txt

aux_source_directory (. SRC_LIST)

include_directories (../include)

add_executable (main ${SRC_LIST})

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

Here, set is actually the same as before; EXECUTABLE_OUTPUT_PATH is a system-defined predefined variable with the following meaning:

  • EXECUTABLE_OUTPUT_PATH: The storage location of the target binary executable file

  • PROJECT_SOURCE_DIR: The root directory of the project

So here, the set means to set the location for storing ELF files to the bin directory under the project root directory. (CMake has many predefined variables; detailed information can be searched online)

After adding these two CMakeLists.txt files, the overall file structure is as follows:

A Complete Guide to Using CMake

4.3 Running and Checking

Now let’s run cmake, but this time let’s change to the build directory first:

cd build
cmake ..
make

A Complete Guide to Using CMake

This way, the Makefile will be generated in the build directory, and the binary program will be generated in the bin directory. Then run the executable program:

cd ../bin
./main

A Complete Guide to Using CMake

Here, let me explain why we run cmake in the build directory. Remember in the first example I asked you to pay special attention to what files are generated after running cmake and make? Many files will be generated in this process, but unfortunately, they have nothing to do with our execution. Therefore, if we can isolate the compilation in a specific folder, all intermediate files generated during this cmake process will be created in that directory, making it very convenient to delete them later. If we do not do this, the auxiliary files generated during the cmake run will be mixed with the source files, which will pollute the directory structure of the program. Another way of writing: the previous project used two CMakeLists.txt files; the outer CMakeLists.txt is used to control the global settings, using add_subdirectory to control the execution of other CMakeLists.txt in different directories.

The above example can also use only one CMakeLists.txt; just change the content of the outer CMakeLists.txt to the following:

CMakeLists.txt

cmake_minimum_required (VERSION 2.8)

project (demo)

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

aux_source_directory (src SRC_LIST)

include_directories (include)

add_executable (main ${SRC_LIST})

At the same time, the CMakeLists.txt in the src directory should be deleted.

Then you can compile and run normally.

5. Compilation Control of Dynamic and Static Libraries

Sometimes we only need to compile dynamic and static libraries and wait for other programs to use them. Let’s see how to use CMake in this situation.

5.1 Generating Library Files

5.1.1 Project Structure

First, reorganize the files as follows, keeping only testFunc.h and TestFunc.c:

A Complete Guide to Using CMake

We will run cmake in the build directory and store the generated library files in the lib directory.

5.1.2 Example Code

CMakeLists.txt

cmake_minimum_required (VERSION 3.5)

project (demo)

set (SRC_LIST ${PROJECT_SOURCE_DIR}/testFunc/testFunc.c)

add_library (testFunc_shared SHARED ${SRC_LIST})
add_library (testFunc_static STATIC ${SRC_LIST})

set_target_properties (testFunc_shared PROPERTIES OUTPUT_NAME "testFunc")
set_target_properties (testFunc_static PROPERTIES OUTPUT_NAME "testFunc")

set (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

Here, new commands and predefined variables appear:

  • add_library: Generates dynamic or static libraries (the first parameter specifies the library name; the second parameter determines whether it is dynamic or static, defaulting to static if not specified; the third parameter specifies the source files for generating the library)

  • set_target_properties: Sets the final generated library name and other features, such as setting library version numbers, etc.

  • LIBRARY_OUTPUT_PATH: The default output path for library files, set here to the lib directory under the project directory

PS: Previously, we used set_target_properties to redefine the output name of the library. If set_target_properties is not used, the library name will be the name defined in add_library; however, when specifying the library name using add_library twice (the first parameter), the names cannot be the same. Using set_target_properties allows the name to be the same, but the final generated library file will have different suffixes (one will be .so, the other will be .a), which looks better in comparison.

5.1.3 Running and Checking

cd build/
cmake ..
make
cd ../lib/
ls

A Complete Guide to Using CMake

5.2 Linking Library Files

Since we have generated the library, let’s perform a linking test.

5.2.1 Project Structure

Recreate a project directory, copy the generated library over, and create src and bin directories in the project directory. Add a main.c file in the src directory, resulting in the following structure:

A Complete Guide to Using CMake

5.2.2 Example Code

main.c

#include <stdio.h>

#include "testFunc.h"

int main(void)
{
	func(100);
	
	return 0;
}

CMakeLists.txt

cmake_minimum_required (VERSION 3.5)

project (demo)

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

set (SRC_LIST ${PROJECT_SOURCE_DIR}/src/main.c)

# find testFunc.h
include_directories (${PROJECT_SOURCE_DIR}/testFunc/inc)

find_library(TESTFUNC_LIB testFunc HINTS ${PROJECT_SOURCE_DIR}/testFunc/lib)

add_executable (main ${SRC_LIST})

target_link_libraries (main ${TESTFUNC_LIB})

Two new commands appear here:

  • find_library: Searches for the specified library in the specified directory and stores the absolute path of the library in a variable. The first parameter is the variable name, the second parameter is the library name, and the third parameter is HINTS, with other usages available in the CMake documentation.

  • target_link_libraries: Links the target file with the library file

The benefit of using find_library is that it will check whether the library exists during the execution of cmake…, allowing for early error detection instead of waiting until the linking stage.

5.2.3 Running and Checking

Change to the build directory, then run cmake… && make. Finally, go to the bin directory and check that main has been generated, then run it:

cd build/
cmake ..
make
cd ../bin/
./main

A Complete Guide to Using CMake

A Complete Guide to Using CMake

PS: In the lib directory, there are static and dynamic libraries of testFunc. find_library(TESTFUNC_LIB testFunc… defaults to searching for dynamic libraries. If you want to specify directly whether to use a dynamic or static library, you can write find_library(TESTFUNC_LIB libtestFunc.so… or find_library(TESTFUNC_LIB libtestFunc.a…

PS: To check which libraries the ELF file uses, you can use readelf -d ./xx, for example: readelf -d ./main

6. Conditional Compilation

Sometimes when compiling a program, you want to add some compilation options, such as -Wall, -std=c++11, etc., which can be done using add_compile_options. Here’s a simple program to demonstrate.

6.1 Simple Program

6.1.1 Project Structure

A Complete Guide to Using CMake

6.1.2 Example Code

main.cpp

#include <iostream>

int main(void)
{
	auto data = 100;
	std::cout << "data: " << data << "\n";
	return 0;
}

CMakeLists.txt

cmake_minimum_required (VERSION 2.8)

project (demo)

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

add_compile_options(-std=c++11 -Wall) 

add_executable(main main.cpp)

6.1.3 Running and Checking

Then cd to the build directory and execute cmake… && make to obtain the executable file main in the bin directory.

A Complete Guide to Using CMake

6.2 Adding Compilation Options

Sometimes you may wish to compile only specific source files during compilation. You can use the option command in CMake. The main situations encountered can be divided into two types:

  1. Originally generating multiple bin or library files, now only wanting to generate a portion of specified bin or library files

  2. For the same bin file, only wanting to compile part of the code (using macros to control)

6.2.1 Generating Partially Specified Bin or Library Files

6.2.1.1 Project Structure

Suppose our project will generate two bin files, main1 and main2, with the following project structure:

A Complete Guide to Using CMake

6.2.1.2 Example Code

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(demo)

option(MYDEBUG "enable debug compilation" OFF)

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

add_subdirectory(src)

Here, the option command is used. The first parameter is the name of this option, the second parameter is a string describing what this option does, and the third is the value of the option, ON or OFF. If not written, it defaults to OFF.

Then write the CMakeLists.txt in the src directory as follows:

src/CMakeLists.txt

cmake_minimum_required (VERSION 3.5)

add_executable(main1 main1.c)

if (MYDEBUG)
    add_executable(main2 main2.c)
else()
    message(STATUS "Currently is not in debug mode")    
endif()

Note that here we use if-else to decide whether to compile main2.c based on the option, where the contents of main1.c and main2.c are as follows:

main1.c

// main1.c
#include <stdio.h>

int main(void)
{
	printf("hello, this main1\n");
	
	return 0;
}

main2.c

// main2.c
#include <stdio.h>

int main(void)
{
	printf("hello, this main2\n");
	
	return 0;
}

6.2.1.3 Running and Checking

Then cd to the build directory and enter cmake… && make to compile only main1. If you want to compile main2, set MYDEBUG to ON and re-enter cmake… && make to recompile.

Each time you want to change MYDEBUG, you need to modify CMakeLists.txt, which can be a bit cumbersome. In fact, you can operate through the command line of cmake, for example, if we want to set MYDEBUG to OFF, first cd to the build directory and then enter cmake… -DMYDEBUG=ON, then you can compile both main1 and main2 (in the bin directory).

6.2.2 Compiling Part of the Code

Suppose we have a main.c file with the following content:

main.c

#include <stdio.h>

int main(void)
{
#ifdef WWW1
	printf("hello world1\n");
#endif    

#ifdef WWW2     
	printf("hello world2\n");
#endif

	return 0;
}

You can control the printed information through macro definitions. The content of our CMakeLists.txt is as follows:

cmake_minimum_required(VERSION 3.5)

project(demo)

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

option(WWW1 "print one message" OFF)
option(WWW2 "print another message" OFF)

if (WWW1)
    add_definitions(-DWWW1)
endif()

if (WWW2)
    add_definitions(-DWWW2)
endif()

add_executable(main main.c)

Here, the names of the options are kept consistent with the macro names in main.c, making it more intuitive, though different names can also be chosen. By combining with add_definitions(), you can control the output of a single bin file.

The overall project structure is as follows:

A Complete Guide to Using CMake

cd to the build directory, execute cmake… && make, and then execute ./main in the bin directory, you will find the print output is empty.

Next, execute the following commands to see the printing effects:

  • cmake… -DWWW1=ON -DWWW2=OFF && make

  • cmake… -DWWW1=OFF -DWWW2=ON && make

  • cmake… -DWWW1=ON -DWWW2=ON && make

There is a small pitfall to note: suppose there are two options named A and B. If you first call cmake to set A, and then call cmake to set B without deleting the cache file generated from the previous cmake execution, this time, even though B is not set, it will still default to the last value of A.

So if the options change, you either need to delete the cache files generated from the previous cmake execution or explicitly specify the values for all options.

Conclusion

The above is a record of my learning CMake, providing a simple example to help everyone get started with CMake. While learning, I also read many blogs from other netizens. There are many more knowledge points related to CMake; specific details can be searched online.

In short, CMake allows us to avoid writing complex Makefiles and is cross-platform, making it a powerful tool worth learning.

This article is sourced from the internet, and the copyright belongs to the original author. If there is any infringement, please contact us for deletion.

A Complete Guide to Using CMake

Scan to join the high-quality embedded group chat

A Complete Guide to Using CMake

Follow me [Learn Embedded Together], learn together, grow together.

If you think this article is good, click “Share“, “Like“, or “View“!

Leave a Comment