CMake是一个类似于QMake的构建工具,你可以理解成它是用来生成makefile的工具。具体的介绍请看官方文档

测试环境:Windows 10, Microsoft Visual Studio 2019

所有测试代码在这里

Step 1: Hello CMake

我们从一个最简单的例子入手。

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.10)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# add the executable
add_executable(Tutorial tutorial.cxx)

tutorial.cxx

1
2
3
4
5
6
7
#include <iostream>
#include <filesystem>
int main(int argc, char* argv[]) {
std::cout << "Hello CMake!" << std::endl;
std::cout << std::filesystem::temp_directory_path().u8string() << std::endl;
return 0;
}

这个例子中,我们设定了C++标准。

构建和运行

首先,用cmake生成MSVC的相应工程文件

1
2
3
4
$ cd step1
$ mkdir build
$ cd build
$ cmake ..

再编译、链接,生成最终的可执行文件

1
$ cmake --build .

运行一下:

1
2
3
$ ./Tutorial.exe
Hello CMake!
C:\Users\ADMINI~1\AppData\Local\Temp\

默认编译的是Debug版本,若需编译Release版本,需指定config

1
$ cmake --build . --config Release

若需要指定编译器,指定平台,可以这样

1
2
3
4
5
$ cd step1
$ cmake -G "Visual Studio 16 2019" -A Win32 -S \path_to_source\ -B "build32"
$ cmake -G "Visual Studio 16 2019" -A x64 -S \path_to_source\ -B "build64"
$ cmake --build build32 --config Release
$ cmake --build build64 --config Release

给工程添加版本号和对应的头文件

添加版本号

1
project(Tutorial VERSION 1.0)

然后,配置一个头文件

1
configure_file(TutorialConfig.h.in TutorialConfig.h)

TutorialConfig.h.in

1
2
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

构建时,将会生成在可执行文件所在目录生成一个TutorialConfig.h。我们需要在项目中使用它,需要添加头文件包含目录。

1
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}")

修改tutorial.cxx测试一下

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <filesystem>
#include "TutorialConfig.h"
int main(int argc, char* argv[]) {
std::cout << "Hello CMake!" << std::endl;
std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "." << Tutorial_VERSION_MINOR << std::endl;
std::cout << std::filesystem::temp_directory_path().u8string() << std::endl;
return 0;
}

Step 2: 添加一个库

我们来添加一个实现开方的数学函数库,再在主工程中调用它。

它的目录结构像这样:

1
2
3
4
5
6
7
8
|--step2/
| |--build/
| |--MathFunctions/
| | |--mysqrt.h
| | |--mysqrt.cxx
| | |--CMakeLists.txt
| |--tutorial.cxx
| |--CMakeLists.txt

实现mysqrtlib

mysqrt.h

1
double mysqrt(double y);

mysqrt.cxx

1
2
3
4
5
6
7
8
9
10
11
double mysqrt(double y) {
double x, z, tempf;
unsigned long *tfptr = ((unsigned long *)&tempf) + 1;
tempf = y;
*tfptr = (0xbfcdd90a - *tfptr)>>1;
x = tempf;
z = y*0.5;
x = (1.5*x) - (x*x)*(x*z);
x = (1.5*x) - (x*x)*(x*z);
return x*y;
}

再添加MathFunctions的CMakeLists.txt, 它只有一行

1
add_library(MathFunctions mysqrt.cxx)

在主工程中添加依赖

1
2
3
4
add_subdirectory(MathFunctions)
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)
target_include_directories(Tutorial PUBLIC "${PROJECT_SOURCE_DIR}/MathFunctions")

add_subdirectory添加子目录,类似于qmake中的subdirs,以便于执行cmake时,会编译mysqrtlib。

target_link_libraries添加mysqrtlib依赖,类似于qmake中的LIBS += -lmysqrtlib

target_include_directories添加头文件包含目录,类似于qmake中的 INCLUDEPATH += $$PWD/mysqrtlib

然后在tutorial.cxx中调用mysqrt

