CMake Tutorial: Building C/C++ Projects with Different Generators

For CMake, I had always understood it to be a project build tool, until I encountered --build and realized that CMake also unifies the compilation phase across different platforms. To understand CMake‘s build and compile process, one must first understand generators.

1. Generators

The CMake generator is responsible for writing input files for the underlying build system (e.g., Makefile). Running cmake --help will display the available generators. For cmake 3.26.5, the generators supported by my system include:

Generators

The following generators are available on this platform (* marks default):
  Green Hills MULTI            = Generates Green Hills MULTI files
                                 (experimental, work-in-progress).
* Unix Makefiles               = Generates standard UNIX makefiles.
  Ninja                        = Generates build.ninja files.
  Ninja Multi-Config           = Generates build-<Config>.ninja files.
  Watcom WMake                 = Generates Watcom WMake makefiles.
  CodeBlocks - Ninja           = Generates CodeBlocks project files.
  CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files.
  CodeLite - Ninja             = Generates CodeLite project files.
  CodeLite - Unix Makefiles    = Generates CodeLite project files.
  Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
  Kate - Ninja                 = Generates Kate project files.
  Kate - Unix Makefiles        = Generates Kate project files.
  Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files.
  Sublime Text 2 - Unix Makefiles
                               = Generates Sublime Text 2 project files.

As discussed in this article, CMake includes different types of generators, such as command-line generators, IDE generators, and others.

1. Command-Line Generators

These generators are used for command-line build tools, such as Make and Ninja. Before using CMake to generate a build system, the selected toolchain must be configured.

Supported generators include:

  • Borland Makefiles
  • MSYS Makefiles
  • MinGW Makefiles
  • NMake Makefiles
  • NMake Makefiles JOM
  • Ninja
  • Unix Makefiles
  • Watcom WMake

2. IDE Generators

These generators are used for integrated development environments, which include their own compilers. For example, Visual Studio and Xcode both have their own compilers.

Supported generators include:

  • Visual Studio 6
  • Visual Studio 7
  • Visual Studio 7 .NET 2003
  • Visual Studio 8 2005
  • Visual Studio 9 2008
  • Visual Studio 10 2010
  • Visual Studio 11 2012
  • Visual Studio 12 2013
  • Xcode

3. Other Generators

These generators create configurations and work with other IDE tools and must be included in the IDE or command-line generators.

Supported generators include:

  • CodeBlocks
  • CodeLite
  • Eclipse CDT4
  • KDevelop3
  • Kate
  • Sublime Text 2

To invoke the CMake generator, you can use the -G command-line switch.

2. CMake Build and Compile

When using CMake to build and compile related projects on the Linux platform, the following commands are typically used:

mkdir build
cd build
cmake ..
cmake --build .

Among these, cmake .. and cmake --build . are commonly used commands for building and compiling C/C++ projects.

[Analysis]

  1. cmake ..: Builds the project, using cmake to generate the Makefile file (the main function of cmake) and other files required by the build system

  2. cmake --build .: Compiles and links the project in the current directory

  • Compilation: Syntax analysis, semantic analysis, etc., generates platform-specific assembly code

  • Linking: Symbol resolution, address relocation

[Extension]

  1. First, using the command line: cmake <source tree>, for example: cmake .., generates project files in your build directory (external build method), referred to as project files in the official documentation, also known as build tree or binary tree, which includes Makefile and some other related files/directories/subdirectories.

  2. Next, naturally compile and link the generated project files in the build directory using cmake --build ..

  3. Finally, the . following --build refers to the path of the generated project file’s build tree. Generally, if you clearly know which builder (Build Generator) is used in your system, such as Unix Makefiles, you can directly use make for compilation and linking. This --build format is often used in automated scripts or in IDE environments.

Note: <source tree> refers to the source files + the path where the top-level CMakeLists resides; cmake .. assumes the path is in the previous level.

Of course, there is another way to write the CMake build and compile for C/C++ projects, as follows:

Note: The current directory is the top-level directory where CMakeLists.txt resides

  1. Build the Project
