0%

Build using CMake on Windows

Building is a fairly general term without strict definition; It usually refers to the whole process that outputing the final product (an executable or a library) from source materials. Depending on the requirements, building will involve several of the following: pre-processing, compiling, linking, converting data files, automatically testing, packaging.

Among the tools that defining the building behaviors for Platform Dependent products whose programming language usually be C or C++, CMake is the most popular one as it is open-source and cross-platform. The building process with CMake takes place in two stages:

  1. Given abstract, platform and compiler independent building procedures defined by developers, generating standard Makefile or Project Files for IDEs (Visual Studio, Xcode etc.) .
  2. Invoke the desired native build tool to undertake the actual building process.

Here we introduce the usage of CMake. The environment of this tutorial is Windows 10; The output of each command will be different from that running on Linux. For the syntax of CMake Language, you may visit CMake Syntax for details.

Hello World

The following is a good starting point for learning about CMake.

  1. Create a new folder test.
  2. Under the directory, create the source file main.cpp.
    1
    2
    3
    4
    5
    #include <stdio.h>
    int main(){
    printf("Hello World from test Main!\n");
    return 0;
    }
  3. Create the CMake file named exactly CMakeLists.txt
    1
    2
    3
    cmake_minimum_required(VERSION 3.10)
    project(Main)
    add_executable(Main main.cpp)
  4. Run cmake . to generate native project files. Under Windows, CMake will generate a Visual Studio project by default. When finished, lots of contents created in the directory:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    │  ALL_BUILD.vcxproj
    │ ALL_BUILD.vcxproj.filters
    │ CMakeCache.txt
    │ CMakeLists.txt
    │ cmake_install.cmake
    │ main.cpp
    │ Main.sln
    │ Main.vcxproj
    │ Main.vcxproj.filters
    │ ZERO_CHECK.vcxproj
    │ ZERO_CHECK.vcxproj.filters
    └─CMakeFiles/
  5. Run cmake --build . to create executable. You could find the Main.exe in <root-dir>/Debug.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    │  ALL_BUILD.vcxproj
    │ ALL_BUILD.vcxproj.filters
    │ CMakeCache.txt
    │ CMakeLists.txt
    │ cmake_install.cmake
    │ main.cpp
    │ Main.sln
    │ Main.vcxproj
    │ Main.vcxproj.filters
    │ ZERO_CHECK.vcxproj
    │ ZERO_CHECK.vcxproj.filters
    ├─CMakeFiles/
    ├─Debug/
    │ Main.exe
    │ Main.ilk
    │ Main.pdb
    ├─Main.dir/
    └─x64/
  6. Run executable via Debug\Main.exe
    1
    Hello World from test Main!

Explanation

Generate a Project Buildsystem

To generate a buildsystem with CMake, the following must be defined:

  • Source Tree: The top-level directory containing source files provided by the project. Then the generation will start with CMakeLists.txt under this directory.
  • Build Tree: The top-level directory where buildsystem files and output artifacts being placed. CMake will also create a CMakeCache.txt here to store persistent information.
  • Generator: The type of buildsystem to generate. If not specified, CMake will choose the proper one automatically. When using one of the Command-Line Build Tool Generators CMake expects that the environment needed by the compiler toolchain is already configured in the shell. When using one of the IDE Build Tool Generators, no particular environment is needed.

You could run CMake with one of the following command signatures to specify them.

  1. cmake [<options>] <path-to-source>: Then the current directory is the build tree, <path-to-source> is the source tree. Both absolute an relative path is valid. The source tree must contain a CMakeLists.txt file and must not contain a CMakeCache.txt file.
  2. cmake [<options>] <path-to-existing-build>: Then <path-to-existing-build> is the build tree which must contain a CMakeCache.txt file because CMake will load the source tree from it.
  3. cmake [<options>] -S <path-to-source> -B <path-to-build>. Specify both <path-to-build> and <path-to-source>. The source tree must contain a CMakeLists.txt file; The build tree will be created automatically if it does not already exist.

Options

For the full list of options please visit https://cmake.org/cmake/help/latest/manual/cmake.1.html#options or cmake --help

  • -C <initial-cache>: Pre-load a script that contains a list of set() commands to initialize the cache values. The loaded entries take priority over the project’s default values.
  • -D <var>:<type>=<value> or -D <var>=<value>: Create or update a CMake CACHE entry.
  • -G <generator-name> Specify a build system generator. Run cmake --help to get the name of supported generators
  • --log-level=<ERROR|WARNING|NOTICE|STATUS|VERBOSE|DEBUG|TRACE>: Set the log level. The message() command will only output messages of the specified log level or higher. The default log level is STATUS.

