【08】CMake入门

0 阅读8分钟

[26/3/20]CMake入门

参考资料:

【现代C++: CMake简明教程】www.bilibili.com/video/BV1xa…

【CMake 保姆级教程【C/C++】】 www.bilibili.com/video/BV14s…

初次使用

  1. 安装 cmake:sudo apt install cmake
  2. 检查安装结果:cmake --version
  3. 创建一个 cpp 文件,并写源码。假设这个文件名为:hello.cpp
  4. 在 cpp 项目目录下创建一个空文本文件,名字写成:CMakeLists.txt
  5. 在这个 txt 文件里写以下内容:
# 指定最低的cmake工具版本号
cmake_minimum_required(VERSION 3.20)

# 指定本工程的工程名字
project(my_project)

# 指定编译结果文件名 以及用什么文件来编译
add_executable(hello_app hello.cpp)
  1. 运行 cmake:cmake --build build
  2. 运行编译后的程序,验证编译成功(注意在 build 文件夹下):./build/hellp_app

构建指令说明

对于构建:一般来说是分成两步的,先用 CMake 生成 makefile,再用 make 构建。

所以传统 CMake 的操作流程为:

  • mkdir build; cd build; cmake ..; make

但是这四条命令太繁琐了,有更加现代的语法进行替换:

  1. cmake -B build:CMake 帮你创建 build 目录并构建 makefile
  2. cmake --build build:CMake 帮你调用 make 进行构建

多文件编译

如果是多文件,那么 add_executable(输出文件名 源文件名列表)

这里的源文件名列表,需要把所有的源代码文件列出来,你可以采用空格 ``进行分隔也可以采用分号 ; 进行分隔。

(这和 g++ *.cpp -o my_app 有异曲同工之妙)

但是手写所有的文件名太累了,所以可以采用文件搜索功能!

编译命令

首先,由于 CMakeLists.txt 这个文件名是被强制要求的,所以 CMake 永远会对着这个文件名去运行,所以你不需要把这个名字作为参数输入。

然后,假设你当前的工作文件夹下就有这个文件,你可以直接使用 cmake . 来进行编译。

但是这会使得所有的编译过程文件全都出现在当前文件夹下,所以你还可以这样:

mkdir build 接着 cd build 最后 cmake .. 就是告诉 CMake 在哪个目录下找这个文件。

最后要运行 make 来编译(前面只是使用 CMake 生成 Makefile)

CMake 语言:变量

set

  1. 设置变量: set(变量名 变量内容):CMake 的变量默认都是字符串,且和 Shell 类似,需要使用 ${val_name} 来访问
  1. 变量合并:可以这样把 b 变量的内容追加到 a 中:set(a a b)

实例:

# 设置变量,注意后面的多个文件名会合并成一个字符串!
set(SRC_LIST main.cpp a.cpp b.cpp c.cpp)	# 也可以用;分隔
add_executable(myApp ${SRC_LIST})					# 别忘了${}

# 通过设置 宏变量 来指定C++标准,可以是11/14/17/20...
set(CMAKE_CXX_STANDARD 11)

对于设置 C++标准,你还可以这样:

cmake CMakeLists.txt -DCMAKE_CXX_STANDARD=11

请注意,这里的 -D 表示设置宏,这条命令和直接写入到文件中效果等价

文件搜索

  1. aux_source_directory(<dir> variable):把指定路径下搜索到的源文件存储到变量里(自动检索 .c 和 .cpp 这两类文件)
  2. file(GLOB/GLOB_RECURES 变量名 要找的目录和文件类型)
    1. GLOB:只找当前目录
    2. GLOB_RECURES:递归搜索
    3. 可以使用通配符
  1. 宏:${PROJECT_SOURCE_DIR}:这个宏指代的是 整个项目最上层的 CMakeLists.txt 所在的路径,在指定路径时,可以使用这个宏变量来辅助
  2. 宏:${CMAKE_CURRENT_SOURCE_DIR}当前的 CMakeLists.txt 所在的路径

指定头文件路径

  1. 可以使用 tree 命令先查看项目结构,然后推荐的项目结构可能如下:
.
├── CMakeLists.txt
├── build
├── include
│   └── head.h
└── src
    ├── a.cpp
    ├── b.cpp
    ├── c.cpp
    └── main.cpp
  1. 此时由于 main.cpp 和 .h 文件不在同一级目录,直接编译会显示找不到头文件
  2. include_directories(headpath):用这条命令设置头文件路径

库相关

文件后缀

基础概念:文件名

平台动态库静态库
Linuxlibxxx.solibxxx.a
Winlibxxx.dlllibxxx.lib

制作库相关命令

首先,由于是制作 库 而不是可执行程序,

所以,不需要指定可执行文件的输出路径,

也不需要指定输出可执行文件,