cmake . -Bbuild -G "Unix Makefiles"
  • -B: Specifies the build directory where the generated Makefile or project files will be saved. This directory can be a relative or absolute path. For example: cmake -B build . will create a build directory named build and save the generated Makefile or project files in that directory. You can also use an absolute path to specify the build directory, for example: cmake -B /path/to/build . will create a build directory named build and save the generated Makefile or project files in the /path/to/build directory. In short, the -B option is used to specify the build directory where CMake generated Makefile or project files will be saved. This allows us to separate the source code from the build process for better project management.

  • -G: Refers to Generator, which means specifying the generator. A generator is a build system-specific tool for converting CMake generated Makefile or project files into actual executable files, static libraries, or dynamic libraries. Different build systems have different generators, and each generator can convert CMake generated files into files suitable for a specific build system. For example, the Unix Makefile generator generates Makefile files suitable for Unix/Linux systems, the Ninja generator generates project files suitable for the Ninja build system, and the Visual Studio generator generates project files suitable for Visual Studio IDE, etc. Generators work by reading CMake generated files and converting them into files that the build system can use. In short, a generator is a build system-specific tool for converting CMake generated Makefile or project files into actual executable files, static libraries, or dynamic libraries. Different build systems have different generators, and each generator can convert CMake generated files into files suitable for a specific build system. You can use the -G option to specify the generator, such as Unix Makefiles, Ninja, Visual Studio, etc. For example:

    • cmake -G "Unix Makefiles" .. generates Makefile files suitable for Unix/Linux systems.

    • cmake -G Ninja .. generates project files suitable for the Ninja build system.

    • cmake -G "Visual Studio 16" .. generates project files suitable for Visual Studio 2016.

    • cmake -G "CodeBlocks - Unix Makefiles" .. generates Makefile files suitable for the Code::Blocks IDE.

    • cmake -G "Xcode" .. generates project files suitable for Xcode IDE.

  1. Compile the Project
cmake --build build

The above command compiles and links the project in the build directory; on Linux, the default command used for project compilation is make. But here we use cmake --build build to achieve compilation and linking.

Note: Why do we use cmake --build instead of directly using make? The main reason is for cross-platform compatibility. There is a concept in cmake called cmake generator, which allows cmake to support different underlyings, such as Makefile series, Ninja series, etc. For example, to use Ninja, simply add -G Ninja, i.e., cmake -G Ninja. Without the command cmake --build build, you would need to call the underlying command, such as make or ninja. But now cmake provides a unified command interface, and using this format allows for correct compilation regardless of the underlying system.

3. CMake Build and Compile Practical Demonstration

Let us demonstrate how to use CMake for cross-platform building and compiling of project files through the multi-directory file project example from [CMake Learning Notes] | Modular Project Management (1).

The project structure is as follows:

# Project file directory structure
[root@localhost multi_dir]# tree -L 2
.
├── app
│   └── main.c
├── build
├── CMakeLists.txt
├── hello
│   ├── CMakeLists.txt
│   ├── include
│   └── src
└── world
    ├── CMakeLists.txt
    ├── include
    └── src

8 directories, 4 files

The main CMakeLists.txt file in the top-level directory: CMakeLists.txt.

# CMake minimum required version
cmake_minimum_required(VERSION 3.0)

# Project information, can be anything
project(HelloWorld)

# Set C/C++ version (e.g., c99, c++11, c++17, etc.), below indicates using c99 version
set(CMAKE_C_STANDARD 99)

# Specify directories to add to the compiler's header file search path; the specified directory is interpreted as a relative path to the current source path.
# Of course, absolute paths and custom variables can also be used. By default, the include_directories command adds the directory
# to the end of the list (AFTER option). However, you can change its default behavior by setting the CMAKE_INCLUDE_DIRECTORIES_BEFORE
# variable to ON, which will add the directory to the front of the list. You can also specify whether to add to the front or back of the list
# each time you call the include_directories command using AFTER or BEFORE options.
include_directories(hello/include world/include)

# Set the variable DIR_SRCS, whose value is the source file main.c under app/
set(DIR_SRCS ./app/main.c)

# Add subdirectories hello and world; this way, the CMakeLists.txt files and source code under each subdirectory will also be processed
add_subdirectory(hello)
add_subdirectory(world)

# Specify static library path, ${PROJECT_SOURCE_DIR} represents the path of the main CMakeLists.txt file,
# i.e., the project root directory file path
link_directories(${PROJECT_SOURCE_DIR}/world)
# Link the static library libworld.a generated by the subdirectory; it is generally specified by omitting the prefix (lib) and suffix (.a)
link_libraries(world)

# Specify the generated target
add_executable(HelloWorld ${DIR_SRCS})

# Link the dynamic library libhello.so generated by the subdirectory; it is generally specified by omitting the prefix (lib) and suffix (.so)
target_link_libraries(HelloWorld hello)

The source files are as follows:

Header file in the hello subdirectory: hello/include/hello.h.

#ifndef HELLOWORLD_HELLO_H
#define HELLOWORLD_HELLO_H

extern void hello(void);

#endif //HELLOWORLD_HELLO_H

Source file in the hello subdirectory: hello/src/hello.c.

#include "hello.h"
#include &lt;stdio.h&gt;

void hello()
{
    printf("hello.\n");
}