Build a Project

Use cmake --build <dir> [<options>] [-- <build-tool-options>] to build an already-generated project binary tree.

  • --build <dir> The binary directory when building.
  • --parallel [<jobs>], -j [<jobs>]: Specify the maximum number of concurrent processes. If <jobs> is omitted, use the default number.
  • --target <tgt>..., -t <tgt>...: Build specific <tgt>s .
  • --config <cfg>: For multi-configuration tools, choose specific <cfg>.
  • --clean-first: Clean existing built target and re-build it.
  • -target clean: Clean existing built target only.

Output message

message([SEND_ERROR|STATUS|FATAL_ERROR|DEBUG|TRACE] "message text" ...)

CMake displays STATUS to TRACE messages on stdout with prefix --; All other message types are sent to stderr. FATAL_ERROR will terminate the process immediately whereas CMake Error stops generation only but continues processing.

Set Project Name

project(<PROJECT-NAME> [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]])

This command will set the name of the project and store it in PROJECT_NAME. When called from the top-level CMakeLists.txt, it will also store the project name in CMAKE_PROJECT_NAME. Simultaneously, the variables PROJECT_SOURCE_DIR, <PROJECT-NAME>_SOURCE_DIR, PROJECT_BINARY_DIR, <PROJECT-NAME>_BINARY_DIR will be defined according to the absolute path of the corresponding directory.

Add Executable

add_executable(<name> [source1] [source2 ...])

This command will add an executable target called <name> to be built from the source files listed in the command invocation; The source files can also be added later using target_sources(). The <name> must be globally unique within a project. By default the executable will be created in the build tree directory with the name <name> or <name>.exe depending on the native platform.

In-Source and Out-of-Source Build

Some build trees created with GNU autotools have a make distclean command that removes Makefiles and others belonging to the generated build system. However, CMake has no way to track exactly which files are generated by itself. Therefore, it's recommended to adopt the out-of-source build ---- placing the build tree separately from the source tree. Then one can clean the build by clear or delete the build tree without affect the original source files.

A Better Hello World

Reorganize the Project Directory

  1. Create src/ to place source files
  2. Create CMakeLists.txt under src/
  3. Create build/ to place buildsystem

After that, the structure of our project will be

1
2
3
4
5
6
7
|   CMakeLists.txt
|
+---build
| \---bin
\---src
CMakeLists.txt
main.cpp

Config Source and Binary Directory

1
add_subdirectory(source_dir [binary_dir])

This command will add source_dir run its CMakeLists.txt; A relative path will be evaluated with respect to the current directory. The binary_dir specifies the directory in which to place the output files. Both relative path and absolute path are valid; A relative path it will be evaluated with respect to the current output directory. If binary_dir is not specified, the value of source_dir before expanding will be used.

In top-level CMakeLists.txt become:

1
2
3
cmake_minimum_required(VERSION 3.10)
project(Main)
add_subdirectory(src/ main)

Config Source Directory

It's tedious to list all source files manually, regardless of using

1
add_executable(project source1.c source2.c)

or

1
2
set(DIR source1.c source2.c)
add_executable(project ${DIR})

aux_source_directory(<dir> <variable>) will collects the names of all the source files in <dir> and stores the list in the <variable>. Note that there is no way for the build system that knows when a new source file has been added; When a new source is just added to the directory, one would have to manually rerun CMake to generate a build system incorporating the new file.

In CMakeLists.txt of src/, add

1
2
aux_source_directory(. SRC_DIR)
add_executable(Main ${SRC_DIR})

Config Binary Directory

In CMakeLists.txt of src/, add

1
2
3
aux_source_directory(. SRC_DIR)
add_executable(Main ${SRC_DIR})
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

Specify the C++ Standard

In top-level CMakeLists.txt, add

1
2
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

Add Configured Header File

Add a Version Number

Add the version number in project.

1
project(Main VERSION 1.0)

Then the variables PROJECT_VERSION, <PROJECT-NAME>_VERSION, PROJECT_VERSION_MAJOR, <PROJECT-NAME>_VERSION_MAJOR, PROJECT_VERSION_MINOR, <PROJECT-NAME>_VERSION_MINOR, PROJECT_VERSION_PATCH, <PROJECT-NAME>_VERSION_PATCH, PROJECT_VERSION_TWEAK, <PROJECT-NAME>_VERSION_TWEAK will be defined accordingly.

Add a Configured Header File

It's also valid to specify the version number directly in the source code,; Using CMakeLists.txt provides more flexibility. Under src/, create a new file config.h.in with the following content:

1
2
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@

