CMake 学习笔记
目录
本教程提供了一个渐进式的指导,参考的是官方教程 CMake Tutorial ,然后加入学习过程的笔记。涵盖了使用 CMake 构建一个工程时经常遇到的问题 。通过一个示例项目,展示各种功能是如何一起工作的,这对理解 CMake 非常有用。
1. 最简单的工程 #
最简单工程是从源码文件直接编译生成一个可执行的问题,最简单的解决方案只需要在 CMakeLists.txt 文件中添加三行。
新建一个工程目录,在目录下新建一个源文件 Tutorial.c
:
#include <stdio.h>
int main (int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
新建一个 CMakeLists.txt
:
cmake_minimum_required(VERSION 3.1)
project(Tutorial)
add_executable(Tutorial Tutorial.c)
CMakeLists.txt
是 cmake 生成 Makefile 所依赖的描述性文件,文件内容由一行一行的命令组成,命令不区分大小写。
- cmake_minimum_required 表示该项目对 CMake 的最低版本要求。
- project 用于设置项目名称。
- add_executable 添加了一个生成的可执行文件,和依赖的源码。
这样的话,执行 cmake .
生成 Makefile ,再执行 make
开始编译,就可以使用 Tutorial.c
生成的可执行文件 Tutorial
。
cmake 执行过程中会生成大量的缓存文件,又没有提供类似 cmake clean
的命令来清除生成的文件,有一个简单的方法可以解决这个问题。在工程目录下新建一个名为 build
的子目录,进入这个子目录中执行 cmake ..
,这样生成的文件都会输出到 build
子目录中,方便清理。
添加一个版本号 #
一个软件工程通常需要版本号,可以直接写在源码里,但是 CMake 提供了更便捷的方法。可以在 project()
命令中添加版本号,例如:
project(Tutorial VERSION 1.0)
其实,还可以在该命令中添加项目的描述等,它的语法是:
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
这样添加的版本号,被 cmake 存放在特定的变量中:
<PROJECT-NAME>_VERSION
,存放完整的版本号 。<PROJECT-NAME>_VERSION_MAJOR
,存放 major 。<PROJECT-NAME>_VERSION_MINOR
,存放 minor 。<PROJECT-NAME>_VERSION_PATCH
, 存放 patch 。<PROJECT-NAME>_VERSION_TWEAK
,存放 tweak 。
<PROJECT-NAME>
表示工程的名称,这里的值为 Tutorial ,所以 Tutorial_VERSION_MAJOR
的值就是 1
, Tutorial_VERSION_MINOR
的值就是 0
。我们需要新建一个名为 TutorialConfig.h.in
的文件传递这些变量:
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
然后,在 CMakeLists.txt
中添加一条命令:
configure_file(TutorialConfig.h.in TutorialConfig.h)
cmake 执行这条命令的时候,会读取 TutorialConfig.h.in
文件,并将其中的变量替换成真实的值,然后在执行目录下生成头文件 TutorialConfig.h
:
#define Tutorial_VERSION_MAJOR 1
#define Tutorial_VERSION_MINOR 0
下一步,我们需要在源码文件 Tutorial.c
中导入头文件,然后使用头文件里定义的宏打印版本号:
#include <stdio.h>
#include "TutorialConfig.h" # 导入头文件
int main (int argc, char *argv[])
{
printf("Hello World!\n");
printf("MAJOR Version is %d\n", Tutorial_VERSION_MAJOR); # 引用版本号的宏定义
printf("MINOR Version is %d\n", Tutorial_VERSION_MINOR);
return 0;
}
如果你是在源码目录下执行 cmake ,这样修改就可以了。如果是在 build
目录下执行 cmake ,生成的 TutorialConfig.h
文件位于 build
目录下,源文件中的 #include "TutorialConfig.h"
语句会找不到头文件,这时,需要将 build
目录页加入到头文件的检索路径中,可以在 CMakeLists.txt
的末尾加一行:
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}")
target_include_directories()
的语法是:
target_include_directories(<target> [SYSTEM] [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
它的作用是向目标文件添加头文件检索路径。当编译 target
目标文件时,去指定的 iters*
路径下检索头文件。target
的值必须是通过 add_executable()
或 add_library()
命令添加的目标文件名称。
PROJECT_BINARY_DIR
是指 cmake 提供的一个变量,表示目标文件输出的路径,通常就是执行 cmake 命令时的路径。还有一个变量 PROJECT_SOURCE_DIR
,表示源码的路径。它们的值都是由 project()
命令自动设置的。在 cmake 中,应用变量的语法是 ${variable-name}
。
测试 #
修改完毕后,CMakeLists.txt
的完整内容是:
cmake_minimum_required(VERSION 3.1)
project(Tutorial VERSION 1.0)
configure_file(TutorialConfig.h.in TutorialConfig.h)
add_executable(Tutorial Tutorial.c)
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}")
然后,我们在 build
子目录下依次执行 cmake ..
和 make
(或者 cmake --build .
) ,生成的文件如下:
~$ ls
CMakeCache.txt Tutorial inc
CMakeFiles TutorialConfig.h
Makefile cmake_install.cmake
~$ ./Tutorial
Hello World!
MAJOR Version is 1
MINOR Version is 0
2. 添加一个库 #
下面我们在工程中添加一个用于数学计算的链接库,把库的源码放在 MathFunctions
子目录中,工程结构如下:
~$ tree
.
├── CMakeLists.txt
├── MathFunctions
│ ├── MathFunctions.h
│ └── mysqrt.c
├── Tutorial.c
└── TutorialConfig.h.in
头文件 MathFunctions.h
中声明了一个计算平方根的函数 mysqrt()
:
double mysqrt(double x);
它定义在 mysqrt.c
文件中:
#include <stdio.h>
#include "MathFunctions.h"
// a hack square root calculation using simple operations
double mysqrt(double x) {
if (x <= 0) {
return 0;
}
double result = x;
// do ten iterations
for (int i = 0; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
double delta = x - (result * result);
result = result + 0.5 * delta / result;
printf("Computing sqrt of %f to be %f .\n", x, result);
}
return result;
}
然后,我们在 Tutorial.c
文件中调用这个函数计算一个数的平方根:
#include <stdio.h>
#include "TutorialConfig.h"
#include "MathFunctions/MathFunctions.h"
int main (int argc, char *argv[])
{
double input = 4;
printf("Hello World!\n");
printf("Version %d.%d\n", Tutorial_VERSION_MAJOR, Tutorial_VERSION_MINOR);
double output = mysqrt(input);
printf("The square root of %f is %f .\n", input, output);
return 0;
}
编译的时候,我们希望将 mysqrt.c
生成一个共享库,再连接到 Tutorial
目标中。所以,需要对 CMake 的描述文件做如下修改:
- 在
MathFunctions
子目录下新建一个CMakeLists.txt
,内容是add_library(MathFunctions mysqrt.c)
,表示将mysqrt.c
编译为库文件。 - 在顶层目录的
CMakeLists.txt
文件中如下内容:add_subdirectory(MathFunctions)
,表示向 CMake 工程添加一个子目录,执行时会调用子目录中的CMakeLists.txt
。target_link_libraries(Tutorial PUBLIC MathFunctions)
,表示目标文件Tutorial
要链接MathFunctions
。- 因为
Tutorial.c
文件中导入了MathFunctions
子目录下的头文件,所以,要用target_include_directories()
命令将这个子目录也加入头文件检索路径。
完整的顶层目录 CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.1)
project(Tutorial VERSION 1.0)
add_subdirectory(MathFunctions)
configure_file(TutorialConfig.h.in TutorialConfig.h)
add_executable(Tutorial Tutorial.c)
target_link_libraries(Tutorial PUBLIC MathFunctions)
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/MathFunctions"
)
然后,在 build
目录下依次执行 cmake ..
和 cmake --build .
,编译生成可执行文件 Tutorial
,执行:
~$ ./Tutorial
Hello World!
Version 1.0
Computing sqrt of 4.000000 to be 2.500000 .
Computing sqrt of 4.000000 to be 2.050000 .
Computing sqrt of 4.000000 to be 2.000610 .
Computing sqrt of 4.000000 to be 2.000000 .
Computing sqrt of 4.000000 to be 2.000000 .
Computing sqrt of 4.000000 to be 2.000000 .
Computing sqrt of 4.000000 to be 2.000000 .
Computing sqrt of 4.000000 to be 2.000000 .
Computing sqrt of 4.000000 to be 2.000000 .
Computing sqrt of 4.000000 to be 2.000000 .
The square root of 4.000000 is 2.000000 .
提供可选项 #
上一步添加的 MathFunctions
库可以做成一个可选的模块,这是大型工程里的常见做法。
首先需要用 option()
命令设置一个选项:
option(USE_MYMATH, "Use tutorial provided math implementation", ON)
这样就设置了一个名为 USE_MYMATH
的选项,初始值是 ON
。option()
的作用是新建一个用户可以选择的选项,语法很简单:
option(<variable> "<help_text>" [value])
三个参数依次是选项的变量名,选项的描述,选项的初始值。选项的值只有两个:ON
和 OFF
,如果没有设置初始值,默认就是 OFF
。
然后可以通过这个选项,把模块相关的语句包裹在一个选择语句中:
option(USE_MYMATH "Use tutorial provided math implementation" ON)
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${EXTRA_INCLUDES}"
)
list()
命令用于操作列表,APPEND
可以将一个元素追加到一个列表的尾部。后面通过新建的列表调用模块,这样的话,只有 USE_MYMATH
的值为 ON
时,才会将模块包含在编译过程中。
源码中的修改比较简单,只需修改 Tutorial.c
文件,通过宏定义将模块相关的语句做成可选项:
#ifdef USE_MYMATH
#include "MathFunctions/MathFunctions.h"
#endif
#ifdef USE_MYMATH
double output = mysqrt(input);
printf("The square root of %f is %f .\n", input, output);
#endif
这个宏定义的值也需要通过 TutorialConfig.h.in
文件传递给源码:
#cmakedefine USE_MYMATH
执行到 configure_file()
命令的时候,如果 USE_MYMATH
的值是 ON
,这条语句会被替换为 #define USE_MYMATH
;如果 USE_MYMATH
的值是 OFF
,这条语句会被替换为一行注释 /* #undef USE_MYMATH */
。
设置库文件的使用要求 #
构建 C 语言的工程时,应该清晰的规划模块之间的关系,在编译时,先编译出子模块的目标文件,再由这些目标文件链接起来生成上一层的目标文件,层层递进,最终编译出可执行文件。目标文件包括 add_executable()
和 add_library()
命令生成的可执行文件和库。目标文件的依赖关系是通过两条命令控制的:
target_include_directories()
,向目标文件添加编译时的头文件检索目录。target_link_libraries()
,向目标文件添加编译时依赖的库。
这两条命令需要一个控制传递属性的参数,可选三种关键字:
PRIVATE
,私有。库文件提供的方法只供目标文件使用,不会暴露给更上层目标文件。即生产者需要,消费者不需要。INTERFACE
,接口。库文件提供的方法会暴露给上层文件使用,本目标文件只用到了库文件提供的一些数据结构和声明等。即生产者不需要,消费者需要。PUBLIC
,公开。库文件提供的方法可以供所有目标文件使用。即生产者和消费者都需要。
以上一节的工程为例,目录结果是:
.
├── build
├── CMakeLists.txt
├── MathFunctions # 生成 libMathFunctions.a
│ ├── CMakeLists.txt
│ ├── MathFunctions.h
│ └── mysqrt.c
├── Tutorial.c
└── TutorialConfig.h.in
目标 MathFunctions
需要的头文件 MathFunctions.h
中的声明,并将方法暴露给上一层的 Tutorial.c
使用。所以,目标 MathFunctions
对头文件 MathFunctions.h
的使用要求就是 INTERFACE
,目标 Tutrial
对目标 MathFunctions
的使用要求是 PRIVATE
。
那么,可以在 MathFunctions/CMakeLists.txt
中添加一行:
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
然后就可以把顶层 CMakeLists.txt
中包含头文件的部分删除,并将库文件的使用要求改为私有:
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
endif()
target_link_libraries(Tutorial PRIVATE ${EXTRA_LIBS})
target_include_directories(Tutorial PRIVATE
"${PROJECT_BINARY_DIR}"
)
3. 安装和测试 #
这个工程只需要把可执行文件安装到指定的目录,所以,在顶层 CMakeLists.txt
目录下添加一行:
install(TARGETS Tutorial DESTINATION bin)
这样,在执行 make install
时,就会把 Tutorial
文件安装到 /usr/local/bin/
目录下,默认的前缀是 /usr/local/
,如果要换别的目录,可以写上绝对路径。