hello subdirectory CMakeLists.txt file: hello/CMakeLists.txt.

# Add header file path
include_directories(./include)

# Set the variable DIR_SRCS, whose value is the source file hello.c under hello/src/
set(DIR_SRCS ./src/hello.c)

# Generate dynamic link library
add_library(hello SHARED ${DIR_SRCS}) 

Header file in the world subdirectory: world/include/world.h.

#ifndef HELLOWORLD_WORLD_H
#define HELLOWORLD_WORLD_H

extern void world(void);

#endif //HELLOWORLD_WORLD_H

Source file in the world subdirectory: world/src/world.c.

#include "world.h"
#include &lt;stdio.h&gt;

void world()
{
    printf("world.\n");
}

world subdirectory CMakeLists.txt file: world/CMakeLists.txt.

# Add header file path
include_directories(./include)

# Set the variable DIR_SRCS, whose value is the source file world.c under world/src/
set(DIR_SRCS ./src/world.c)

# Generate static link library
add_library(world STATIC ${DIR_SRCS})

Main source file in the app subdirectory: app/main.c.

#include "hello.h"
#include "world.h"

int main()
{
    hello();
    world();

    return 0;
}

Next, let us demonstrate how to use CMake to invoke different underlying Generator for cross-platform building and compiling of different project files.

1. Unix Makefiles

This section demonstrates using the Unix Makefiles generator to generate Makefile files suitable for Unix/Linux systems. Then, through cmake --build build, we invoke the underlying command make to compile and link the project. The example is as follows:

[root@localhost multi_dir]# ls
app  CMakeLists.txt  hello  world

# cmake uses Unix Makefiles to build the project
[root@localhost multi_dir]# cmake . -Bbuild -G "Unix Makefiles"
-- The C compiler identification is GNU 8.5.0
-- The CXX compiler identification is GNU 8.5.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.4s)
-- Generating done (0.0s)
-- Build files have been written to: /backup/cmake/multi_dir/build

[root@localhost multi_dir]# ls
app  build  CMakeLists.txt  hello  world
[root@localhost multi_dir]# ls build/
CMakeCache.txt  CMakeFiles  cmake_install.cmake  hello  Makefile  world

# cmake uses make to compile the project, generating the executable file HelloWorld
[root@localhost multi_dir]# cmake --build build
[ 16%] Building C object world/CMakeFiles/world.dir/src/world.c.o
[ 33%] Linking C static library libworld.a
[ 33%] Built target world
[ 50%] Building C object hello/CMakeFiles/hello.dir/src/hello.c.o
[ 66%] Linking C shared library libhello.so
[ 66%] Built target hello
[ 83%] Building C object CMakeFiles/HelloWorld.dir/app/main.c.o
[100%] Linking C executable HelloWorld
[100%] Built target HelloWorld

[root@localhost multi_dir]# ls
app  build  CMakeLists.txt  hello  world
[root@localhost multi_dir]#
[root@localhost multi_dir]# ls build/
CMakeCache.txt  CMakeFiles  cmake_install.cmake  hello  HelloWorld  Makefile  world

[root@localhost multi_dir]# ./build/HelloWorld
hello.
world.

2. Ninja

This section demonstrates using the Ninja generator to generate Makefile files suitable for the Ninja build system. Then, through cmake --build build, we invoke the underlying command ninja to compile and link the project.

Note: The prerequisite for cmake to use the Ninja generator is that the ninja executable must be successfully installed on the machine. The installation method can be done via the command line or by compiling from source; specific installation methods can be searched online.

The example is as follows:

# cmake uses Ninja to build the project
[root@localhost multi_dir]# cmake . -Bbuild -G  Ninja
-- The C compiler identification is GNU 8.5.0
-- The CXX compiler identification is GNU 8.5.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.4s)
-- Generating done (0.0s)
-- Build files have been written to: /backup/cmake/multi_dir/build

[root@localhost multi_dir]# ls
app  build  CMakeLists.txt  hello  world
[root@localhost multi_dir]#
[root@localhost multi_dir]# ls build/
build.ninja  CMakeCache.txt  CMakeFiles  cmake_install.cmake  hello  world

# cmake uses ninja to compile the project, generating the executable file HelloWorld
[root@localhost multi_dir]# cmake --build build
[6/6] Linking C executable HelloWorld

[root@localhost multi_dir]# ls
app  build  CMakeLists.txt  hello  world
[root@localhost multi_dir]#
[root@localhost multi_dir]# ls build/
build.ninja  CMakeCache.txt  CMakeFiles  cmake_install.cmake  hello  HelloWorld  world

[root@localhost multi_dir]# ./build/HelloWorld
hello.
world.

Leave a Comment