1、你好,CMake
我觉得针对这个问题最简单(但不是最正确的)的回答应该是:“CMake是服务于将源代111码转换成可执行的文件的工具”。
将源码转换为可工作应用会比较神奇。不仅是效果本身(即设计并赋予生命的工作机制),而且是将理念付诸于过程的行为本身。
CMake本身是一个工具集,由五个可执行的程序组成:cmake、ctest、cpack、cmake-gui和ccmake,其中cmake可以说是出镜率最高的明星级别程序了,它用于在构建项目的第一步,进行项目的配置、生成和构建项目的主要可执行文件的工作。其他的程序们ctest用于运行和报告测试结果的测试驱动程序,cpack用来生成安装程序和源包的打包程序,cmake-gui是 cmake 的图形界面,ccmake(注意ccmake比cmake多了一个c)也是用于图形界面,与cmake-gui不同的地方在于ccmake是基于控制台(terminal)的图形界面。
CMake设计的出发点在于面向开发者,而开发者的工作流程基本是: 设计、编码和测试;理想情况下,这个过程应该从一个简单地按钮开始。在更改了代码之后,工具将以智能的、快速的、可扩展的方式,在不同的操作系统和环境中以相同的方式工作。支持多个集成开发环境(IDE) 和持续集成(CI) 流水,这些流水在更改提交到代码库后,就能对代码进行测试。为了寻求针对上述许多此类需求的一种答案,CMake便由此孕育而生。即是Cmake是抱着如此“崇高”的初衷隆重登场,但是很多接触CMake的初学者、技术人员和Cpp开发人员对其评价很不好甚至有些嗤之以鼻,确实,毋庸置疑的一点是使用CMake语言和工具要正确配置和使用源码需要一(ju)定(da)的工作量,但造成这些“操蛋”的爆肝工作量背后,并不是因为CMake 复杂,而是因为“自动化”的需求很复杂。抛开一切,单单就论“做出一个真正模块化的、功能强大的C++ 构建应用”以满足各种需求的难度,这个应该是很难吧?但是CMake 确实做到了(doge)。
两个概念需要区分:<build tree> 和<source tree>,分别对应构建树和源码树;构建树是目标/输出目录的路径,源码树是源码所在的路径。
构建软件是一个通用的过程:编译可执行程序和库、管理依赖关系、测试、安装、打包、生成文档和测试更多功能,当然了上述其中有一些步骤是可以跳过的,但至少我们需要使用CMake完成编译可执行程序。目前,CMake 的开发很活跃,并已成为C 和C++ 开发人员的行业标准。以自动化的方式构建代码的问题比CMake 出现的要早得多,所以会有很多选择:Make、Autotools、SCons、Ninja、Premake 等。但为什么CMake 可以后来居上呢?关于CMake,Rafał Świdziński持有以下几个重要观点:
• 专注于支持现代编译器和工具链。
• CMake 是真正的跨平台——支持Windows、Linux、macOS 和Cygwin 的构建。
• 为主流IDE 生成项目文件:Microsoft Visual Studio, Xcode 和Eclipse CDT。此外,也是其他项目的模型,如CLion。
• CMake 操作在合适的抽象级别上——允许将文件分组到可重用的目标和项目中。
• 有很多用CMake 构建的项目,其提供了一种简单的方法将它们包含到自己的项目中。
• CMake 将测试、打包和安装视为构建过程的固有组成。
• 弃用旧的、未使用的特性,从而保持CMake 的精简。
CMake 提供了统一的、流线型的体验。不管是在IDE 中构建,还是直接从命令行构建,还照顾到构建后阶段。即使前面所有的环境都不同,持续集成/持续部署(CI/CD) 流水也可以轻松地使用相同的CMake 配置,并使用单一标准构建项目。
表面上可以感受到的工作流程:“CMake 是在一端读取源代码,在另一端生成二进制文件的工具”。但正如上文所说CMake是一个工具集,那就说明了CMake 自己并没有构建任何东西的能力,CMake它依赖于系统中的其他工具来执行实际的编译、链接和其他任务。CMake好似一个在构建过程中工作的“协调器”,它清楚哪些步骤需要完成,理解最终目标是什么,以及忙碌于为构建工作找到合适的“工人”和“材料”。综上,这个过程有三个阶段:配置、生成、构建阶段,可见图2中的表示。
图2:CMake在配置、生成和构建阶段的示意图
普通变量、缓存变量、环境变量
普通变量、缓存变量和环境变量这三类变量组成了CMake变量这一个“复杂”的主题,让人头疼的一点在于上述三个变量在不同的作用域中的“被使用和修改”,而且CMake作用域之间变量如何影响的“特定规则”也会经常在CMake变量的使用过程中体现。
基本的变量操作指令是set()\unset(),变量名区分大小写并可以包含字符(使用括号和引号参数允许在变量名中包含空格。但当以后引用时,必须使用反斜杠来转义空格(\),因此,建议在变量名中只使用字母数字字符、减号(-) 和下划线(_))。具体的使用方式为在设置变量时只需使用set()并提供名称和值,要取消变量的设置时可以使用unset()并提供名称。
set(MyString1 "Text1")
set([[My String2]] "Text2")
set("My String 3" "Text3")
message(${MyString1})
message(${My\ String2})
message(${My\ String\ 3})
unset(MyString1)
由上面示例可以看到,对已定义变量的引用需要使用${} 语法,e.g. message(${MyString1}),其中message是用以构建过程中的打印,通过${}告诉CMake遍历作用域堆栈,尝试将${MyString1}替换为具体的值供message命令打印出来。值得注意的是在查询${MyString1}过程中,CMake若是没有找到对应的变量则会将其替换为空字符串并不会产生错误。另外,在通过${} 语法进行变量的求值和展开时,是由内而外执行的。
考虑包含以下变量的例子:
• MyInner 的值是Hello
• MyOuter 的值是${My
若使用message(”${MyOuter}Inner} World”),输出将是Hello World,这是因为${My 替换了${MyOuter},当与顶层值Inner} 结合时,会创建另一个变量引用${MyInner}。
当涉及到变量类别时,变量引用的工作方式有点奇怪。以下是通常情况适用的方式:
• ${} 用于引用普通变量或缓存变量。
• $ENV{} 用于引用环境变量。
• $CACHE{} 用于引用缓存变量。
首先说明如何修改或创建一个环境变量,使用set(ENV{<variable>} <value>) 指令用以声明,使用unset(ENV{<variable>})来清除某一个环境变量,其中ENV表示环境变量标志性前缀,variable指变量名称,value则为变量值,需要注意的是设定或读取环境变量时,都通过ENV前缀来访问环境变量,读取环境变量值时,要在ENV前加$符号;但if判断是否定义时,不用加$符号。具体示例如下:
//示例1:
set(ENV{CXX} "clang++")
unset(ENV{VERBOSE})
//示例2:
set(ENV{CMAKE_PATH} "myown/path/example")
# 判断CMAKE_PATH环境变量是否定义
if(DEFINED ENV{CMAKE_PATH}) //注意此处ENV前没有$符号
message("CMAKE_PATH_1: $ENV{CMAKE_PATH}") //注意此处ENV前有$符号
else()
message("NOT DEFINED CMAKE_PATH VARIABLES")
endif()
设定环境变量后,其作用域只影响当前CMake进程,也就是说环境变量设定后是整个CMake进程的作用域都可用,但是不会影响CMake进程外的整个系统环境。
另一个需要注意的点在于,环境变量在启动CMake进程后会基于CMake在配置阶段中收集的信息在CMake生成阶段生成环境变量的副本,该副本会在单一的、全局的作用域中可用。即,若使用ENV 变量作为指令的参数,这些值将在CMake生成构建系统期间(配置阶段+生成阶段)插入,并且会将其嵌入到构建树中,在构建系统完成后即使再通过脚本或者命令行修改环境变量ENV{<variable>}的value,在构建阶段时该环境变量值也不会更新成新的value(因为在构建系统中保存的是之前环境变量的副本),具体实例如下:
//示例3:
//CMakeLists.txt:
cmake_minimum_required(VERSION 3.20.0)
project(Environment)
//在配置期间打印myenv环境变量
message("generated with " $ENV{myenv})
//在构建阶段过程中打印相同的变量
add_custom_target(EchoEnv ALL COMMAND echo "myenv in build
is" $ENV{myenv})
在上述示例3的CMakeLists.txt中是有两个展示阶段:第一将在配置期间打印myenv环境变量并通过add_custom_target() 添加一个构建阶段,第二将在构建阶段过程中打印相同的变量。构建上述CMakeLists.txt通过一个bash脚本文件执行,见下:
//示例4:
//bash脚本:
//先定义myenv环境变量,并打印
export myenv=first
echo myenv is now $myenv
// 基于CMakeList.txt生成一个构建系统
cmake -B build .
cd build
//基修改myenv环境变量,并打印
export myenv=second
echo myenv is now $myenv
//开始构建
cmake --build .
运行上面的代码,可以清楚地看到在配置过程中,设置的值会保留在生成的构建系统中:
1. $ ./build.sh | grep -v "\-\-"
2. myenv is now first
3. generated with first
4. myenv is now second
5. Scanning dependencies of target EchoEnv
6. myenv in build is first
7. Built target EchoEnv
缓存变量可以通过$CACHE{<name>} 语法来引用,而设置一个缓存变量使用set(<variable> <value> CACHE <type> <docstring> [FORCE])指令,与用于普通变量的set() 指令相比,缓存变量的设定中有一些必需参数和关键字(CACHE &FORCE)。与环境变量不同的是,缓存变量是CMake进程在配置阶段收集相关信息后存储在在构建树中的CMakeCache.txt 文件中的变量,缓存变量不可像环境变量中在脚本使用但是可以通过cmake-gui或者ccmake来声明。
Cache Variable缓存变量相当于一个全局变量,在同一个CMake工程中任何地方都可以使用,比如父目录,子目录等,而如上文中缓存变量的指令格式是set(<variable> <value>... CACHE <type> <docstring> [FORCE])
# variable:变量名称
# value:变量值列表
# CACHE:cache变量的标志
# type:变量类型,取决于变量的值。类型为:BOOL、FILEPATH、PATH、STRING、INTERNAL
# docstring:必须是字符串,作为变量概要说明
# FORCE:强制选项,强制修改变量值
其中FORCE选项,在定义缓存变量时不加也能定义成功,但是修改时不加FORCE选项则修改无效,所以不论定义或修改缓存变量时,建议都加上FORCE选项,具体实例如下:
//设置一个string类型的缓存变量,名称为FOO,变量值为BAR
set(FOO "BAR" CACHE STRING "interesting value" FORCE)
//设置一个string类型的缓存变量,名称为CMAKE_BUILD_TYPE,变量值为Release
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
• 函数作用域: 用于执行用function() 定义的自定义函数 • 目录作用域: 当从add_subdirectory() 指令执行嵌套目录中的CMakeLists.txt 文件
如果对C/C++比较熟悉的话,CMake中变量的作用域就可以类比C/C++中的一些定义,举例来说,(1)、CMake中变量的函数作用域可类比成C/C++中函数的参数值传递(不是引用、也不是指针),也就是说在一般情况下CMake函数中对变量的修改不会影响到函数外的CMake变量value值,而CMake函数中的变量就是从parent scope中“查询”到并生成的副本;(2)、CMake中变量的目录作用域,也是类似于C/C++中的include文件依赖,也就是在子目录下的对变量的修改不会影响父目录中变量定义的value值;(3)、话已至此,不妨再类比一下CMake中的普通变量、缓存变量和环境变量,CMake普通变量就好比C/C++中的普通变量定义。都是作用在定义变量时所在的作用域(scope)之下;(4)、CMake缓存变量有些像C/C++中的指针,只是CMake中引用缓存变量的value值时不用像C/C++那样加一个“取地址符”,而且这个缓存变量(“指针”)是不对外部可见的(不能通过命令行修改和引用CMake缓存变量),如果想要CMake中修改后的缓存变量value值生效就必须加上FORCE关键字;(5)、CMake的环境变量就好比C/C++中的宏定义了,不仅对外部可见,同时CMake中还支持对环境变量(“宏”)的修改。
回归本质,CMake变量作用域作为一个通用概念是为了分离不同的抽象层,以便在调用用户定义的函数时,该函数中设置的变量是局部的,这些局部变量不会影响全局作用域,即使局部变量的名称与全局变量的名称完全相同。若显式需要,函数也应该具有对全局变量的读/写访问权。这种变量(或作用域) 分离必须在多个层面上工作——当一个函数调用另一个函数时,分离规则同样适用。针对变量的作用域,理解“副本“的概念是关键,当创建嵌套(子)作用域时,CMake只需用来自当前(父)作用域的所有变量的副本填充,后续嵌套(子)作用域命令将影响这些副本。但若完成了嵌套(子)作用域的执行,所有的副本都会删除,而原始的父作用域将恢复,嵌套作用域中操作的变量将不会更新到父作用域中。
接下来再根据CMake Documentation中的定义,感受一下CMake变量作用域的具体说明。第一,针对函数作用域(Function Scope):
A variable “set” or “unset” binds in this scope and is visible for the current function and any nested calls within it, but not after the function returns.---from cmake language
举个例子,当在函数内通过set()或unset()将变量”v”与当前函数作用域绑定时,变量”v”的新值仅在函数作用域内有效,出了这个作用域,如果这个作用域外也有同名的变量”v”,那么使用将是域外同名变量”v”的值。第二,针对目录作用域(Directory Scope):
Each of the Directories in a source tree has its own variable bindings. Before processing the CMakeLists.txt file for a directory, CMake copies all variable bindings currently defined in the parent directory, if any, to initialize the new directory scope. .---from cmake language
目录作用域的启用一般是在父目录下的CmakeList.txt中有add_subdirectory(“子目录路径”)指令,而在子目录的CMakeLists.txt会将父目录的所有变量拷贝到当前CMakeLists.txt中,当前CMakeLists.txt中的变量的作用域仅在当前子目录有效。
综上,不管是针对CMake函数作用域还是CMake目录作用域,其都有两个特点:向下有效和数值拷贝生成副本,在不使用特殊关键字的情况下,嵌套(子)作用域针对普通变量的修改不会影响到父作用域。针对变量,普通变量仅仅有效于当前作用域,而缓存变量和环境变量可以在全局作用域中使用。
三类控制结构:条件块、循环、定义指令 没有控制结构,CMake 语言就不完整
CMake中的控制结构就是提供一个结构,让用户可以针对具体的情况来设置触发条件<condition> 表达式来控制需要执行的命令语言。在所有的控制结构中一般都是需要提供条件判断<condition> 表达式的,在if()、elseif()和while()的条件判断表达式的语法都是相同的。这些<condition> 表达式都是根据非常简单的语法求值,如逻辑运算、字符串和变量的求值、比较、CMake检查等,本文中不对上述的语法做详细的展开,但提醒一下条件<condition> 表达式中的“字符串和变量的求值”的语法中,需要注意求值时加引用符
CMake中的条件块是一个必须以if()开头并以endif()结尾的块状结构,在开头的if()和结尾的endif()之间可以添加任意数量的elseif(),但只能有单独一个的、可选的else(),其CMake条件块的结构命令如下:
if(<condition>)
<commands>
elseif(<condition>) # optional block, can be repeated
<commands>
else() # optional block
<commands>
endif()
在具体的条件判断流程中,若满足if() 指令中指定的<condition> 表达式,则执行第一部分的<commands>,如上例第2行部分,否则,CMake 将在属于该块中满足条件的第一个elseif() 指令节中执行命令。若没有这样的命令,CMake将检查是否提供了else(),并执行该部分代码中的指令,如上例中第6行的部分。若以上条件都不满足,则不会触发条件块中任何的指令,并在endif() 之后继续执行。
CMake中的循环控制块是一个必须以while()开头创建并以endwhile()结尾的块状结构,只要while() 中提供的<condition> 表达式为true,其后续的指令都会执行,其CMake循环块的结构命令如下:
while(<condition>)
<commands>
endwhile()
除了while()循环结构外,还有一个相对更加常用和简介的循环结构块:foreach()。上文中while()循环块是需要具体的、额外的<condition> 表达式来控制需要执行的命令语言,而foreach()循环则是类似C/C++的for循环风格来控制的,只是foreach块的打开和关闭指令分别是foreach() 和endforeach(),其定义如下所示:
foreach(<loop_var> RANGE <min> <max> [<step>])
<commands>
endforeach()
上述中的<min>和<step>参数变量可选择配置,默认的话从0开始,min和max都必须是非负整数,在RANGE中max和min都是包括在循环内部的。如果设置了min的value值,则必须小于max的value值。
上文中提到foreach()是相对while()而言,在CMake中更加常用和简介的循环结构块,这个是因为foreach()在处理列表变量时十分便捷:
foreach(<loop_variable> IN [LISTS <lists>] [ITEMS <items>])
CMake 将从所有提供的<lists> 列表变量中获取元素,也就是输入循环中的list可以是多个,然后再是从所有显式声明的<items>中获取元素值,并将它们都存储在<loop_variable> 中,对每个项逐个执行<commands>。可以选择只提供列表,只提供值或者两者都提供,见下例:
set(MY_LIST 1 2 3)
foreach(VAR IN LISTS MY_LIST ITEMS e f)
message(${VAR})
endforeach()
上述示例中是声明了MY_LIST的列表变量为【1,2,3】,在foreach循环中会获取MY_LIST中的所有元素和<items>中的e、f值,存储在VAR中,在每一次循环中命令指令就是打印VAR的数值,上述代码的打印结果见下:
还是以上述foreach使用为例,foreach中还可以优化成一个更简化的指令行并获取相同的结果:
foreach(VAR 1 2 3 e f)
除此以外,从3.17 版本开始,foreach() 已经学会了如何压缩列表(ZIP_LISTS),
foreach(<loop_var>... IN ZIP_LISTS <lists>)
在压缩列表中CMake 将为每个提供的列表创建一个num_<N> 变量,用每个列表中的项填充该变量。同时,除了使用CMake自动创建的num_<N> 变量,用户也可以自定义传递多个<loop_var> 变量名(每个列表一个),每个列表将使用单独的变量来存储,详细见下:
//声明两个具有相同数量元素的list
set(L1 "one;two;three;four")
set(L2 "1;2;3;4;5")
//通过ZIP_LISTS关键子来“压缩“上面的两个列表,以在单次循环中处理相同索引的列表元素
// 示例1:通过num_<N> 变量存储获取元素
foreach(num IN ZIP_LISTS L1 L2)
message("num_0=${num_0}, num_1=${num_1}")
endforeach()
// 示例2:通过自定义传递两个<loop_var>变量,存储获取元素
foreach(word num IN ZIP_LISTS L1 L2)
message("word=${word}, num=${num}")
endforeach()
上面针对多个列表的压缩处理,前提条件是这些待处理的多个列表中的元素个数是相同的,若列表之间的项数不同,CMake 将不会为较短的列表定义变量。
除了CMake官方提供和定义的一些指令以外,CMake还提供了用户进行自定义指令的方法:定义指令,CMake中的定义指令通过两种方法实现:macro()和function(),在这里还是可以将CMake中的定义指令macro()和function()的实现与C风格的宏定义和C++的函数定义比较:
• macro() 的工作方式像是查找和替换指令,而不是像function() 这样的实际子例程调用。与函数相反,宏不会在调用堆栈上创建单独的条目。所以宏中调用return() 将比在函数中返回调用语句的级别高一级(若已经在顶层作用域中,可能会终止执行)。
• function() 为本地变量创建一个单独的作用域,这与macro() 命令不同,后者在调用者的变量作用域中工作,所以使用CMake的function需要注意变量的作用域问题。
CMake中macro()和function()具体使用方法还是配合下面的示例进行说明。
||宏
//CMake中的宏
macro(<name> [<argument>])
<commands>
endmacro()
完成CMake宏的声明之后就可以通过调用宏的名称<name>来执行宏(函数调用不区分大小写),下例将重点强调宏中变量作用域相关的问题:
//定义了一个名为MyMacro的宏,参数为myVar
macro(MyMacro myVar)
set(myVar "new value")
message("argument: ${myVar}")
endmacro()
set(myVar "first value")
message("myVar is now: ${myVar}")
//调用宏
MyMacro("called value")
message("myVar is now: ${myVar}")
若是运行上面的CMake配置,则可以得到如下的输出:
myVar is now: first value
argument: called value
myVar is now: new value
上例中尽管调用MyMacro尝试显式地将myVar 设置为“new value”,但后续message打印的${myVar}并不是“new value”,而是在第10行中传递给宏的参数${"called value"},也就是宏中对全局作用域中的myVar 变量的修改,并不影响宏中message(”argument:${myVar}”),这是因为传递给宏的参数没有视为真正的变量,而是作为常量查找并替换指令。所以宏MyMacro中对全局作用域中的myVar 变量的修改行为,是一种副作用!上述的例子是CMake不提倡的一种实践方式,因为一旦变量作用域和宏作为“查找和替换指令”的行为未被正确使用,就会产生难以描述的负面影响。
具体宏与函数的差异,可以往下阅读以完成概率和使用的对比理解。
||函数:
//CMake中的函数声明
function(<name> [<argument>])
<commands>
endfunction()
还是使用一个经典的CMake函数的使用示例来进行详细说明:
//定义了一个名为MyFunction的函数,参数为FirstArg
function(MyFunction FirstArg)
message("Function: ${CMAKE_CURRENT_FUNCTION}")
message("File: ${CMAKE_CURRENT_FUNCTION_LIST_FILE}")
message("FirstArg: ${FirstArg}")
set(FirstArg "new value")
message("FirstArg again: ${FirstArg}")
message("ARGV0: ${ARGV0} ARGV1: ${ARGV1} ARGC: ${ARGC}")
endfunction()
set(FirstArg "first value")
//调用函数,并传参个数比函数声明时的多了一个
MyFunction("Value1" "Value2")
message("FirstArg in global scope: ${FirstArg}"))
示例中的CMAKE_CURRENT_FUNCTION、CMAKE_CURRENT_FUNCTION_LIST_DIR、CMAKE_CURRENT_FUNCTION_LIST_FILE和CMAKE_CURRENT_FUNCTION_LIST_LINE是CMake从3.17版本后为每个函数设置的官方变量,而同时CMake官方也定义了一些引用来访问命令调用中传递的参数, ${ARGC}输出参数的数量、${ARGV}输出所有参数的列表、${ARG0}, ${ARG1}, ${ARG2}输出特定索引处的实参值、${ARGN}输出最后一个预期参数之后由调用者传递的匿名参数列表。若是运行上面的CMake配置,则可以得到如下的输出:
Function: MyFunction
File: /root/examples/chapter02/08-definitions/function.cmake
FirstArg: Value1
FirstArg again: new value
ARGV0: Value1 ARGV1: Value2 ARGC: 2
FirstArg in global scope: first value
由上例我们可以得到两个重要的事实:第一,函数中对全局变量的修改只停留在函数作用域中,在函数结束后不会影响到父作用域中的变量value值。第二,传递给函数的实参值被真正使用在了函数的作用域内,在第13行调用函数MyFunction并传入Value1(Value2是“多余”的匿名实参值),而后在函数内打印message("FirstArg: ${FirstArg}")输出的是“Value1”,随后set(FirstArg "new value")再打印输出的是修改后的“new value”,结束函数后回到全局作用域打印变量输出的是第11行第一次声明的“first value”,如果是宏则会在最终输出“new value”了。
综上,CMake中的宏macro()和函数function()都是提供给用户以自定义指令的方法,只不过,CMake函数function()开放了自己的作用域(function scope),并可以在其作用域内安全的调用set()指令以提供函数的一个命名参数,任何更改都将是函数的局部更改(除非指定了PARENT_SCOPE),不影响PARENT SCOPE。
CMake中打印指令,也就是message() 指令是用于将文本打印到标准输出,并且CMake通过提供MODE 参数,可以自定义输出的样式,并且在出现错误的情况下,可以停止代码:message(<mode> ”text”) 的执行,默认的MODE是“STATUS”,其他的可选MODE模式如下:
• FATAL_ERROR: 将停止处理和生成。
举个简单例子,使用FATAL_ERROR的模式,在CMake中只打印第一条消息,然后就停止执行:
message(FATAL_ERROR "First Message Printed")
message("Won't print this.")
• SEND_ERROR: 将继续处理,但跳过生成。
• WARNING: 继续处理。
• AUTHOR_WARNING: CMake 警告。继续处理。
• DEPRECATION: 若启用了CMAKE_ERROR_DEPRECATED 或
CMAKE_WARN_DEPRECATED 变量,将做出相应处理。
• NOTICE 或省略模式(默认): 将向stderr 输出一条消息,以吸引用户的注意。
• STATUS: 将继续处理,建议用于用户的主要消息。
• VERBOSE: 将继续处理,用于通常不是很有必要的更详细的信息。
• DEBUG: 将继续处理,并包含在项目出现问题时可能有用的详细信息。
• TRACE: 将继续处理,并建议在项目开发期间打印消息。通常,在发布项目之前,将这些类型的消息删除。
Modern CMake中重要的一个模块就是引用官方和CMake社区中已经配置好了的CMake模板,所谓的CMake模板就是将CMake代码划分到单独的.cmake文件中,以保持内容的有序和独立性。然后通过include()指令,从父列表文件引用:
include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>])
若提供文件名(一个扩展名为.cmake),CMake 将尝试打开并执行它。这里不会创建嵌套的、单独的作用域,因此对该文件中变量的修改会影响调用作用域。若文件不存在,CMake 将抛出一个错误,除非用optional 关键字指定为可选。若需要知道include() 指令操作是否成功,可以提供一个带有变量名的RESULT_VARIABLE 关键字,若include()引用成功,则用包含的文件的完整路径填充,失败则用未找到(NOTFOUND) 填充。
脚本模式下运行时,将从当前工作目录解析相对路径。要强制搜索与脚本本身相关的内容,请提供绝对路径:
include("${CMAKE_CURRENT_LIST_DIR}/<filename>.cmake")
若不提供路径,但提供了模块的名称(没有.cmake 或其他),CMake 将尝试找到一个模块并包含它。然后,CMake 将在CMake 模块目录CMAKE_MODULE_PATH 中,搜索名称为<module>.cmake的文件。
为了可以知道CMake 脚本可以做什么,CMake提供了一个可以快速浏览文件的操作命令:
file(READ <filename> <out-var> [...])
file({WRITE | APPEND} <filename> <content>...)
file(DOWNLOAD <url> [<file>] [...])
简而言之,file() 指令会以一种与系统无关的方式读取、写入和传输文件,并使用文件系统、文件锁、路径和存档。详情请参阅附录部分。
除了CMake官方和自定义的指令外,有时需要使用系统中可用的工具(毕竟,CMake 主要是一个构建系统生成器),CMake 为此提供了execute_process()指令以用来运行其他进程,并收集它们的输出。这个命令非常适合脚本,也可以在配置阶段的项目中使用。下面是命令的一般形式:
execute_process(COMMAND <cmd1> [<arguments>] [OPTIONS])
CMake 将使用操作系统的API 来创建子进程(因此,诸如&&、|| 和> 等shell 操作符将不起作用)。可以通过不止一次地提供COMMAND <cmd> <arguments> 参数来连接命令,并将一个命令的输出传递给另一个命令。
若进程没有在要求的限制内完成任务,可以选择使用TIMEOUT <seconds> 参数来终止进程,并且可以根据需要设置WORKING_DIRECTORY <directory>。通过RESULTS_VARIABLE <variable> 参数,可以在列表中收集所有任务的退出代码。若只对最后执行命令的结果感兴趣,请使用单数形式:RESULT_VARIABLE <variable>。
为了收集输出,CMake 提供了两个参数:OUTPUT_VARIABLE 和ERROR_VARIABLE(以类似的方式使用)。若想合并stdout 和stderr,请对两个参数使用相同的变量。
本章中参考的源代码可以从GitHub中获取,网址为https://github.com/dev-cafe/cmake-cookbook。开源代码遵循MIT许可:只要原始版权和许可声明包含在软件/源代码的任何副本中,可以以任何方式重用和重新混合代码。许可的全文可以在https://opensource.org/licenses/MIT 中看到。
本节示例中将演示如何运行CMake配置和构建一个简单的项目,该项目由单个源文件组成,用于生成可执行文件。我们将用C++讨论这个项目。
将以下源代码编译为单个可执行文件:
#include <cstdlib>
#include <iostream>
#include <string>
std::string say_hello() { return std::string("Hello, CMake world!"); }
int main() {
std::cout << say_hello() << std::endl;
return EXIT_SUCCESS;
}
对应的CMakeLists.txt配置及注释如下:
完成对CMakeLists.txt配置后,可以通过创建build目录,在build目录下来配置项目:
mkdir -p build
cd build
cmake ..
-- The CXX compiler identification is GNU 8.1.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/cmake-cookbook/chapter-01/recipe-01/cxx-example/build
cmake --build .
Scanning dependencies of target hello-world
[ 50%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o
[100%] Linking CXX executable hello-world
[100%] Built target hello-world
3.2 简单构建和链接库文件:静态库和动态库
项目中会有单个源文件构建的多个可执行文件的可能,而且项目中的多个源文件,通常分布在不同子目录中,本小节的实践有助于项目的源代码结构的模块化、代码重用和关注点分离(这些都是Modern CMake中设计时的重要内容)。同时,这种分离可以简化并加速项目的重新编译。本示例中,我们将展示如何将源代码编译到库中,以及如何链接这些库。
回看第一个例子,这里并不再为可执行文件提供单个源文件,我们现在将引入一个类,用来包装要打印到屏幕上的消息。更新一下的hello-world.cpp:
#include "Message.hpp"
#include <cstdlib>
#include <iostream>
int main() {
Message say_hello("Hello, CMake World!");
std::cout << say_hello << std::endl;
Message say_goodbye("Goodbye, CMake World");
std::cout << say_goodbye << std::endl;
return EXIT_SUCCESS;
}
Message类包装了一个字符串,并提供重载过的<<操作,并且包括两个源码文件:Message.hpp头文件与Message.cpp源文件。Message.hpp中的接口包含以下内容:
#pragma once
#include <iosfwd>
#include <string>
class Message {
public:
Message(const std::string &m) : message_(m) {}
friend std::ostream &operator<<(std::ostream &os, Message &obj) {
return obj.printObject(os);
}
private:
std::string message_;
std::ostream &printObject(std::ostream &os);
};
Message.cpp实现如下:
#include "Message.hpp"
#include <iostream>
#include <string>
std::ostream &Message::printObject(std::ostream &os) {
os << "This is my very nice message: " << std::endl;
os << message_;
return os;
}
这里有两个文件需要编译,所以CMakeLists.txt必须进行修改。本例中,先把它们编译成一个库,而不是直接编译成可执行文件,具体实施和注释如下所示:
本小节中引入了两个新命令:add_library 和 target_link_libraries:
编译成功后,构建目录包含libmessage.a一个静态库(在GNU/Linux上)和hello-world可执行文件。此外,CMake还接受其他值作为add_library的第二个参数的有效值:
• STATIC:用于创建静态库,即编译文件的打包存档,以便在链接其他目标时使用,例如:可执行文件。
• SHARED:用于创建动态库,即可以动态链接,并在运行时加载的库。可以在CMakeLists.txt中使用add_library(message SHARED Message.hpp Message.cpp)从静态库切换到动态共享对象(DSO)。
• OBJECT:可将给定add_library的列表中的源码编译到目标文件,不将它们归档到静态库中,也不能将它们链接到共享对象中。如果需要一次性创建静态库和动态库,那么使用对象库尤其有用。
• MODULE:又为DSO组。与SHARED库不同,它们不链接到项目中的任何目标,不过可以进行动态加载。该参数可以用于构建运行时插件。
目前为止,看到的示例比较简单,CMake执行流是线性的:从一组源文件到单个可执行文件,也可以生成静态库或动态库。为了确保完全控制构建项目、配置、编译和链接所涉及的所有步骤的执行流,CMake提供了自己的语言。本节中,将探索条件结构if-else- else-endif的使用,修改后的CMakeLists.txt和相关注释如下所示:
前面3.3的配置中引入了条件句:通过硬编码的方式给定逻辑变量值。不过,这会影响用户修改这些变量。CMake代码没有向读者传达,该值可以从外部进行修改。推荐在CMakeLists.txt中使用option()命令,以选项的形式显示逻辑开关,用于外部设置,从而切换构建系统的生成行为。本节的示例将展示如何使用这个命令:
完成了上述的CmakeLIst.txt文件修改后,可以通过CMake的-D 的CLI选项将信息传递给CMake来切换库的行为:
$ mkdir -p build
$ cd build
$ cmake -D USE_LIBRARY=ON ..
-- ...
-- Compile sources into a library? ON
-- ...
$ cmake --build .
Scanning dependencies of target message
[ 25%] Building CXX object CMakeFiles/message.dir/Message.cpp.o
[ 50%] Linking CXX static library libmessage.a
[ 50%] Built target message
Scanning dependencies of target hello-world
[ 75%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o
[100%] Linking CXX executable hello-world
[100%] Built target hello-world
CMake可以根据平台和生成器选择编译器,还能将编译器标志设置为默认值。然而通常应该控制编译器的选择。本小节将考虑构建类型的选择,并展示如何控制编译器标志:
CMake可以配置构建类型,例如:Debug、Release等。配置时,可以为Debug或Release构建设置相关的选项或属性,例如:编译器和链接器标志。控制生成构建系统使用的配置变量是CMAKE_BUILD_TYPE。该变量默认为空,CMake识别的值为:
• Debug:用于在没有优化的情况下,使用带有调试符号构建库或可执行文件。
• Release:用于构建的优化的库或可执行文件,不包含调试符号。
• RelWithDebInfo:用于构建较少的优化库或可执行文件,包含调试符号。
• MinSizeRel:用于不增加目标代码大小的优化方式,来构建库或可执行文件。
具体的CMakeLists.txt配置及注释如下:
随后验证CMake的输出,如下:
# $ mkdir -p build
# $ cd build
# $ cmake ..
# ...
# -- Build type: Release
# -- C flags, Debug configuration: -g
# -- C flags, Release configuration: -O3 -DNDEBUG
# -- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG
# -- C flags, minimal Release configuration: -Os -DNDEBUG
# -- C++ flags, Debug configuration: -g
# -- C++ flags, Release configuration: -O3 -DNDEBUG
# -- C++ flags, Release configuration with Debug info: -O2 -g -DNDEBUG
# -- C++ flags, minimal Release configuration: -Os -DNDEBUG
# $ cmake -D CMAKE_BUILD_TYPE=Debug ..
# -- Build type: Debug
# -- C flags, Debug configuration: -g
# -- C flags, Release configuration: -O3 -DNDEBUG
# -- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG
# -- C flags, minimal Release configuration: -Os -DNDEBUG
# -- C++ flags, Debug configuration: -g
# -- C++ flags, Release configuration: -O3 -DNDEBUG
# -- C++ flags, Release configuration with Debug info: -O2 -g -DNDEBUG
# -- C++ flags, minimal Release configuration: -Os -DNDEBUG
不同配置使用了哪些标志,这主要取决于选择的编译器。需要在运行CMake时显式地打印标志,也可以仔细阅读运行CMake --system-information的输出,以了解当前平台、默认编译器和语言的默认组合是什么。
前面的示例展示了如何探测CMake,从而获得关于编译器的信息,以及如何切换项目中的编译器。后一个任务是控制项目的编译器标志。CMake为调整或扩展编译器标志提供了很大的灵活性,可以选择下面两种方法:
• CMake将编译选项视为目标属性。因此,可以根据每个目标设置编译选项,而不需要覆盖CMake默认值。
•可以使用-D 的CLI标志直接修改CMAKE_<LANG>_FLAGS_<CONFIG>变量。这将影响项目中的所有目标,并覆盖或扩展CMake默认值。
本示例中将展示这两种方法,具体的代码示例可见https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-08 :
本例中,警告标志有-Wall、-Wextra和-Wpedantic,将这些标示添加到geometry目标的编译选项中,其中compute-areas和 geometry的目标都将使用-fPIC标志。CMake的编译选项可以添加三个级别的可见性INTERFACE、PUBLIC和PRIVATE,具体的定义和设计区分如下:。
• PRIVATE,编译选项会应用于给定的目标,不会传递给与目标相关的目标。
# 示例中, 即使compute-areas将链接到geometry库,compute-areas也不会继承geometry目标上设置的编译器选项。
• INTERFACE,给定的编译选项将只应用于指定目标,并传递给与目标相关的目标。
• PUBLIC,编译选项将应用于指定目标和使用它的目标。
CMake通过环境变量VERBOSE,传递给本地构建工具,用以通过本地构建日志验证这些标志是否按照我们的意图正确使用,下面的示例中会设置环境变量VERBOSE=1:
# $ mkdir -p build
# $ cd build
# $ cmake ..
# $ cmake --build . -- VERBOSE=1
# ... lots of output ...
# [ 14%] Building CXX object CMakeFiles/geometry.dir/geometry_circle.cpp.o
# /usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_circle.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_circle.cpp
# [ 28%] Building CXX object CMakeFiles/geometry.dir/geometry_polygon.cpp.o
# /usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_polygon.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_polygon.cpp
# [ 42%] Building CXX object CMakeFiles/geometry.dir/geometry_rhombus.cpp.o
# /usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_rhombus.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_rhombus.cpp
# [ 57%] Building CXX object CMakeFiles/geometry.dir/geometry_square.cpp.o
# /usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_square.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_square.cpp
# ... more output ...
# [ 85%] Building CXX object CMakeFiles/compute-areas.dir/compute-areas.cpp.o
# /usr/bin/c++ -fPIC -o CMakeFiles/compute-areas.dir/compute-areas.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/compute-areas.cpp
# ... more output ...
编程语言有不同的标准,即提供改进的语言版本。启用新标准是通过设置适当的编译器标志来实现的。前面的示例中,我们已经展示了如何为每个目标或全局进行配置。3.1版本中,CMake引入了一个独立于平台和编译器的机制,用于为C++和C设置语言标准:为目标设置<LANG>_STANDARD属性,本小节中的示例可见:https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-09,下面将展示具体的实施的CMake配置和相关注释:
需要注意的一点是,如果语言标准是所有目标共享的全局属性,可以在全局的CMakeList.txt中的开始就将
• CMAKE_<LANG>_STANDARD
• CMAKE_<LANG>_EXTENSIONS、
• CMAKE_<LANG>_STANDARD_REQUIRED
变量设置为相应的值。做了语言标准的声明后,CMakeList.txt中所有目标上的对应属性都将使用这些设置,如:
本章前面的示例中,已经使用过if-else-endif的条件控制块,在本文中的第二章还介绍了CMake提供的创建循环的语言工具:foreach endforeach和while-endwhile。两者都可以与break结合使用,以便尽早从循环中跳出。本示例将展示如何使用foreach,来循环源文件列表。示例中将应用这样的循环,在引入新目标的前提下,来为一组源文件进行优化降级:
最后,在回顾第二章中的循环语句的使用方式,以foreach()为例,其有四种使用方式:
1. foreach(loop_var arg1 arg2 ...):其中提供循环变量和显式项列表,以上例中的列表变量为例,当为sources_with_lower_optimization中的项打印编译器标志集时,使用此表单。注意,如果项目列表位于变量中,则必须显式展开它;也就是说,${sources_with_lower_optimization} 必须作为参数传递。
e.g.:foreach(_source ${sources_with_lower_optimization}),foreach(p LIB BIN INCLUDE CMAKE)
2. 通过指定一个范围,可以对整数进行循环,例如:foreach(loop_var range total)或foreach(loop_var range start stop [step])。
3. 对列表值变量的循环,例如:foreach(loop_var IN LISTS [list1[...]]) 。参数解释为列表,其内容就会自动展开。
4. 对变量的循环,例如:foreach(loop_var IN ITEMS [item1 [...]])。参数的内容没有展开。
【1】Rafał Świdziński,《Modern CMake for C++》
【2】CMake Documentation,https://cmake.org/cmake/help/latest/
【3】CMake Cookbook,https://www.bookstack.cn/read/CMake-Cookbook/README.md
【4】cmake常用指令入门指南,https://www.cnblogs.com/yinheyi/p/14968494.html
本文仅做学术分享,如有侵权,请联系删文。