configure_file(<input> <output>) will copy the <input> to an <output> file with the evaulated values referenced as @VAR@ or ${VAR}. Each variable reference will be replaced with the current value of the variable, or the empty string if the variable is not defined.

The configured file will be written into the ${CMAKE_CURRENT_BINARY_DIR}; We must add that directory to the list of paths to search for include files. In src/CMakeLists.txt, add

1
2
configure_file(config.h.in config.h)
target_include_directories(Main PUBLIC ${PROJECT_BINARY_DIR})

target_include_directories(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...]) [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...]) specifies include directories to when compiling a given target. The named <target> must have been defined by such as add_executable() or add_library().

Finally, in main.cpp, let's include the header file and print the version number.

1
2
3
4
5
6
7
#include <stdio.h>
#include <config.h>
int main(){
printf("Hello World from test Main!\n");
printf("The Major Version is %s and the Minor Version is %s\n" , PROJECT_VERSION_MAJOR, PROJECT_VERSION_MINOR);
return 0;
}

A Complex Hello World

Add Math Library

Create math/ under src/, add CMakeLists.txt, MathFunctions.cpp with MathFunctions.h

MathFunctions.h

1
extern double power(double base, int exponent);

MathFunctions.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "MathFunctions.h"
double power(double base, int exponent)
{
int result = base;
int i;
if (exponent == 0) {
return 1;
}
for(i = 1; i < exponent; ++i){
result = result * base;
}
return result;
}

src/math/CMakeLists.txt

1
aux_source_directory(. DIR_LIB_SRCS)

After that, the structure will be

1
2
3
4
5
6
7
8
9
10
11
12
|   CMakeLists.txt
|
+---build
\---src
| CMakeLists.txt
| config.h.in
| main.cpp
|
\---math
CMakeLists.txt
MathFunctions.cpp
MathFunctions.h

Build and Use Static Library

In src/math/CMakeLists.txt, add

1
2
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
add_library (MathFunctions ${DIR_LIB_SRCS})

add_library(<name> [STATIC | SHARED] [source1] [source2 ...]) will add a library target called <name>.

The top-level CMakeLists.txt become:

1
2
3
4
cmake_minimum_required(VERSION 3.10)
project(Main VERSION 1.0)
add_subdirectory(src/math math) #process math first
add_subdirectory(src/ main)

The src/CMakeLists.txt become:

1
2
3
4
5
6
7
8
aux_source_directory(. SRC_DIR)
add_executable(Main ${SRC_DIR})

configure_file(config.h.in config.h)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

target_link_libraries(Main PUBLIC MathFunctions) # link the library
target_include_directories(Main PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/math) # include the directory of `MathFunctions.h`

The structure become

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|   CMakeLists.txt
| compile.bat
| README.txt
|
+---build
| +---bin
| | \---Debug
| | Main.exe
| | Main.ilk
| | Main.pdb
| |
| \---lib
| \---Debug
| MathFunctions.lib
| MathFunctions.pdb
|
\---src
| CMakeLists.txt
| config.h.in
| main.cpp
|
\---math
CMakeLists.txt
MathFunctions.cpp
MathFunctions.h

Build and use Dynamic Library

src/math/CMakeLists.txt become

1
2
3
aux_source_directory(. DIR_LIB_SRCS)
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
add_library (MathFunctions SHARED ${DIR_LIB_SRCS}) # to build a shared library

MathFunctions.h become

1
extern  __declspec( dllexport )  double power(double base, int exponent);//

src/CMakeLists.txt become:

1
2
3
4
5
6
7
8
9
aux_source_directory(. SRC_DIR)
add_executable(Main ${SRC_DIR})

configure_file(config.h.in config.h)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

link_directories(${PROJECT_SOURCE_DIR}/lib/Debug) # link the directory of .lib
target_link_libraries(Main PUBLIC MathFunctions)
target_include_directories(Main PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/math)

Then the structrue become

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|   CMakeLists.txt
|
+---build
| +---bin
| | \---Debug
| | Main.exe
| | Main.ilk
| | Main.pdb
| |
| \---lib
| \---Debug
| MathFunctions.dll
| MathFunctions.exp
| MathFunctions.ilk
| MathFunctions.lib
| MathFunctions.pdb
|
\---src
| CMakeLists.txt
| config.h.in
| main.cpp
|
\---math
CMakeLists.txt
MathFunctions.cpp
MathFunctions.h

When running with dynamic-linked library, you should put the .dlls under the same folder of .exe.

https://cmake.org/cmake/help/latest/guide/tutorial/ https://cliutils.gitlab.io/modern-cmake/