1
2
3
4
5
6
7
#include <iostream>
#include "mysqrt.h"

int main(int argc, char* argv[]) {
std::cout << mysqrt(2) << std::endl;
return 0;
}

按照step1相同的步骤进行构建工程的生成和编译运行。

1
2
3
4
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .

使用宏开关控制使用mysqrt还是sqrt

在实际的项目中,我们可以使用一些宏开关来进行条件编译。来操练一下。

修改tutorial.cxx的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include "TutorialConfig.h"
#ifdef USE_MYMATH
#include "mysqrt.h"
#else
#include <cmath>
#endif

int main(int argc, char* argv[]) {
#ifdef USE_MYMATH
std::cout << "use mysqrt" << std::endl;
std::cout << mysqrt(2) << std::endl;
#else
std::cout << "use sqrt" << std::endl;
std::cout << sqrt(2) << std::endl;
#endif
return 0;
}

修改CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
option(USE_MYMATH "Use tutorial provided math implementation" ON)

configure_file(TutorialConfig.h.in TutorialConfig.h)

if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()

add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}" ${EXTRA_INCLUDES})

在CMake的开头位置增加了一个Option,默认开启。

再在TutorialConfig.h.in中增加一个宏定义

1
#cmakedefine USE_MYMATH

然后添加了if 子句用来添加头文件包含文件和依赖库。

使用mysqrt

1
2
3
$ cd step2/build
$ cmake ..
$ cmake --build .

不使用mysqrt

1
2
3
$ cd step2/build
$ cmake .. -DUSE_MYMATH=OFF
$ cmake --build .

Step 3: 简化Step 2的配置

我们可以使用INTERFACE来让Step 2的CMake配置更简洁。

在mysqrtlib的CMake配置中,我们添加了target_include_directories, 但作用域描述为INTERFACE,而非PUBLIC。它是指消费者需要它,而生产者并不需要。

step3\MathFunctions\CMakeLists.txt

1
2
add_library(MathFunctions mysqrt.cxx)
target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

接下来,我们依赖mysqrtlib库时,就不需要处理头文件包含目录的逻辑了。

step3\CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
option(USE_MYMATH  "Use tutorial provided math implementation" ON)

configure_file(TutorialConfig.h.in TutorialConfig.h)

if(USE_MYSQRT)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
endif()

add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}")

构建,运行:

1
2
3
$ cd step3/build
$ cmake ..
$ cmake --build .

Step 4: 安装

当我们需要将头文件、库文件或可执行文件安装到某目录时,会用到install

例如上面的mysqrtlib,需要在mysqrtlib/CMakeLists.txt末尾这样添加:

1
2
install(TARGETS MathFunctions DESTINATION lib)
install(FILES mysqrt.h DESTINATION include)

在主工程的CMakeLists.txt的末尾这样添加:

1
2
install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h" DESTINATION include)

然后,在build目录下执行install命令

1
2
$ cd step4/build
$ cmake --install .

如果我们要修改默认安装位置,需要这样

1
$ cmake --install . --prefix ../dist

Step 7: 打包

通过在主工程的CMakeLists.txt末尾添加几行命令,我们可以使用CMake来打包。

1
2
3
4
5
6
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set(CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set(CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
set(CPACK_SOURCE_GENERATOR "ZIP")
include(CPack)

编译,打包

1
2
3
4
$ cd step7/build
$ cmake ..
$ cmake --build . --config Release
$ cpack

指定打包器

1
$ pack -G ZIP

补充

  • 如果你实现的库中的.cpp实现中需要用到QtNetwork,但不包含在头文件中提供给用户。那么QtNetwork是一个PRIVATE依赖。
  • 如果你实现的库中的.cpp实现中用到了QtWidgets,且需要包含在头文件中提供给用户。那么QtWidgets是一个PUBLIC依赖。
  • 如果你实现的库中的.cpp实现中不依赖XX,仅在头文件中作为接口提供给用户。那么XX是一个INTERFACE依赖。

Reference