【cpp查漏补缺】3-CMake入门

【cpp查漏补缺】3-CMake入门

spiritTrance

前言

上一节 介绍了Makefile的用法,但由于Makefile编写过于复杂,于是CMake工具出现了。CMake用法比Makefile更为简单,其作用是生成Makefile文件。除此之外,CMake允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化Makefile和工程文件。本文将介绍CMake的基本用法。

由于CMake的功能十分强大,本篇文章采用增量式更新的形式(即:作者用到的时候才进行更新)。

CMake常用函数一览

这里先扔出一大堆常见函数,先了解其用法,留下一个印象,之后在后面具体实操。

编译相关

1
2
3
4
# 基本信息设置相关
cmake_minimum_required(VERSION 2.8) # 规定 CMake 的最低版本
project(Demo2 VERSION 0.1) # 定义 项目 名为Demo2,版本为0.1
set(var 1) # 变量定义,类似于var=1
1
2
3
4
5
6
7
8
# 添加源代码相关
# - 指定 生成目标,第一个是目标名,后面的为源代码
add_executable(main main.cc MathFunctions.cc)
# - 指定 源代码所在目录,第一个是目录路径,后面的为相应的变量,注意不会递归查找文件
# - 使用${SOURCE}来引用变量SOURCE:add_executable(main ${SOURCE})
aux_source_directory(./src SOURCE)
# - 添加子目录,请注意,子目录的CMakeLists.txt因为此函数也会被处理,且在子目录设置的变量会传播到本目录
add_subdirectory(./src/calc)
1
2
3
4
5
6
# 库与链接相关
# - 生成链接库:第一个是库名,第二个参数是类型
# 默认是STATIC静态链接库,SHARED为动态链接库,后面为源代码
add_library(MathFunctions [STATIC] ${DIR_LIB_SRCS})
# - 向目标添加相应库,第一个是目标名,第二个是静态链接库名
target_link_libraries(main MathFunctions)
1
2
3
4
5
6
7
8
9
10
# 头文件管理相关
# - 用于给指定目标设置头文件包含目录,其中第二个参数是权限控制:
# - - 1.PUBLIC:允许当前目标和依赖于当前目标的其他目标访问指定的包含目录,且指定目标会加入当前目标的编译过程
# - - 2.INTERFACE:包含目录可以被当前目标和依赖于当前目标的其他目标访问,通常使用于库,不会影响库本身的编译过程
# - - 3.PRIVATE:包含目录仅在当前目标内可见
target_include_directories(main PRIVATE /path/to/include)
# - 给整个项目的【所有目标】设置包含目录,一般不推荐使用
include_directories(/path/to/include)
# - 设置用户可配置的选项,value默认为`OFF`关闭,可选`ON`
option(<option_variable> "option_description" [value])
1
2
3
# 项目配置相关
# 根据*.h.in内容,生成*.h的内容,具体用法见后
configure_file("config.h.in" "config.h")

安装相关

这里仅放置一些常见用法,具体见

1
2
3
4
5
6
# install 命令用于处理在CMake中生成的一系列目标
# 安装可执行文件和库
install(TARGETS main DESTINATION ./bin)
install(TARGETS lib DESTINATION ./lib)
# 安装头文件
install(DIRECTORY include/ DESTINATION .)

测试相关

待补充

特殊内建变量

更多内建变量请查阅官方文档 ,下面列出常用的内建变量。

CMAKE_SOURCE_DIR:处理源代码时,整个项目的最顶层目录
PROJECT_SOURCE_DIR:处理源代码时,最后一个调用Project()的CMakeLists.txt所在的目录
CMAKE_CURRENT_SOURCE_DIR:处理源代码时,当前CMakeLists.txt所在目录
CMAKE_BINARY_DIR:构建过程中,整个项目的顶层目录
CMAKE_LIBRARY_OUTPUT_DIRECTORY:动态链接库的输出目录,默认目录为pwd
CMAKE_ARCHIVE_OUTPUT_DIRECTORY:静态链接库的输出目录,默认目录为pwd
CMAKE_RUNTIME_OUTPUT_DIRECTORY:目标文件的输出目录,默认目录为pwd
CMAKE_CXX_FLAGS:gcc的编译选项,如-O1 -g -Wall等等附加选项
CMAKE_CXX_STANDARD:c++的标准
CMAKE_CXX_STANDARD_REQUIRED:是否强制需要,True或False

举例:我在./build中执行cmake ..,而根目录有CMakeLists.txt,那么CMAKE_SOURCE_DIR.CMAKE_BINARY_DIR./build
如果还是不明白,可以使用message()函数进行打印。

初探CMake

这里沿用我们在上一节 中用到的项目,目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
├── bin
├── build # CMake 构建的工作目录
├── CMakeLists.txt
├── config # 配置文件目录
│ └── conf.h.in
├── include
│ └── calc.h
├── lib
└── src
├── calc
│ ├── add_sub
│ │ ├── add.cpp
│ │ └── sub.cpp
│ ├── CMakeLists.txt
│ └── mul_div
│ ├── div.cpp
│ └── mul.cpp
└── main.cpp