也就是不需要写 add_executable

此外,还需要注意的是,不要把 main.cpp 加入到源文件列表里!

因为库文件就是没有主函数的!

(比如说,可以把 main.cpp 移动到 src 文件夹之外)

  1. 制作动态库(共享库):

add_library(库的名称 SHARED 源文件列表)

  1. 制作静态库命令:

add_library(库的名称 STATIC 源文件列表)

  1. 执行 cmakemake 命令之后,就会生成库文件了
  2. 库在发布时,必须同时将 生成的库文件和相应的头文件 一起发布!

(也可以注意到,源文件列表只包含 .cpp 文件,不需要 .h 文件!)

  1. 补充:指定库文件的生成路径:宏 ${LIBRARY_OUTPUT_PATH}

链接静态库

语法:link_libraries(静态库列表)

注意:列表里可以写文件全名,也可以掐头(lib)去尾(.a)只写中间部分

如果找不到,还需要指定路径:(这个对动态和静态库都生效)

语法:link_directories(库目录列表)

最后:由于静态库是最终被编译进二进制文件的,所以说,后续使用就非常方便,除了要加上述两行指令之外,使用起来和纯源代码编译效果是一样的。

链接动态库

由于动态库需要在运行时被加载,所以要考虑

语法:target_link_libraries(需要库的文件名 动态库列表) 也可以掐头去尾

然后注意啊,由于动态库是运行时才被链接的,所以!

其中,需要库的文件名可以是可执行程序的名称,也可以是其他库

(就是其他库对后面的动态库有依赖的情况)

(这也是为什么链接指令写在生成可执行文件指令之后)

静态库的链接指令需要写在 add_executable 之前,

动态库的链接指令需要写在 add_executalbe 之后!

所以 target_link_libraries 需要被写在最后。

为啥编译的时候没有动态库还能编译成功?这不是有头文件嘛。

消息打印

语法:message([消息等级] "消息内容") 可以打印变量

消息等级参数:

  • 不写:重要消息
  • STATUS:非重要消息(只有这个是标准输出)
  • WARNING:警告,但是会继续执行
  • AUTHOR_WARNING:警告(dev),但是会继续执行
  • SEND_ERROR:错误,会继续执行,但是会跳过生成步骤
  • FATAL_ERROR:错误,终止所有处理过程

字符串操作

  1. 字符串拼接:set(变量名 ${拼接变量1} ${拼接变量2} ...) # 同理可以使用分号 ; 分隔(也可以直接写字符串,不一定必须是变量)
  2. 追加:list(APPEND 变量名 要追加到后面的变量列表)
  3. 移除:list(REMOVE_ITEM 变量名 要删除的变量列表) (要完全匹配!!!)
  4. 获取元素个数:list(LENGTH 要计算长度的变量列表 接收长度变量名)
  5. 获取指定索引元素:list(GET 变量列表 索引值 接收数据的变量名)
  6. 用指定连接符连接:list(JOIN 变量列表 分隔符 合并后的变量名)
  7. 查找元素索引:list(FIND 变量列表 查找元素 输出变量名) # 没有就返回-1
  8. 插入若干元素:list(INSERT 变量列表 插入位置索引 插入值列表)
  9. 插入头部:list(REPEND 变量列表 插入的元素列表)
  10. 移除最后元素:list(POP_BACK 变量列表 [存弹出的变量名])
  11. 移除头部元素:list(POP_FRONT 变量列表 [存弹出的变量名])
  12. 移除指定索引元素:list(REMOVE_AT 变量列表 索引列表)
  13. 移除重复元素:list(REMOVE_DUPLICATES 变量列表)
  14. 列表翻转:list(REVERSE 变量列表)
  15. 列表排序:list(SORT 变量列表 一些可选参数)

自定义宏

这里的自定义宏是写在 CMakeLists.txt 的,并且在程序源码里,可以用

#ifdef 来读取这些宏是否被定义。

定义宏语法:add_definitions(-D宏名字) # 可以定义多个,都是 -D 宏名字 并用空格分开就行

嵌套 CMake

嵌套规则

  1. 最核心的语法是,增加子节点(子目录):add_subdirectory(子文件夹名)
  2. 项目的根目录底下有一个 CMakeLists.txt 编译项目的时候只需要编译这一个就行了,让这个一个去调用各个子层级的 CMakeLists.txt
  3. 对于子层级,一般是,一个库(或一个 mian 项目,也就是要生成一个可执行程序的目录)底下有一个单独的 CMakeLists.txt 用来单独编译这个库(或程序)
  4. 父级的 CMakeLists.txt 中定义的变量可以被子层级调用,反之不行

实例

0) 项目树

项目结构如下,包含两个库 labc 和 lxyz,以及生成两个可执行程序 test1 和 test2:

.
├── CMakeLists.txt
├── include
│   ├── labc.h
│   └── lxyz.h
├── labc
│   ├── CMakeLists.txt
│   ├── a.cpp
│   ├── b.cpp
│   └── c.cpp
├── lxyz
│   ├── CMakeLists.txt
│   ├── x.cpp
│   ├── y.cpp
│   └── z.cpp
├── test1
│   ├── CMakeLists.txt
│   └── main.cpp
└── test2
    ├── CMakeLists.txt
    └── main.cpp

1) 根目录 CMake 内容

对于主目录下的 CMake 文件,内容如下:

# 1. CMake 最小版本限制
cmake_minimum_required(VERSION 3.20)
# 2. 项目名称
project(myApp)

message("当前项目路径: ${PROJECT_SOURCE_DIR}")

# 定义变量用于后续操作(主要是为了被子文件调用)
#  1  设置静态库编译后的输出路径为./lib
set(LIB_PATH ${PROJECT_SOURCE_DIR}/lib)
#  2  设置可执行程序编译后的输出路径为./bin
set(EXEC_PATH ${PROJECT_SOURCE_DIR}/bin)
#  3  设置头文件的查找路径(要和你的实际路径保持一致!)
set(HEAD_PATH ${PROJECT_SOURCE_DIR}/include)
#  4  所调用的库的名字,也用变量保存(后续被子文件调用)
set(LABC_LIB labc)
set(LXYZ_LIB lxyz)
#  5  要构建的可执行程序的名字,也用变量保存(后续被子文件调用)
set(APP_NAME_1 myApp1)
set(APP_NAME_2 myApp2)

# 给当前节点增加子节点! 传入的是子文件夹的名字!
add_subdirectory(labc)
add_subdirectory(lxyz)
add_subdirectory(test1)
add_subdirectory(test2)

以后可以根据需要新增内容。

2) 库目录 CMake 内容

cmake_minimum_required(VERSION 3.20)
project(labc)

# 搜索源文件到变量SRC
# 由于这个cmake文件和源码放在同一目录所以用./
aux_source_directory(./ SRC)
# 指定头文件路径,这里调用了父节点变量
include_directories(${HEAD_PATH})
# 设置生成的库文件输出位置,采用宏变量和父节点变量
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
# 这是个库,设置生成库的名字和源文件,名字用父节点变量
add_library(${LABC_LIB} STATIC ${SRC})

对于 lxyz,只需要修改第 2 行的 labc 和第 12 行就行 LABC_LIB 就可以了。

3) 子项目 CMake 内容

cmake_minimum_required(VERSION 3.20)
project(test1)

# 搜索源代码路径,存入变量SRC
aux_source_directory(./ SRC)
# 指定头文件路径,用父节点变量
include_directories(${HEAD_PATH})
# 想要调用库,只有库的名字不一定找得到,所以还需要库路径
#  - 指定库文件路径,用父节点变量
link_directories(${LIB_PATH})
#  - 指定依赖库名字,用父节点变量
link_libraries(${LABC_LIB})
link_libraries(${LXYZ_LIB})
# 设置生成可执行文件的路径(宏变量),用父节点变量
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})

# 指定生成可执行程序
add_executable(${APP_NAME_1} ${SRC})

对于 test2,只需要修改第 2 行的 test1 和第 18 行的 APP_NAME_1 就可以了。

4) 构建命令

在项目根目录执行

  1. cmake -B build
  2. cmake --build build

在静态库中链接静态库

这个非常简单,就是比如说 main.cpp 依赖库 XYZ,库 XYZ 又依赖静态库 aaa,即:

main.cpp <- XYZ <- aaa

那么在 main.cpp 的 CMake 脚本下,只需要链接静态库 XYZ,不需要考虑 aaa;

而在 XYZ 的 CMake 脚本下,需要指明链接静态库 aaa;

然后你想要链接一个静态库,只需要同时指明这个库的名字和路径就行,也就是:

  1. link_libraries(静态库的名字)
  2. link_directories(静态库所在目录)

最后最坑的一点是,总的来说,你还是需要编译库 aaa 的,

所以你需要把 aaa 所在的子目录加到编译列表里,也就是:

  1. add_subdirectory(静态库所在的文件夹名)

并且这一行内容要在父级文件夹的 CMake 脚本中

在静态库中链接动态库

其实也是类似的,只是用的命令不一样,而且命令的位置有要求:

  1. 先指明库路径:link_directories(动态库所在目录)
  2. 链接库:target_link_libraries(目标名字 动态库名字)

这里需要注意,目标名字是静态库名字,且链接库这行代码必须

写在 add_library 也就是生成当前的静态库指令的后面。

此外同样的,不要忘了把动态库所在的目录加入编译列表:

  1. add_subdirectory(动态库所在的文件夹名)