cmake从入门到完全放弃
前言
C++ 的构建系统就如同众多的C++ 编译器一样繁杂,不同构建系统的构建文件语法、操作方式都大相径庭,细节问题繁多。其中以生成不同平台的构建系统文件的跨平台特性为设计目标的CMake尤甚。如何一统各个构建系统、打通各个编译器。是CMake作为一个新构建系统首先要做的工作。本文是作者几天以来使用CMake构建软件时的笔记,兼作对C++ 新手的分享。
CMake构建一般流程
1 | mkdir build && cd build |
第一次运行时CMake扫描源码目录(Source Directory)中的CMakeLists.txt读取项目配置,并以此在(Build Directory)生成CMakeCache.txt。CMakeCache.txt包含一些配置的缓存信息,用户可以使用程序cmake-gui编辑选项。这一个阶段叫配置(Configure)。
其中有一些通用的选项
- CMAKE_BUILD_TYPE: 从Release、RelWithDebInfo、Debug等选择
- CMAKE_INSTALL_PREFIX: 此工程的安装路径,对于unix默认是/usr/local。
- BUILD_SHARED_LIBS: 设置ON或OFF控制生成共享库的默认行为
在构建(build)阶段可以手动使用目标构建系统(生成器)构建,也可以使用cmake帮忙调用目标构建系统(生成器)构建。
从一个CMakeLists.txt说起…
CMakeLists.txt语法由一系列类似函数调用的语句组成,CMake内置的函数叫命令(Command)。以下是一个非常简单的CMakeLists.txt。
1 | project(Myproject) |
CMakeLists.txt的解释是顺序执行的,首先我们定义了一个工程(project),名字叫Myproject,project()
这个命令还能选择不同的编程语言和项目的一些元信息。然后我们使用add_library()
添加了一个库目标,名字叫MYLIB、包含simple_lib.cpp simple_lib.hpp文件。然后使用target_compile_definitions()
朝MYLIB目标添加了两个宏定义,其中MYLIB_PUBLIC
是公共的(PUBLIC),我解释为“导出”的,会传播到之后依赖此目标的目标中(即main)。MYLIB_PRIVATE
是私有的(PRIVATE)。然后我们使用target_include_directories()
朝MYLIB目标添加了一个include目录。最终我们使用add_executable()
添加可执行文件目标main,包含simple_example.cpp。然后将MYLIB目标公共链接到main。
你可以发现CMake的基础概念是目标。构建系统的核心也在于此:管理目标的构建、不同目标的依赖问题。在CMake中的目标/目录等等还附带有属性(property)。target_compile_definitions
等命令就是在操作目标的属性。最终由CMake分析生成对应构建系统/生成器的描述文件并构建。
CMake的图灵完备/变量、函数、控制流、计算
当然CMakeLists这样的DSL怎么能不去讨论它的图灵完备性呢,让我们借此机会来看看CMake中的计算。
1 | function(fibonacci var n) |
cmake跟shell脚本很像,所有变量都是字符串类型,因此你可以看到这份代码的扭曲程度。这种意义上感觉CMake更像是汇编,只不过寄存器是无限的而且里面只能存字符串。
- 计算
math(EXPR <variable> "<expression>")
- 控制流
if
1 | if(<condition>) |
1 | foreach(<loop_var> <items>) |
1 | while(<condition>) |
- 变量
set(<variable> <value>... [PARENT_SCOPE])
其中function()
新增了一个作用域(SCOPE),set(... PARENT_SCOPE)
设置父作用域。本代码中用其起到返回值的作用。
生成表达式(Generator Expression)
生成表达式是一系列需要计算的字符串表达式。有功能类似运算符的、也有引用目标属性的等等。本文着重讲解目标依赖表达式(Target-Dependent Expressions),更多请翻阅官方手册。
目标依赖表达式能够获取目标属性、目标include目录等字符串。当然你可能会有疑惑,CMakeLists的执行过程中不就是在生成这些字符串吗。如果能够引用这些字符串,那么岂不是还增加了一层变量的依赖?这不是很难解决吗?所以我们得先理解CMake的几个阶段。
- 配置(configuration)– 配置项目,将目标的构建信息全部计算出来
- 生成(generation)- 生成对应构建系统/生成器的构建文件
- 构建(build) – 调用构建系统构建项目
- 安装(install)- 安装生成出的目标代码
如C++的宏只能在预编译阶段处理一样,CMake的生成表达式只能在生成之后计算出对应的值。当然对于某些生成表达式,CMake允许在配置阶段计算,但能使用的命令有限制。
你可能会发现我们在第一节只解决了链接的依赖问题,解决include的依赖问题就需要用到生成表达式了。
这里我们以引用githubuser0xFFFF/Qt-Advanced-Docking-System(下文简称qtads)为例子。代码地址:OrbitZore/cmake-quickstart
去除掉引用qt库的部分,CMakeLists.txt核心如下
1 | set(BUILD_EXAMPLES OFF CACHE INTERNAL "") |
首先我们关闭qtads的样例构建(qads包含BUILD_EXAMPLES选项(option),需要新建BUILD_EXAMPLES CACHE覆盖其行为。INTERNAL表示无法被用户指定),然后使用add_subdirectory()
尝试构建qtads。查阅qtads的CMakeLists.txt我们可以发现其库目标名为qt${QT_VERSION_MAJOR}advanceddocking
1 | //files: Qt-Advanced-Docking-System/examples/hideshow/CMakeLists.txt |
然后就可以使用$<TARGET_PROPERTY:${QtADS_target},INTERFACE_INCLUDE_DIRECTORIES>
来引用Qt-Advanced-Docking-System的公开(Public)include目录了。
找包(Finding Packages)
find_package()
应该是CMake最晦涩难懂的命令了,其查找模式、查找路径的复杂程度比C++的默认include目录更甚。首先我们看到的是这样简单一行的命令
1 | find_package(MyPackage 1.2) |
然后find_package()
命令会顺序尝试三个模式
- 模块模式(Module mode)
- 配置模式(Config mode)
- FetchContent重定向模式(FetchContent redirection mode)
模块模式会顺序尝试CMAKE_MODULE_PATH
环境变量、CMake安装自带路径中查找名为FindMyPackage.cmake的文件。
配置模式会在几个地方查找名为mypackage-config.cmake或者MyPackageConfig.cmake的文件,包括MyPackage_DIR(如果该变量存在)。还会查找mypackage-config-version.cmake或者MyPackageConfigVersion.cmake来决定版本的选择。如果在命令中指定了NAMES还会顺序按着NAME作为MyPackage找。
一般安装了某个库就找到它的xxxConfig.cmake然后把路径设置为xx_DIR环境变量吧(
FetchContent重定向模式在下节讲述。
还记得qt项目都会有的find_packages()
命令吗
1 | find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED) |
对于qt5来说在archlinux中选择的是配置模式,在/usr/lib/cmake/Qt5
下存放着Qt5Config.cmake Qt5ConfigVersion.cmake Qt5ModuleLocation.cmake
。在windows中一般是配置模式,比如(Qt5.12.12版本)在C:/Qt/Qt5.12.12/5.12.12/msvc2017_64/lib/cmake/Qt5
下存放着同样的东西。
现代CMake——FetchContent命令
只需要把上述的add_subdirectory()
命令换成FetchContent
系列即可。代码地址:OrbitZore/cmake-quickstart
1 | include(FetchContent) |
这样无需thridparty
等方式就可以网络引用包了。
在官方文档中还描述了其他几种来源方式
1 | FetchContent_Declare( |