其次,我们应该如何使用CMake呢?首先创建名为CMakeLists.txt的文件,然后开始编写内容。

基本信息维护

首先,我们应该考虑加入基本信息:

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.0)    # 用于规定 cmake 工具的最低版本
project(demo VERSION 0.1) # 用于定义 project 的名称,注意 demo 是定义的变量名,标识一个 project,后面的 VERSION 标识项目的版本

set(CMAKE_CXX_FLAGS "-g -Wall") # 开启警告信息
set(CMAKE_CXX_STANDARD 11) # set 用于设置变量,CMAKE_CXX_STANDARD 用于标识 c++ 标准库的版本,这里设置的是c++11版本
set(CMAKE_CXX_STANDARD_REQUIRED True) # CMAKE_CXX_STANDARD_REQUIRED 表示这个标准是否被需要,这里设置为 True

这里我们用了几个函数,我们现在来一一介绍。

cmake_minimum_required:对CMake的最小版本要求。值得注意的是,如果版本设置过低,其会因为兼容性的原因而不允许进行构建。
project:定义项目名,后面可以跟版本号
set():这一函数用于设置变量名。值得注意的是,我们经常使用这个函数修改内建变量,以便于我们项目的构建。

配置选项

接下来,我们进行一些配置选项的设定。

1
2
3
4
5
6
7
8
9
10
# 配置
option(ECHO "echo test" OFF)
if(ECHO)
message("Echo on!")
set(testVar "This is a test")
else()
message("Echo off!")
set(testVar "This is a fail")
endif()
configure_file("${PROJECT_SOURCE_DIR}/config/conf.h.in" "${PROJECT_SOURCE_DIR}/include/config.h")

1
2
3
// ${PROJECT_SOURCE_DIR}/config/conf.h.in
#cmakedefine ECHO
#cmakedefine testVar "@testVar@"

其中我们主要用到了configure_file() 函数。这个函数提供了一些可能的配置,第一个参数接收配置文件的路径,第二个参数接收生成头文件的路径。
下面我们重点介绍了一下配置文件的编写。首先,我们使用#cmakedefine,来定义一个可能的宏。注意两个宏定义有所区分:
> #cmakedefine ECHO:这一个宏定义和option函数对应,需要注意的是option和这里的宏名称要一模一样。option函数有两个取值:ON和OFF。如果为OFF,那么生成的头文件config.h会被替换成/* #undef ECHO */,否则会替换成#define ECHO.
> #cmakedefine var1 "@var2@":注意这个宏定义,宏名称必须和CMake中设置的变量一样。其次,宏定义的内容可以任意取值。但比较特殊的是用"@@"括起来的CMake定义过的变量。为加以区分,我们用var1和var2来表述。如果var1在CMake中被set或option为ON的话,就会使用var2定义的宏。但如果var2被"@@"括起来的话,var2则会替换成CMake定义过的变量。

最后还需要留意两个地方:有一个是if-else-endif的控制结构。另外一个是内建变量${PROJECT_SOURCE_DIR},这个变量的含义请查阅《特殊内建变量》一节。
下面是configure_file()函数生成的config.h的内容:

1
2
#define ECHO
#define testVar "This is a test"

在这里,也许#cmakedefine var1 "@var2@"的含义还是不太明确。下面给出一些示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# case 1:
# var=ON # by call option()
#cmakedefine var "var" => #define var "var"

# case 2 - 4:
# var=OFF, var2=on # by call option()
#cmakedefine var "var" => /* #undef var */
#cmakedefine var "@var@" => /* #undef var */
#cmakedefine var "@var2@" => /* #undef var */

# case 5 - 7:
# var="test" # by call set()
#cmakedefine var "var" => #define var "var"
#cmakedefine var "@var@" => #define var "test"
#cmakedefine var "@var2@" => #define var ""

Q:为什么我修改了OPTION的定义,但输出的头文件没有变化?
A:注意到CMake构建后产生的CMakeCache.txt文件,不难找到有:

1
ECHO:BOOL=OFF

对的,如果你不清除这个文件,下次构建时就不会依照你的OPTION设定的选项,而是直接从CMakeCache.txt里面读取结果。CMakeCache.txt还包含了众多的内建变量。值得一提的是,set()设定的变量不会被包括进来。

添加源文件

下一步,我们需要添加源文件了:

1
2
3
# ./CMakeLists.txt
# 添加目录下的所有源代码
add_subdirectory("${PROJECT_SOURCE_DIR}/src/calc")
1
2
3
4
#./src/calc/CMakeLists.txt
aux_source_directory("${CMAKE_CURRENT_SOURCE_DIR}/add_sub" CALC_SRC_ADSB)
aux_source_directory("${CMAKE_CURRENT_SOURCE_DIR}/mul_div" CALC_SRC_MLDV)
add_library(calc STATIC ${CALC_SRC_ADSB} ${CALC_SRC_MLDV})

这里需要强调的是,这两份配置来自于两个CMakeLists.txt。第一份来自于根目录,而第二份来自于src/calc。下面介绍三个函数的作用:

add_subdirectory(path):将指定路径path加入项目,并根据指定路径的CMakeList.txt继续进行构建
aux_source_directory(path, var):将指定目录path下的所有源代码存入变量var中。

这里仍然值得注意的是路径的声明,其中用到了内建变量${CMAKE_CURRENT_SOURCE_DIR}。其含义仍然查询《特殊内建变量》一节。可以使用message()函数打印。

添加静态库及动态库

上面的CMakeists.txt还有一个函数没有介绍,这个函数就是用于构建库的函数。

add_library(lib_name, [STATIC | SHARED | MODULE], src...):用于构建目标库,其中第一个参数为库名,第二个参数默认为STATIC,用于构建静态库,后面的参数为源代码。

依赖配置

下一步是为我们的目标文件添加依赖。看过【cpp查漏补缺】1-cpp是如何跑起来的? 的都知道,一个目标文件的构建,需要考虑头文件,依赖库和其他所有源文件。这就是一个目标文件所有的依赖。

1
2
3
add_executable(main "${PROJECT_SOURCE_DIR}/src/main.cpp")
target_include_directories(main PRIVATE "${PROJECT_SOURCE_DIR}/include")
target_link_libraries(main calc)

其中我们用到了以下函数:

add_executable:用于添加可执行目标。后面为所有需要的依赖。这里仅需要src/main.cpp
target_include_directories:用于向目标添加头文件依赖所在的目录,这里是./include/这个目录。关于PUBLIC,PRIVATE和INTERFACE的权限控制符的含义见《编译相关》一节。
target_link_libraries:用于向目标添加库文件依赖,这里是calc这个库,在src/calc/CMakeLists/txt中定义的库。

安装配置

安装配置的作用是将整个CMake构建过程的中间目标,通过make install的方式输出到指定路径。增加配置如下:

1
2
install(TARGETS calc DESTINATION "${PROJECT_SOURCE_DIR}/lib")
install(TARGETS main DESTINATION "${PROJECT_SOURCE_DIR}/bin")

这里仅仅是一些简单示例,更多用法请查看文档

如何启动

首先,由于CMake构建过程中会产生很多中间文件。因此,我们在build文件夹下进行构建:

1
2
cd build
cmake ..

构建完成后,我们发现有很多中间文件,其中生成了makefile。我们进行make:
1
make

执行后的项目结构如下所示:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.
├── bin
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ │ ├── 3.22.1
│ │ │ ├── ...
│ │ │ ├── ...
│ │ │ └── ... (omitted)
│ │ ├── main.dir
│ │ │ ├── ...
│ │ │ └── ... (omitted)
│ │ ├── ...
│ │ └── ... (omitted)
│ ├── ...
│ ├── main
│ ├── Makefile
│ └── src
│ └── calc
│ ├── CMakeFiles
│ │ ├── ...
│ │ ├── ...
│ │ └── ... (omitted)
│ ├── ...
│ ├── libcalc.a
│ ├── Makefile
│ └── ... (omitted)
├── clean_script.sh
├── CMakeLists.txt
├── config
│ └── conf.h.in
├── include
│ ├── calc.h
│ └── config.h
├── lib
└── src
├── calc
│ ├── add_sub
│ │ ├── add.cpp
│ │ └── sub.cpp
│ ├── CMakeLists.txt
│ └── mul_div
│ ├── div.cpp
│ └── mul.cpp
└── main.cpp

可以发现,相应的目标文件都在build里面,和其他的中间文件混在一起很乱。
如果我们在build目录下执行make install呢?由于我们进行了安装配置,可以发现main和libcalc.a分别出现在了bin和lib下面,这达到了我们的预期。
实际上,我们可以修改一些内建变量,达到执行make就能将相应文件放到我们想要放的地方中。在根目录下的CMakeLists.txt的前面几行加入如下内容:
1
2
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)   # 可执行文件所在位置
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) # 静态库文件(archive)所在位置

再次执行make,可以发现相应目标文件都在根目录的bin和lib下了。但与此同时,build下面就不会出现这些目标文件了。

最后一种做法是作者在《编译原理》课程上看到的框架提供的做法,不清楚是否有潜在危害。

测试

待补充

CMake测试

CMake宏

项目迁移

待补充

参考资料

  • Title: 【cpp查漏补缺】3-CMake入门
  • Author: spiritTrance
  • Created at: 2023-10-02 21:40:00
  • Updated at: 2024-01-06 20:07:29
  • Link: https://spirittrance.github.io/2023/10/02/cpp_3_CMake用法/
  • License: This work is licensed under CC BY-NC-SA 4.0.
推荐阅读
【cpp查漏补缺】2-Makefile入门 【cpp查漏补缺】2-Makefile入门 【cpp查漏补缺】1-cpp是如何跑起来的? 【cpp查漏补缺】1-cpp是如何跑起来的? 【疑难杂症】记录从源码构建项目时碰到的一些坑 【疑难杂症】记录从源码构建项目时碰到的一些坑
 Comments