CMake学习笔记(持续更新) miyuri Linux,C 2019-11-19 [TOC] 最后更新:2020年11月22日 前言:CMake的命令对大小写不敏感,但变量对大小写敏感。因此有的动物习惯把CMake的命令全部大写,但这里遵循官方文档一律小写,以和变量名区分。 CMake的配置文件是**CMakeLists.txt**,不管在哪个操作系统下。 # 0 基础项目 1. [cmake_minimum_required](https://cmake.org/cmake/help/latest/command/cmake_minimum_required.html " cmake_minimum_required") ```shell cmake_minimum_required(VERSION <最低版本号>[...<最高版本号>] [FATAL_ERROR]) ``` 指定CMake版本号范围。没有特殊需求的话就指定一个`2.6`吧。顺便把`FATAL_ERROR`也加上,堵死那些还在用上古版本(早于2.4)CMake的计算机。 1. [project](https://cmake.org/cmake/help/latest/command/project.html "project") ```shell project(<项目名> [<使用语言>...]) project(<项目名> [VERSION [.[.[.]]]] [DESCRIPTION <项目描述>] [HOMEPAGE_URL <主页地址>] [LANGUAGES <使用语言>...]) ``` 重点说一下使用语言。CMake支持的使用语言包括:`C`、`CXX`(C++)、`CUDA`、`OBJC`(Objective-C)、`OBJCXX`、`Fortran`、`ASM`(汇编语言)。 如果不填写使用语言,CMake默认会打开`C`和`C++`开关,检查系统是否安装C编译器和C++编译器。如果你只使用其中一种编译器,就很有必要指定使用语言。 如果使用汇编语言,请将`ASM`标签置于最后,这样CMake会检查其他语言的编译器是否支持汇编。 1. [add_executable](https://cmake.org/cmake/help/latest/command/add_executable.html "add_executable") ```shell add_executable(<目标名> [EXCLUDE_FROM_ALL] [源码文件1] [源码文件2 ...]) ``` 使用给定的源文件,为工程制定一个**可执行**编译目标。`EXCLUDE_FROM_ALL`标签将使该可执行文件不会被默认的`make`命令所编译——除非你手动指定该目标名(`make `)。 # 1 编译器设置 ##1.1 选择编译器 众所周知,Linux下的默认自带编译器是GCC(GNU C Compiler),CMake默认情况下也同样调用它。但近些年来,许可证友好、代码简明易读、消耗更少资源的Clang越来越受欢迎。如果想改用诸如Clang之类的其他编译器该怎么办呢? 通过设置`CMAKE_CXX_COMPILER`环境变量解决: ```shell set (CMAKE_CXX_COMPILER <编译器路径>) ``` 其中`<编译器路径>`可以是绝对路径,如`/usr/bin/clang++`,也可以直接填写编译器可执行文件的名称,如`clang++`,这种情况下CMake会自动寻找该文件的路径。 注意: 1. **该行必须被置于`project`行前**! 1. 一个CMake项目一旦被`cmake`命令生成,即无法通过修改CMakeLists.txt的方法修改编译器,除非你删除所有CMake生成的文件。 这就是为什么大家都推荐新建一个专门用于生成的文件夹,在里面进行生成。因为这样的话遇到问题只要直接把整个文件夹一删,即可干干净净地从头再来。CMake不提供类似`make clean`的清理命令! ## 1.2 设置编译参数 CMake提供的参数太多,具体参见[官方文档](https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html "cmake-variables(7)")。这里介绍几个(我自己认为)比较常用的参数。 **注意:**下列参数应置于`project`行后。 - [`CMAKE_C_STANDARD`](https://cmake.org/cmake/help/latest/variable/CMAKE_C_STANDARD.html "CMAKE_C_STANDARD"):设置默认C语言标准。 - [`CMAKE_CXX_STANDARD`](https://cmake.org/cmake/help/latest/variable/CMAKE_CXX_STANDARD.html "CMAKE_CXX_STANDARD"):设置默认的C++语言标准。 - [`CMAKE__FLAGS`](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS.html "CMAKE__FLAGS"):设置传递给编译器的参数。 - [`CMAKE__FLAGS_`](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS_CONFIG.html "CMAKE__FLAGS_"):设置在``模式下传递给编译器的参数。 - [`CMAKE_VERBOSE_MAKEFILE`](https://cmake.org/cmake/help/latest/variable/CMAKE_VERBOSE_MAKEFILE.html "CMAKE_VERBOSE_MAKEFILE"):让Make工作在啰嗦模式下,输出更加详细的日志。 下文是一个编译参数的配置例子: ```shell set (CMAKE_VERBOSE_MAKEFILE ON) set (CMAKE_CXX_COMPILER "clang++") set (CMAKE_CXX_FLAGS "-Wall -Wextra") set (CMAKE_CXX_FLAGS_DEBUG "-O0 -glldb") set (CMAKE_CXX_FLAGS_RELEASE "-O4") set (CMAKE_CXX_STANDARD 20) ``` 顺便一提,CMake通过设置`CMAKE_BUILD_TYPE`参数来设置生成模式。例如在生成时添加`-D CMAKE_BUILD_TYPE=Release`即可在编译时启用`FLAGS_RELEASE`后的参数。 # 2 库管理 ## 2.1 添加本地库 C/C++语言是十分灵活的语言,用户不需要把所有代码全部保存在单个源文件里,而可以分成多个不同的源文件,最后由编译器统一编译链接(下文又将这两步流程合并为“生成”)进单个可执行文件中。 而那些不包含`main`函数、只参与添砖加瓦工作的源文件,便可视作本地库。因为本地库不像下文提到的外部库那样已经编译妥当只待调用,因而需要使用[add_library](https://cmake.org/cmake/help/latest/command/add_library.html "add_library")命令添加至项目,并指定编译目标名。 ```shell add_library(<目标名> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [源文件1] [源文件2 ...]) ``` 其中`STATIC`代表静态库,扩展名一般为`.a`、`.lib`;`SHARED`代表动态库,扩展名一般为`.so`、`.dll`,下文介绍的“外部库”几乎全为动态库;`MODULE`代表不在生成期进行链接的动态插件库。 ## 2.2 添加外部库 ### 2.2.1 find_package 众所周知,现代软件开发已经离不开先辈动物们提供的现成运行库了。Linux拥有强大的软件包管理系统,因而只需要安装对应的开发库软件包(包名一般以`dev`、`devel`结尾),并在生成选项中添加这些开发库的相关信息,就可以轻松生成,而不需要像司马Windows下的开发环境那样手动添加头文件、库文件路径。 用Makefile生成过项目的动物应该都知道`pkgconf`(又名`pkg-config`)。CMake同样拥有属于自己的开发库辅助工具:[find_package](https://cmake.org/cmake/help/latest/command/find_package.html "find_package")。 ```shell find_package(<包名> [版本] [EXACT] [QUIET] [MODULE] [REQUIRED] [[COMPONENTS] [组件名...]] [OPTIONAL_COMPONENTS 组件名...] [NO_POLICY_SCOPE]) ``` 注意:这个命令与`pkgconf`关系不大,它是通过调用对应名称的模块(Module)来进行包查找的。模块文件名为`Find<包名>.cmake`,CMake本身预装了许多Find模块,可通过`cmake --help-module-list | grep "^Find"`命令查看。如果想添加自己的模块,可以先将自定义模块文件夹的路径赋予`CMAKE_MODULE_PATH`变量,再将自己编写的模块文件放入。 ###2.2.2 熟悉的pkgconf 然而,CMake自带的Find模块满打满算也就不到200种,根本无法覆盖多如牛毛的开发库。 百姓无不怀念我大`pkgconf`!!! 所幸,CMake提供了一个与`pkgconf`对接的接口。 首先,我们需要使用上面介绍的`find_package`命令导入[FindPkgConfig](https://cmake.org/cmake/help/latest/module/FindPkgConfig.html "FindPkgConfig")模块: ```shell find_package(PkgConfig REQUIRED) ``` 然后再使用`pkg_check_module`功能,调用`pkgconf`命令查询指定库,并将查询到的结果保存至变量中。命令格式如下: ```shell pkg_check_modules(<前缀> [REQUIRED] [QUIET] [NO_CMAKE_PATH] [NO_CMAKE_ENVIRONMENT_PATH] [IMPORTED_TARGET [GLOBAL]] <包配置> [<包配置>...]) ``` <包配置>既可以是单纯的开发包名,也可以使用数学比较符指定版本号,具体请查阅官方文档。 执行完`pkg_check_modules`后,所有`pkgconf`的输出结果都被保存在了一组名为`<前缀>_XXX`的变量中。下表为输出变量一览表: | 变量 | 描述 | 对应命令 | ------------ | ------------ | ------------ | | `<前缀>_FOUND` | 如果找到该包,则设为1 | | | `<前缀>_LIBRARIES` | 库列表 | `pkgconf <包名> --libs-only-l`,再删去每个库名前面的`-l`(小写的L) | `<前缀>_LINK_LIBRARIES` | 库路径列表 | `pkgconf <包名> --libs-only-l`,再列出每个库名对应的库的绝对路径 | `<前缀>_LIBRARY_DIRS` | 库目录列表 | `pkgconf <包名> --libs-only-L`,再删去每个目录前面的`-L` | `<前缀>_LDFLAGS` | 所有链接标记 | `pkgconf <包名> --libs` | `<前缀>_LDFLAGS_OTHER` | 其他(other)链接标记 | `pkgconf <包名> --libs-only-other` | `<前缀>_INCLUDE_DIRS` | 包含目录列表 | `pkgconf <包名> --cflags-only-I`,再删去每个目录前面的`-I`(大写的i) | `<前缀>_CFLAGS` | 所有编译标记 | `pkgconf <包名> --cflags` | `<前缀>_CFLAGS_OTHER` | 其他(other)编译标记 | `pkgconf <包名> --cflags-only-other` 从上表可以看出,要导入外部包,最低只需要两个变量:`<前缀>_LDFLAGS`和`<前缀>_CFLAGS`。 ## 2.3 向目标导入库 库添加/查找完了当然是不够的,你得把这些库喂给依赖它们的编译目标中。 对于1.1中提到的静态库,既然你已经把库加进来了,直接用[target_link_libraries](https://cmake.org/cmake/help/latest/command/target_link_libraries.html "target_link_libraries")导入即可,不在话下。 对于1.2.1中由模块返回的库信息,鉴于每个查找模块返回的变量不一样,需要自己查阅官方文档或模块源文件,弄清该模块的返回值,再对症下药。 对于1.2.2中返回的库信息,基本思路有以下两种: 1. 用[target_link_options](https://cmake.org/cmake/help/latest/command/target_link_options.html "target_link_options")导入`<前缀>_LDFLAGS`,用[target_compile_options](https://cmake.org/cmake/help/latest/command/target_compile_options.html "target_compile_options")导入`<前缀>_CFLAGS`。简单粗暴。 1. 进一步拆分,用`target_link_libraries`导入`<前缀>_LIBRARIES`,用[target_link_directories](https://cmake.org/cmake/help/latest/command/target_link_directories.html "target_link_directories")导入`<前缀>_LIBRARY_DIRS`,用[target_include_directories](https://cmake.org/cmake/help/latest/command/target_include_directories.html "target_include_directories")导入`<前缀>_INCLUDE_DIRS`,用`target_compile_options`导入`<前缀>_CFLAGS_OTHER`。愿意使用这种方法的动物更多,分得开意味着好管理。 另外,这些`target_`命令有一个共同点,它们都可以指定继承属性``。 关于`target_link_libraries`,这篇[Qiita贴子(日文警告)](https://qiita.com/chapuni/items/7ddffb46fa327c5bef43 "CMake: target_link_libraries(PUBLIC/PRIVATE/INTERFACE\) の実践的な解説")提出了一个十分离经叛道的说法: - 静态库使用`INTERFACE`,能够加快静态库源代码的编译速度,因为编译过程只和源代码自身有关(是的,和依赖库没有关系,那是最后链接可执行文件时的事情),使用`PUBLIC`会导致时间白白浪费在等待其他依赖库编译完成上。 - 动态库使用`PRIVATE`,避免不必要的系统开销。 - **任何情况下不要使用默认的**`PUBLIC`**!!!!!!!!!!!** - 有一个例外情况。向可执行文件链接库的时候,不能使用`INTERFACE`。可以不填或使用`PRIVATE`和`PUBLIC`。 考虑到作者是有能力向LLVM提交代码的大佬,该言论颇具说服力,虽然本狐并没有完全整明白`INTERFACE`的继承原理是怎么和节省编译时间搭上边的QAQ。这篇文章只讲述了`target_link_libraries`,但应该也适用于其他`target_`……吧。(待究明) 本文由 miyuri 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。
还不快抢沙发