CMake入门笔记系列(一):CMake编译过程详解 | Micro CMake for C++
本文首发于3d视觉工坊,由于本人是作者故而再发布在知乎账号上作为备份整理
前言
在开始学习CMake的时候,大家脑海里往往都会有这么几个问题: 为什么要学CMake?CMake有什么平替吗?CMake该如何入门?,等。 而针对诸如此类问题的回答也通常存在两个版本,通俗版本是:“我是被逼的!我也没得选,谁让大家都用它呢”,而较为官方的版本是:“CMake目前仍是C/C++开发人员的行业标准,同时CMake本身也还在发展(慢且持续地提升),近期也没听说CMake要被谁给取代”。
回想起本人接触CMake的历程,在学习的过程中是走了很多弯路的。 以我自身经历为例,本人最开始是通过ROS wiki上的CMakeList.txt示例学习的,甚至我都没有理解“CMake是什么东西、CMake背后的设计理念是什么”就直接使用了,一个“catkin_make”命令解决所有的问题。然后就是遇到具体CMake问题后在网络上零零散散地临时学习一下怎么具体的使用,但是随着接触的代码工程越来越大、代码系统越来越复杂,之前的关于CMake的知识储备就不够用了。
本人开始针对CMake进行系统的学习和笔记的整理后,才形成 对CMake的初步认知 并不再“凭感觉“和”无厘头经验”配置和管理源代码到可执行文件和库。在本人学习CMake的过程中,帮助最大的几个教材和资料就是CMake官方的 CMake Documentation 和 CMake Cookbook ,以及后来Rafał Świdziński著作的 《Modern CMake for C++》, 回忆起当初硬啃CMake的时光,特别是对照着官方英文的文档看着不熟悉的专业术语时,其过程是有些折磨和煎熬的。 为了减轻学习过程中的痛苦,本人就对相关CMake的知识做了适当的简化和整理,尝试以最基本、最浅显的内容以帮助读者对CMake形成一个初步的认识并且掌握CMake的基本语法。
本篇文章的内容是本人CMake系列学习笔记和总结的第一篇,目的是为了能在阅读完成后对CMake形成一个初步的认识并且掌握CMake的基本语法,在本文中的第三章也会对第二章的CMake语法内容进行具体示例的展开和说明,以展示最基本的利用CMake工具将源代码配置、编译成一个可执行文件或者可管理库文件的步骤,完成对CMake的最初级、初级、初级的入门。如果需要、渴望更加详细CMake内容则可以参考文末的参考资料,自行阅读。
最后,本人是抱着分享和学习的态度发布的这篇文章,若有不当之处,希望大家批评指正。
1、 你好,CMake
1.1 CMake是什么?
我觉得针对这个问题最简单(但不是最正确的)的回答应该是:“CMake是服务于将源代码转换成可执行的文件的工具”。
将源码转换为可工作应用会比较神奇。不仅是效果本身(即设计并赋予生命的工作机制),而且是将理念付诸于过程的行为本身。
CMake本身是一个工具集,由五个可执行的程序组成:cmake、ctest、cpack、cmake-gui和ccmake,其中cmake可以说是出镜率最高的明星级别程序了,它用于在构建项目的第一步,进行项目的配置、生成和构建项目的主要可执行文件的工作。其他的程序们ctest用于运行和报告测试结果的测试驱动程序,cpack用来生成安装程序和源包的打包程序,cmake-gui是 cmake 的图形界面,ccmake(注意ccmake比cmake多了一个c)也是用于图形界面,与cmake-gui不同的地方在于ccmake是基于控制台(terminal)的图形界面。
1.2 CMake设计初衷是为了什么?
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,《Modern CMake for C++》的作者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 配置,并使用单一标准构建项目。
1.3 CMake在“暗地里”是怎么工作的?
表面上可以感受到的工作流程:“CMake 是在一端读取源代码,在另一端生成二进制文件的工具”。 但正如上文所说CMake是一个工具集,那就说明了CMake 自己并没有构建任何东西的能力 ,CMake它依赖于系统中的其他工具来执行实际的编译、链接和其他任务。CMake好似一个在构建过程中工作的“协调器”,它清楚哪些步骤需要完成,理解最终目标是什么,以及忙碌于为构建工作找到合适的“工人”和“材料”。综上, 这个过程有三个阶段:配置、生成、构建阶段 ,可见图2中的表示。
2、 基本的CMake语法
2.1 变量
普通变量、缓存变量、环境变量
普通变量、缓存变量和环境变量 这三类变量组成了CMake变量这一个“复杂”的主题,让人头疼的一点在于上述三个变量在不同的作用域中的“被使用和修改”,而且CMake作用域之间变量如何影响的“特定规则”也会经常在CMake变量的使用过程中体现。
基本的变量操作指令是set()\unset(),变量名区分大小写并可以包含字符(使用括号和引号参数允许在变量名中包含空格。但当以后引用时,必须使用反斜杠来转义空格(\),因此,建议在变量名中只使用字母数字字符、减号(-) 和下划线(_))。具体的使用方式为在设置变量时只需使用set()并提供名称和值,要取消变量的设置时可以使用unset()并提供名称。
1、 set(MyString1 "Text1")
2、 set([[My String2]] "Text2")
3、 set("My String 3" "Text3")
5、 message(${MyString1})
6、 message(${My\ String2})
7、 message(${My\ String\ 3})
9、 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{} 用于引用缓存变量。
2.1.1 环境变量
首先说明如何修改或创建一个环境变量,使用set(ENV{<variable>} <value>) 指令用以声明,使用unset(ENV{<variable>})来清除某一个环境变量,其中ENV表示环境变量标志性前缀,variable指变量名称,value则为变量值, 需要注意的是设定或读取环境变量时,都通过ENV前缀来访问环境变量,读取环境变量值时,要在ENV前加$符号;但if判断是否定义时,不用加$符号。 具体示例如下:
1、 //示例1:
2、 set(ENV{CXX} "clang++")
3、 unset(ENV{VERBOSE})
5、 //示例2:
6、 set(ENV{CMAKE_PATH} "myown/path/example")
7、 # 判断CMAKE_PATH环境变量是否定义
8、 if(DEFINED ENV{CMAKE_PATH}) //注意此处ENV前没有$符号
9、 message("CMAKE_PATH_1: $ENV{CMAKE_PATH}") //注意此处ENV前有$符号
10、 else()
11、 message("NOT DEFINED CMAKE_PATH VARIABLES")
12、 endif()
设定环境变量后,其作用域只影响当前CMake进程,也就是说环境变量设定后是整个CMake进程的作用域都可用,但是不会影响CMake进程外的整个系统环境。
另一个需要注意的点在于,环境变量在启动CMake进程后会基于CMake在配置阶段中收集的信息在 CMake生成阶段 生成环境变量的 副本 ,该副本会在 单一的、全局的 作用域中可用。即,若使用ENV 变量作为指令的参数,这些值将在CMake生成构建系统期间(配置阶段+生成阶段)插入,并且会将其嵌入到构建树中,在构建系统完成后即使再通过脚本或者命令行修改环境变量ENV{<variable>}的value,在构建阶段时该环境变量值也不会更新成新的value(因为在构建系统中保存的是之前环境变量的副本),具体实例如下:
13、 //示例3:
14、 //CMakeLists.txt:
15、 cmake_minimum_required(VERSION 3.20.0)
16、 project(Environment)
17、 //在配置期间打印myenv环境变量
18、 message("generated with " $ENV{myenv})
19、 //在构建阶段过程中打印相同的变量
20、 add_custom_target(EchoEnv ALL COMMAND echo "myenv in build
21、 is" $ENV{myenv})
在上述示例3的CMakeLists.txt中是有两个展示阶段:第一将在配置期间打印myenv环境变量并通过add_custom_target() 添加一个构建阶段,第二将在构建阶段过程中打印相同的变量。构建上述CMakeLists.txt通过一个bash脚本文件执行,见下:
22、 //示例4:
23、 //bash脚本:
24、 //先定义myenv环境变量,并打印
25、 export myenv=first
26、 echo myenv is now $myenv
27、 // 基于CMakeList.txt生成一个构建系统
28、 cmake -B build .
29、 cd build
30、 //基修改myenv环境变量,并打印
31、 export myenv=second
32、 echo myenv is now $myenv
33、 //开始构建
34、 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
2.1.2 缓存变量
缓存变量可以通过$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选项,具体实例如下:
1、 //设置一个string类型的缓存变量,名称为FOO,变量值为BAR
2、 set(FOO "BAR" CACHE STRING "interesting value" FORCE)
3、 //设置一个string类型的缓存变量,名称为CMAKE_BUILD_TYPE,变量值为Release
4、 set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
2.1.3 变量作用域
• 函数作用域: 用于执行用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目录作用域,其都有两个特点:向下有效和数值拷贝生成副本,在不使用特殊关键字的情况下,嵌套(子)作用域针对普通变量的修改不会影响到父作用域。针对变量,普通变量仅仅有效于当前作用域,而缓存变量和环境变量可以在全局作用域中使用。
2.2 控制结构
三类控制结构:条件块、循环、定义指令
没有控制结构,CMake 语言就不完整
CMake中的控制结构就是提供一个结构,让用户可以针对具体的情况来设置触发条件<condition> 表达式来控制需要执行的命令语言。在所有的控制结构中一般都是需要提供条件判断<condition> 表达式的,在if()、elseif()和while()的条件判断表达式的语法都是相同的。这些<condition> 表达式都是根据非常简单的语法求值,如逻辑运算、字符串和变量的求值、比较、CMake检查等,本文中不对上述的语法做详细的展开,但提醒一下条件<condition> 表达式中的“ 字符串和变量的求值 ”的语法中,需要注意求值时 加引用符${} 和 不加引用符${} 时的CMake条件解析时的区别。其他的具体可见CMake的条件指令的语法手册。
2.2.1 条件块
CMake中的条件块是一个必须以if()开头并以endif()结尾的块状结构,在开头的if()和结尾的endif()之间可以添加任意数量的elseif(),但只能有单独一个的、可选的else(),其CMake条件块的结构命令如下:
1、 if(<condition>)
2、 <commands>
3、 elseif(<condition>) # optional block, can be repeated
4、 <commands>
5、 else() # optional block
6、 <commands>
7、 endif()
在具体的条件判断流程中,若满足if() 指令中指定的<condition> 表达式,则执行第一部分的<commands>,如上例第2行部分,否则,CMake 将在属于该块中满足条件的第一个elseif() 指令节中执行命令。若没有这样的命令,CMake将检查是否 提供了else() ,并执行该部分代码中的指令,如上例中第6行的部分。若以上条件都不满足,则不会触发条件块中任何的指令,并在endif() 之后继续执行。
2.2.2 循环
CMake中的循环控制块是一个必须以while()开头创建并以endwhile()结尾的块状结构,只要while() 中提供的<condition> 表达式为true,其后续的指令都会执行,其CMake循环块的结构命令如下:
1、 while(<condition>)
2、 <commands>
3、 endwhile()
除了while()循环结构外,还有一个相对更加常用和简介的循环结构块:foreach()。上文中while()循环块是需要具体的、额外的<condition> 表达式来控制需要执行的命令语言,而foreach()循环则是类似C/C++的for循环风格来控制的,只是foreach块的打开和关闭指令分别是foreach() 和endforeach(),其定义如下所示:
1、 foreach(<loop_var> RANGE <min> <max> [<step>])
2、 <commands>
3、 endforeach()
上述中的<min>和<step>参数变量可选择配置,默认的话从0开始,min和max都必须是非负整数,在RANGE中max和min都是包括在循环内部的。如果设置了min的value值,则必须小于max的value值。
上文中提到foreach()是相对while()而言,在CMake中更加常用和简介的循环结构块,这个是因为foreach()在处理列表变量时十分便捷:
1、 foreach(<loop_variable> IN [LISTS <lists>] [ITEMS <items>])
CMake 将从 所有 提供的<lists> 列表变量中获取元素,也就是输入循环中的list可以是多个,然后再是从所有显式声明的<items>中获取元素值,并将它们都存储在<loop_variable> 中,对每个项逐个执行<commands>。可以选择只提供列表,只提供值或者两者都提供,见下例:
1、 set(MY_LIST 1 2 3)
2、 foreach(VAR IN LISTS MY_LIST ITEMS e f)
3、 message(${VAR})
4、 endforeach()
上述示例中是声明了MY_LIST的列表变量为【1,2,3】,在foreach循环中会获取MY_LIST中的所有元素和<items>中的e、f值,存储在VAR中,在每一次循环中命令指令就是打印VAR的数值,上述代码的打印结果见下:
1. 1
2. 2
3. 3
4. e
5. f
还是以上述foreach使用为例,foreach中还可以优化成一个更简化的指令行并获取相同的结果:
1、 foreach(VAR 1 2 3 e f)
除此以外,从3.17 版本开始,foreach() 已经学会了如何压缩列表(ZIP_LISTS),
1、 foreach(<loop_var>... IN ZIP_LISTS <lists>)
在压缩列表中CMake 将为每个提供的列表创建一个num_<N> 变量,用每个列表中的项填充该变量。同时,除了使用CMake自动创建的num_<N> 变量,用户也可以自定义传递多个<loop_var> 变量名(每个列表一个),每个列表将使用单独的变量来存储,详细见下:
1、 //声明两个具有相同数量元素的list
2、 set(L1 "one;two;three;four")
3、 set(L2 "1;2;3;4;5")
4、 //通过ZIP_LISTS关键子来“压缩“上面的两个列表,以在单次循环中处理相同索引的列表元素
5、 // 示例1:通过num_<N> 变量存储获取元素
6、 foreach(num IN ZIP_LISTS L1 L2)
7、 message("num_0=${num_0}, num_1=${num_1}")
8、 endforeach()
10、 // 示例2:通过自定义传递两个<loop_var>变量,存储获取元素
11、 foreach(word num IN ZIP_LISTS L1 L2)
12、 message("word=${word}, num=${num}")
13、 endforeach()
上面针对多个列表的压缩处理,前提条件是这些待处理的多个列表中的元素个数是相同的,若列表之间的项数不同,CMake 将不会为较短的列表定义变量。
2.2.3 定义指令
除了CMake官方提供和定义的一些指令以外,CMake还提供了用户进行自定义指令的方法:定义指令,CMake中的定义指令通过两种方法实现:macro()和function(),在这里还是可以将CMake中的定义指令macro()和function()的实现与C风格的宏定义和C++的函数定义比较:
• macro() 的工作方式像是 查找和替换指令 ,而不是像function() 这样的实际子例程调用。与函数相反, 宏不会在调用堆栈上创建单独的条目 。所以宏中调用return() 将比在函数中返回调用语句的级别高一级(若已经在顶层作用域中,可能会终止执行)。
• function() 为本地变量创建一个单独的作用域,这与macro() 命令不同,后者在调用者的变量作用域中工作,所以使用CMake的function需要注意变量的作用域问题。
CMake中macro()和function()具体使用方法还是配合下面的示例进行说明。
|| 宏
1、 //CMake中的宏
2、 macro(<name> [<argument> ])
3、 <commands>
4、 endmacro()
完成CMake宏的声明之后就可以通过调用宏的名称<name>来执行宏(函数调用不区分大小写),下例将重点强调宏中变量作用域相关的问题:
1、 //定义了一个名为MyMacro的宏,参数为myVar
2、 macro(MyMacro myVar)
3、 set(myVar "new value")
4、 message("argument: ${myVar}")
5、 endmacro()
7、 set(myVar "first value")
8、 message("myVar is now: ${myVar}")
9、 //调用宏
10、 MyMacro("called value")
11、 message("myVar is now: ${myVar}")
若是运行上面的CMake配置,则可以得到如下的输出:
1. myVar is now: first value
2. argument: called value
3. myVar is now: new value
上例中尽管调用MyMacro尝试显式地将myVar 设置为“new value”, 但后续message打印的${myVar}并不是“new value”,而是在第10行中传递给宏的参数${"called value"} ,也就是宏中对全局作用域中的myVar 变量的修改,并不影响宏中message(”argument:${myVar}”), 这是因为传递给宏的参数没有视为真正的变量,而是作为常量查找并替换指令 。所以宏MyMacro中对全局作用域中的myVar 变量的修改行为,是一种副作用!上述的例子是CMake不提倡的一种实践方式,因为一旦变量作用域和宏作为“查找和替换指令”的行为未被正确使用,就会产生难以描述的负面影响。
具体宏与函数的差异,可以往下阅读以完成概率和使用的对比理解。
|| 函数:
1、 //CMake中的函数声明
2、 function(<name> [<argument> ])
3、 <commands>
4、 endfunction()
还是使用一个经典的CMake函数的使用示例来进行详细说明:
1、 //定义了一个名为MyFunction的函数,参数为FirstArg
2、 function(MyFunction FirstArg)
3、 message("Function: ${CMAKE_CURRENT_FUNCTION}")
4、 message("File: ${CMAKE_CURRENT_FUNCTION_LIST_FILE}")
5、 message("FirstArg: ${FirstArg}")
6、 set(FirstArg "new value")
7、 message("FirstArg again: ${FirstArg}")
8、 message("ARGV0: ${ARGV0} ARGV1: ${ARGV1} ARGC: ${ARGC}")
9、 endfunction()
11、 set(FirstArg "first value")
12、 //调用函数,并传参个数比函数声明时的多了一个
13、 MyFunction("Value1" "Value2")
14、 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配置,则可以得到如下的输出:
1. Function: MyFunction
2. File: /root/examples/chapter02/08-definitions/function.cmake
3. FirstArg: Value1
4. FirstArg again: new value
5. ARGV0: Value1 ARGV1: Value2 ARGC: 2
6. 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。
2.3 实用指令
2.3.1 message() 指令
CMake中打印指令,也就是message() 指令是用于将文本打印到标准输出,并且CMake通过提供MODE 参数,可以自定义输出的样式,并且在出现错误的情况下,可以停止代码:message(<mode> ”text”) 的执行,默认的MODE是“STATUS”,其他的可选MODE模式如下:
- • FATAL_ERROR: 将停止处理和生成。
举个简单例子,使用FATAL_ERROR的模式,在CMake中只打印第一条消息,然后就停止执行:
1、 message(FATAL_ERROR "First Message Printed")
2、 message("Won't print this.")
- • SEND_ERROR: 将继续处理,但跳过生成。
- • WARNING: 继续处理。
- • AUTHOR_WARNING: CMake 警告。继续处理。
- • DEPRECATION: 若启用了CMAKE_ERROR_DEPRECATED 或
- CMAKE_WARN_DEPRECATED 变量,将做出相应处理。
- • NOTICE 或省略模式(默认): 将向stderr 输出一条消息,以吸引用户的注意。
- • STATUS: 将继续处理,建议用于用户的主要消息。
- • VERBOSE: 将继续处理,用于通常不是很有必要的更详细的信息。
- • DEBUG: 将继续处理,并包含在项目出现问题时可能有用的详细信息。
- • TRACE: 将继续处理,并建议在项目开发期间打印消息。通常,在发布项目之前,将这些类型的消息删除。
2.3.2 include() 指令
Modern CMake中重要的一个模块就是引用官方和CMake社区中已经配置好了的CMake模板,所谓的CMake模板就是将CMake代码划分到单独的.cmake文件中,以保持内容的有序和独立性。然后通过include()指令,从父列表文件引用:
1、 include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>])
若提供文件名(一个扩展名为.cmake),CMake 将尝试打开并执行它。这里不会创建嵌套的、单独的作用域,因此对该文件中变量的修改会影响调用作用域。若文件不存在,CMake 将抛出一个错误,除非用optional 关键字指定为可选。若需要知道include() 指令操作是否成功,可以提供一个带有变量名的RESULT_VARIABLE 关键字,若include()引用成功,则用包含的文件的完整路径填充,失败则用未找到(NOTFOUND) 填充。
脚本模式下运行时,将从当前工作目录解析相对路径。要强制搜索与脚本本身相关的内容,请提供绝对路径:
1、 include("${CMAKE_CURRENT_LIST_DIR}/<filename>.cmake")
若不提供路径,但提供了模块的名称(没有.cmake 或其他),CMake 将尝试找到一个模块并包含它。然后,CMake 将在CMake 模块目录CMAKE_MODULE_PATH 中,搜索名称为<module>.cmake的文件。
2.3.3 file() 指令
为了可以知道CMake 脚本可以做什么,CMake提供了一个可以快速浏览文件的操作命令:
1、 file(READ <filename> <out-var> [...])
2、 file({WRITE | APPEND} <filename> <content>...)
3、 file(DOWNLOAD <url> [<file>] [...])
简而言之,file() 指令会以一种与系统无关的方式读取、写入和传输文件,并使用文件系统、文件锁、路径和存档。详情请参阅附录部分。
2.3.4 execute_process() 指令
除了CMake官方和自定义的指令外,有时需要使用系统中可用的工具(毕竟,CMake 主要是一个构建系统生成器),CMake 为此提供了execute_process()指令以用来运行其他进程,并收集它们的输出。这个命令非常适合脚本,也可以在配置阶段的项目中使用。下面是命令的一般形式:
1、 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,请对两个参数使用相同的变量。
3、 简单的CMake构建
本章中参考的源代码可以从GitHub中获取,网址为 https:// github.com/dev-cafe/cma ke-cookbook 。开源代码遵循MIT许可:只要原始版权和许可声明包含在软件/源代码的任何副本中,可以以任何方式重用和重新混合代码。许可的全文可以在 https:// opensource.org/licenses /MIT 中看到。
3.1 简单的可执行文件生成
本节示例中将演示如何运行CMake配置和构建一个简单的项目,该项目由单个源文件组成,用于生成可执行文件。我们将用C++讨论这个项目。
将以下源代码编译为单个可执行文件:
1、 #include <cstdlib>
2、 #include <iostream>
3、 #include <string>
5、 std::string say_hello() { return std::string("Hello, CMake world!"); }
7、 int main() {
8、 std::cout << say_hello() << std::endl;
9、 return EXIT_SUCCESS;
10、 }
对应的CMakeLists.txt配置及注释如下:
1. # set minimum cmake version
2. # FATAL_ERROR关键语法,如果CMake版本低于3.5则发出致命错误
3. cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
4. # project name and language
5. # LANGUAGES关键语法一般都不做声明,C表示C语言,CXX表示C++
6. # CMake中,C++是默认的编程语言。不过,我们还是建议使用LANGUAGES选项在project命令中显式地声明项目的语言
7. project(recipe-01 LANGUAGES CXX)
8. # 指示CMake创建一个新目标:可执行文件hello-world。
9. # 这个可执行文件是通过编译和链接源文件hello-world.cpp生成的,CMake将为编译器使用默认设置,并自动选择生成工具
10. add_executable(hello-world hello-world.cpp)
完成对CMakeLists.txt配置后,可以通过创建build目录,在build目录下来配置项目:
1、 mkdir -p build
2、 cd build
3、 cmake ..
4、
5、 -- The CXX compiler identification is GNU 8.1.0
6、 -- Check for working CXX compiler: /usr/bin/c++
7、 -- Check for working CXX compiler: /usr/bin/c++ -- works
8、 -- Detecting CXX compiler ABI info
9、 -- Detecting CXX compiler ABI info - done
10、 -- Detecting CXX compile features
11、 -- Detecting CXX compile features - done
12、 -- Configuring done
13、 -- Generating done
14、 -- Build files have been written to: /home/user/cmake-cookbook/chapter-01/recipe-01/cxx-example/build
15、
16、 cmake --build .
17、 Scanning dependencies of target hello-world
18、 [ 50%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o
19、 [100%] Linking CXX executable hello-world
20、 [100%] Built target hello-world
3.2 简单构建和链接库文件:静态库和动态库
项目中会有单个源文件构建的多个可执行文件的可能,而且项目中的多个源文件,通常分布在不同子目录中,本小节的实践有助于项目的源代码结构的 模块化、代码重用和关注点分离 (这些都是Modern CMake中设计时的重要内容)。同时,这种分离可以简化并加速项目的重新编译。本示例中,我们将展示如何将源代码编译到库中,以及如何链接这些库。
回看第一个例子, 这里并不再为可执行文件提供单个源文件 ,我们现在将引入一个类,用来包装要打印到屏幕上的消息。更新一下的hello-world.cpp:
1、 #include "Message.hpp"
2、 #include <cstdlib>
3、 #include <iostream>
4、 int main() {
5、 Message say_hello("Hello, CMake World!");
6、 std::cout << say_hello << std::endl;
7、 Message say_goodbye("Goodbye, CMake World");
8、 std::cout << say_goodbye << std
::endl;
9、 return EXIT_SUCCESS;
10、 }
Message类包装了一个字符串,并提供重载过的<<操作,并且包括两个源码文件:Message.hpp头文件与Message.cpp源文件。Message.hpp中的接口包含以下内容:
1、 #pragma once
2、 #include <iosfwd>
3、 #include <string>
4、 class Message {
5、 public:
6、 Message(const std::string &m) : message_(m) {}
7、 friend std::ostream &operator<<(std::ostream &os, Message &obj) {
8、 return obj.printObject(os);
9、 }
10、 private:
11、 std::string message_;
12、 std::ostream &printObject(std::ostream &os);
13、 };
Message.cpp实现如下:
1、 #include "Message.hpp"
2、 #include <iostream>
3、 #include <string>
4、 std::ostream &Message::printObject(std::ostream &os) {
5、 os << "This is my very nice message: " << std::endl;
6、 os << message_;
7、 return os;
8、 }
这里有两个文件需要编译,所以CMakeLists.txt必须进行修改。本例中,先把它们编译成一个库,而不是直接编译成可执行文件,具体实施和注释如下所示:
# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
# project name and language
project(recipe-03 LANGUAGES CXX)
# generate a library from sources
# add_library的第一个参数是目标名,整个CMakeLists.txt中,可使用相同的名称来引用库。
# 生成库是根据第二个参数(STATIC或SHARED)和操作系统确定的。
# 生成的库的实际名称将由CMake通过在前面添加前缀lib和适当的扩展名作为后缀来形成,此例是生成静态库:libmessage.a
add_library(message
STATIC
Message.hpp
Message.cpp
add_executable(hello-world hello-world.cpp)
# 将库链接到可执行文件,此命令还确保hello-world可执行文件可以正确地依赖于消息库。
# 因此,在消息库链接到hello-world可执行文件之前,需要完成message消息库的构建。
target_link_libraries(hello-world message)
# STATIC:用于创建静态库,即编译文件的打包存档,以便在链接其他目标时使用,例如:可执行文件。
# SHARED:用于创建动态库,即可以动态链接,并在运行时加载的库。
# 可以在CMakeLists.txt中使用add_library(message SHARED Message.hpp Message.cpp)从静态库切换到动态共享对象(DSO)。
# DSO: Dynamic Shared Object
# OBJECT:可将给定add_library的列表中的源码编译到目标文件,不将它们归档到静态库中,也不能将它们链接到共享对象中。
# 如果需要一次性创建静态库和动态库,那么使用对象库尤其有用。
本小节中引入了两个新命令:add_library 和 target_link_libraries:
- add_library(message STATIC Message.hpp Message.cpp) :生成必要的构建指令,将指定的源码编译到库中。add_library的第一个参数是目标名。整个CMakeLists.txt中,可使用相同的名称来引用库。生成的库的实际名称将由CMake通过在前面添加前缀lib和适当的扩展名作为后缀来形成。生成库是根据第二个参数(STATIC或SHARED)和操作系统确定的。
- target_link_libraries(hello-world message): 将库链接到可执行文件。此命令还确保hello-world可执行文件可以正确地依赖于消息库。因此,在消息库链接到hello-world可执行文件之前,需要完成消息库的构建。
编译成功后,构建目录包含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库不同,它们不链接到项目中的任何目标,不过可以进行动态加载。该参数可以用于构建运行时插件。
3.3 使用条件块控制编译
目前为止,看到的示例比较简单,CMake执行流是线性的:从一组源文件到单个可执行文件,也可以生成静态库或动态库。为了确保完全控制构建项目、配置、编译和链接所涉及的所有步骤的执行流,CMake提供了自己的语言。本节中,将探索条件结构if-else- else-endif的使用,修改后的CMakeLists.txt和相关注释如下所示:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-04 LANGUAGES CXX)
# introduce a toggle for using a library
set(USE_LIBRARY OFF)
# message语法有以下可选的关键字指定消息的类型:
# (无) = 重要消息;
# STATUS = 非重要消息;
# WARNING = CMake 警告, 会继续执行;
# AUTHOR_WARNING = CMake 警告 (dev), 会继续执行;
# SEND_ERROR = CMake 错误, 继续执行,但是会跳过生成的步骤;
# FATAL_ERROR = CMake 错误, 终止所有处理过程;
message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
# BUILD_SHARED_LIBS is a global flag offered by CMake
# to toggle the behavior of add_library
# 虽然 add_library 语法是默认生成静态库,但是为了确保本例的静态库生成,还是保险地声明了 BUILD_SHARED_LIBS 全局变量,并设置为OFF
# BUILD_SHARED_LIBS 全局变量为false或未定义时,调用add_library将生成一个静态库
set(BUILD_SHARED_LIBS OFF)
# 声明一个列表_sources,列表中的内容为[Message.hpp Message.cpp]
# 在CMake的代码风格是,局部变量在声明时在名称前加下划线
# 如下例中的 _sources 是一个局部变量,不在当前范围外使用,故前面加了下划线 “_”
list(APPEND _sources Message.hpp Message.cpp)
# 逻辑真或假可以用多种方式表示:
# 如果将逻辑变量设置为:1、ON、YES、true、Y或非零数,则逻辑变量为true。
# 如果将逻辑变量设置为:0、OFF、NO、false、N、IGNORE、NOTFOUND、空字符串,或者以-NOTFOUND为后缀,则逻辑变量为false
if(USE_LIBRARY)
# add_library will create a static library
# since BUILD_SHARED_LIBS is OFF
add_library(message ${_sources})
add_executable(hello-world hello-world.cpp)
target_link_libraries(hello-world message)
else()
add_executable(hello-world hello-world.cpp ${_sources})
endif()
3.4 向用户显示选项
前面3.3的配置中引入了条件句:通过硬编码的方式给定逻辑变量值。不过,这会影响用户修改这些变量。CMake代码没有向读者传达,该值可以从外部进行修改。推荐在CMakeLists.txt中使用option()命令,以选项的形式显示逻辑开关,用于外部设置,从而切换构建系统的生成行为。本节的示例将展示如何使用这个命令:
# set minimum cmake version & project name and language
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-05 LANGUAGES CXX)
# expose options to the user
# 用 option 选项命令替换上一个示例的set(USE_LIBRARY OFF)命令。该option选项将修改USE_LIBRARY的值,并设置其默认值为OFF
# option可接受三个参数:
# option(<option_variable> "help string" [initial value])
# <option_variable>表示该选项的变量的名称。
# "help string"记录选项的字符串,在CMake的终端或图形用户界面中可见。
# [initial value]选项的默认值,可以是ON或OFF。
option(USE_LIBRARY "Compile sources into a library" OFF)
# 在声明了 option 选项变量后,就可通过CMake的-DCLI选项,将信息传递给CMake来切换库的行为:cmake -D USE_LIBRARY=ON ..
# 其中,“ -D ” 开关用于为CMake设置任何类型的变量:逻辑变量、路径等等
message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
# 引用 CMakeDependentOption 的配置,目的是下例中通过CMake提供 cmake_dependent_option() 命令用来定义依赖于其他选项的选项
include(CMakeDependentOption)
# second option depends on the value of the first
cmake_dependent_option(
MAKE_STATIC_LIBRARY "Compile sources into a static library" OFF
"USE_LIBRARY" ON
# third option depends on the value of the first
cmake_dependent_option(
MAKE_SHARED_LIBRARY "Compile sources into a shared library" ON
"USE_LIBRARY" ON
# 此关键字的设置与C++移植至WINDOWS相关,这里可以不做具体作用的分析
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
# list sources
list(APPEND _sources Message.hpp Message.cpp)
if(USE_LIBRARY)
message(STATUS "Compile sources into a STATIC library? ${MAKE_STATIC_LIBRARY}")
message(STATUS "Compile sources into a SHARED library? ${MAKE_SHARED_LIBRARY}")
if(MAKE_SHARED_LIBRARY)
add_library(message SHARED ${_sources})
add_executable(hello-world hello-world.
cpp)
target_link_libraries(hello-world message)
endif()
if(MAKE_STATIC_LIBRARY)
add_library(message STATIC ${_sources})
add_executable(hello-world hello-world.cpp)
target_link_libraries(hello-world message)
endif()
else()
add_executable(hello-world hello-world.cpp ${_sources})
endif()
# CMake有适当的机制,通过包含模块来扩展其语法和功能,这些模块要么是CMake自带的,要么是定制的。
# 上述例子中,包含了一个名为CMakeDependentOption的模块。如果没有include这个模块,cmake_dependent_option()命令将不可用。
# 手册中的任何模块都可以以命令行的方式使用cmake --help-module <name-of-module>。
# 例如,cmake --help-module CMakeDependentOption将打印刚才讨论的模块的手册页(帮助页面)
完成了上述的CmakeLIst.txt文件修改后,可以通过CMake的-D 的CLI选项将信息传递给CMake来切换库的行为:
1、 $ mkdir -p build
2、 $ cd build
3、 $ cmake -D USE_LIBRARY=ON ..
4、 -- ...
5、 -- Compile sources into a library? ON
6、 -- ...
7、 $ cmake --build .
8、 Scanning dependencies of target message
9、 [ 25%] Building CXX object CMakeFiles/message.dir/Message.cpp.o
10、 [ 50%] Linking CXX static library libmessage.a
11、 [ 50%] Built target message
12、 Scanning dependencies of target hello-world
13、 [ 75%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o
14、 [100%] Linking CXX executable hello-world
15、 [100%] Built target hello-world
3.5 指定编译器
CMake可以根据平台和生成器选择编译器,还能将编译器标志设置为默认值。然而通常应该控制编译器的选择。本小节将考虑构建类型的选择,并展示如何控制编译器标志:
# set minimum cmake version & project name and language
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-06 LANGUAGES C CXX)
# CMake将语言的编译器存储在CMAKE_<LANG>_COMPILER变量中,其中<LANG>是受支持的任何一种语言,对于我们的目的是CXX、C或Fortran
# 建议使用-D CMAKE_<LANG>_COMPILER CLI选项设置编译器,而不是导出CXX、CC和FC。
# CLI选项:-D CMAKE_<LANG>_COMPILER 是确保跨平台并与非POSIX兼容的唯一方法,避免变量污染与项目一起构建的外部库环境,使用CLI中的-D选项,例如:
# $ cmake -D CMAKE_CXX_COMPILER=clang++ ..
message(STATUS "Is the C++ compiler loaded? ${CMAKE_CXX_COMPILER_LOADED}")
if(CMAKE_CXX_COMPILER_LOADED)
message(STATUS "The C++ compiler ID is: ${CMAKE_CXX_COMPILER_ID}")
message(STATUS "Is the C++ from GNU? ${CMAKE_COMPILER_IS_GNUCXX}")
message(STATUS "The C++ compiler version is: ${CMAKE_CXX_COMPILER_VERSION}")
endif()
message(STATUS "Is the C compiler loaded? ${CMAKE_C_COMPILER_LOADED}")
if(CMAKE_C_COMPILER_LOADED)
message(STATUS "The C compiler ID is: ${CMAKE_C_COMPILER_ID}")
message(STATUS "Is the C from GNU? ${CMAKE_COMPILER_IS_GNUCC}")
message(STATUS "The C compiler version is: ${CMAKE_C_COMPILER_VERSION}")
endif()
# CMake提供了额外的变量来与编译器交互:
# CMAKE_<LANG>_COMPILER_LOADED:如果为项目启用了语言<LANG>,则将设置为TRUE。
# CMAKE_<LANG>_COMPILER_ID:编译器标识字符串,编译器供应商所特有。
# 例如,GCC用于GNU编译器集合,AppleClang用于macOS上的Clang, MSVC用于Microsoft Visual Studio编译器。
# 注意,不能保证为所有编译器或语言定义此变量。
# CMAKE_COMPILER_IS_GNU<LANG>:如果语言<LANG>是GNU编译器集合的一部分,则将此逻辑变量设置为TRUE。
# 注意变量名的<LANG>部分遵循GNU约定:C语言为CC, C++语言为CXX, Fortran语言为G77。
# CMAKE_<LANG>_COMPILER_VERSION:此变量包含一个字符串,该字符串给定语言的编译器版本
3.6 构建类型切换
CMake可以配置构建类型,例如:Debug、Release等。配置时,可以为Debug或Release构建设置相关的选项或属性,例如:编译器和链接器标志。控制生成构建系统使用的配置变量是CMAKE_BUILD_TYPE。该变量默认为空,CMake识别的值为:
- • Debug:用于在没有优化的情况下,使用带有调试符号构建库或可执行文件。
- • Release:用于构建的优化的库或可执行文件,不包含调试符号。
- • RelWithDebInfo:用于构建较少的优化库或可执行文件,包含调试符号。
- • MinSizeRel:用于不增加目标代码大小的优化方式,来构建库或可执行文件。
具体的CMakeLists.txt配置及注释如下:
# set minimum cmake version & project name and language
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-07 LANGUAGES C CXX)
# we default to Release build type
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS "C flags, Debug configuration: ${CMAKE_C_FLAGS_DEBUG}")
message(STATUS "C flags, Release configuration: ${CMAKE_C_FLAGS_RELEASE}")
message(STATUS "C flags, Release configuration with Debug info: ${CMAKE_C_FLAGS_RELWITHDEBINFO}")
message(STATUS "C flags, minimal Release configuration: ${CMAKE_C_FLAGS_MINSIZEREL}")
message(STATUS "C++ flags, Debug configuration: ${CMAKE_CXX_FLAGS_DEBUG}")
message(STATUS "C++ flags, Release configuration: ${CMAKE_CXX_FLAGS_RELEASE}")
message(STATUS "C++ flags, Release configuration with Debug info: ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
message(STATUS "C++ flags, minimal Release configuration: ${CMAKE_CXX_FLAGS_MINSIZEREL}")
随后验证CMake的输出,如下:
1、 # $ mkdir -p build
2、 # $ cd build
3、 # $ cmake ..
4、 # ...
5、 # -- Build type: Release
6、 # -- C flags, Debug configuration: -g
7、 # -- C flags, Release configuration: -O3 -DNDEBUG
8、 # -- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG
9、 # -- C flags, minimal Release configuration: -Os -DNDEBUG
10、 # -- C++ flags, Debug configuration: -g
11、 # -- C++ flags, Release configuration: -O3 -DNDEBUG
12、 # -- C++ flags, Release configuration with Debug info: -O2 -g -DNDEBUG
13、 # -- C++ flags, minimal Release configuration: -Os -DNDEBUG
14、
15、 # $ cmake -D CMAKE_BUILD_TYPE=Debug ..
16、 # -- Build type: Debug
17、 # -- C flags, Debug configuration: -g
18、 # -- C flags, Release configuration: -O3 -DNDEBUG
19、 # -- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG
20、 # -- C flags, minimal Release configuration: -Os -DNDEBUG
21、 # -- C++ flags, Debug configuration: -g
22、 # -- C++ flags, Release configuration: -O3 -DNDEBUG
23、 # -- C++ flags, Release configuration with Debug info: -O2 -g -DNDEBUG
24、 # -- C++ flags, minimal Release configuration: -Os -DNDEBUG
不同配置使用了哪些标志,这主要取决于选择的编译器。需要在运行CMake时显式地打印标志,也可以仔细阅读运行CMake --system-information的输出,以了解当前平台、默认编译器和语言的默认组合是什么。
3.7 编译器选项设置
前面的示例展示了如何探测CMake,从而获得关于编译器的信息,以及如何切换项目中的编译器。后一个任务是控制项目的编译器标志。CMake为调整或扩展编译器标志提供了很大的灵活性,可以选择下面两种方法:
- • CMake将编译选项视为目标属性。因此,可以根据每个目标设置编译选项,而不需要覆盖CMake默认值。
- •可以使用-D 的CLI标志直接修改CMAKE_<LANG>_FLAGS_<CONFIG>变量。这将影响项目中的所有目标,并覆盖或扩展CMake默认值。
本示例中将展示这两种方法,具体的代码示例可见 https:// github.com/dev-cafe/cma ke-cookbook/tree/v1.0/chapter-01/recipe-08 :
# set minimum cmake version & project name and language
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-08 LANGUAGES CXX)
# 打印当前编译器标志。CMake将对所有C++目标使用这些
message("C++ compiler flags: ${CMAKE_CXX_FLAGS}")
list(APPEND flags "-fPIC" "-Wall")
if(NOT WIN32)
list(APPEND flags "-Wextra" "-Wpedantic")
endif()
add_library(geometry
STATIC
geometry_circle.cpp
geometry_circle.hpp
geometry_polygon.cpp
geometry_polygon.hpp
geometry_rhombus.cpp
geometry_rhombus.hpp
geometry_square.cpp
geometry_square.hpp
# 推荐为每个目标设置编译器标志。使用target_compile_options()不仅允许对编译选项进行细粒度控制,而且还可以更好地与CMake的更高级特性进行集成
target_compile_options(geometry
PRIVATE
${flags}
add_executable(compute-areas compute-areas.cpp)
target_compile_options(compute-areas
PRIVATE
"-fPIC"
# 如果配置了 CMAKE_CXX_FLAGS,则CMAKE_CXX_FLAGS的配置会扩展到本项目中所有的目标,在目标中原有target_compile_options的选项上做添加
target_link_libraries(compute-areas geometry)
本例中,警告标志有-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:
1、 # $ mkdir -p build
2、 # $ cd build
3、 # $ cmake ..
4、 # $ cmake --build . -- VERBOSE=1
5、 # ... lots of output ...
6、 # [ 14%] Building CXX object CMakeFiles/geometry.dir/geometry_circle.cpp.o
7、 # /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
8、 # [ 28%] Building CXX object CMakeFiles/geometry.dir/geometry_polygon.cpp.o
9、 # /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
10、 # [ 42%] Building CXX object CMakeFiles/geometry.dir/geometry_rhombus.cpp.o
11、 # /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
12、 # [ 57%] Building CXX object CMakeFiles/geometry.dir/geometry_square.cpp.o
13、 # /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
14、 # ... more output ...
15、 # [ 85%] Building CXX object CMakeFiles/compute-areas.dir/compute-areas.cpp.o
16、 # /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
17、 # ... more output ...
3.8 为语言设定标准
编程语言有不同的标准,即提供改进的语言版本。启用新标准是通过设置适当的编译器标志来实现的。前面的示例中,我们已经展示了如何为每个目标或全局进行配置。3.1版本中,CMake引入了一个独立于平台和编译器的机制,用于为C++和C设置语言标准:为目标设置<LANG>_STANDARD属性,本小节中的示例可见: https:// github.com/dev-cafe/cma ke-cookbook/tree/v1.0/chapter-01/recipe-09 ,下面将展示具体的实施的CMake配置和相关注释:
# 声明最低要求的CMake版本,项目名称和语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-09 LANGUAGES CXX)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
add_library(animals
SHARED
Animal.cpp
Animal.hpp
Cat.cpp
Cat.hpp
Dog.cpp
Dog.hpp
Factory.hpp
set_target_properties(animals
PROPERTIES
CXX_STANDARD 14 #设置语言的标准
CXX_EXTENSIONS OFF #告诉CMake,只启用ISO C++标准的编译器标志,而不使用特定编译器的扩展
CXX_STANDARD_REQUIRED ON #指定所选标准的版本。如果这个版本不可用,CMake将停止配置并出现错误。
#当这个属性被设置为OFF时,CMake将寻找下一个标准的最新版本,直到一个合适的标志。
#这意味着,首先查找C++14,然后是C++11,然后是C++98
POSITION_INDEPENDENT_CODE 1
add_executable(animal-farm animal-farm.cpp)
set_target_properties(animal-farm
PROPERTIES
CXX_STANDARD 14
CXX_EXTENSIONS OFF
CXX_STANDARD_REQUIRED ON
target_link_libraries(animal-farm animals)
需要注意的一点是,如果 语言标准是所有目标共享的全局属性 ,可以在全局的CMakeList.txt中的开始就将
- • CMAKE_<LANG>_STANDARD
- • CMAKE_<LANG>_EXTENSIONS、
- • CMAKE_<LANG>_STANDARD_REQUIRED
变量设置为相应的值。做了语言标准的声明后,CMakeList.txt中所有目标上的对应属性都将使用这些设置,如:
# 声明最低要求的CMake版本,项目名称和语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-09 LANGUAGES CXX)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
... 其他略 ...
3.9 使用控制流
本章前面的示例中,已经使用过if-else-endif的条件控制块,在本文中的第二章还介绍了CMake提供的创建循环的语言工具:foreach endforeach和while-endwhile。两者都可以与break结合使用,以便尽早从循环中跳出。本示例将展示如何使用foreach,来循环源文件列表。示例中将应用这样的循环,在引入新目标的前提下,来为一组源文件进行优化降级:
# set minimum cmake version & project name and language
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-10 LANGUAGES CXX)
add_library(geometry
STATIC
geometry_circle.cpp
geometry_circle.hpp
geometry_polygon.cpp
geometry_polygon.hpp
geometry_rhombus.cpp
geometry_rhombus.hpp
geometry_square.cpp
geometry_square.hpp
# we wish to compile the library with the optimization flag: -O3
target_compile_options(geometry
PRIVATE
# 声明一个list:sources_with_lower_optimization,包含{geometry_circle,geometry_rhombus}
# CMake中,列表是用分号分隔的字符串组。列表可以由list或set命令创建:
# 例如,set(var a b c d e)和list(APPEND a b c d e)都创建了列表a;b;c;d;e。
list(
APPEND sources_with_lower_optimization
geometry_circle.cpp
geometry_rhombus.cpp
# we use the IN LISTS foreach syntax to set source properties
# 循环这些源文件,将它们的优化级别调到-O2。使用它们的源文件属性完成
message(STATUS "Setting source properties using IN LISTS syntax:")
foreach(_source IN LISTS sources_with_lower_optimization)
# set_source_files_properties(file PROPERTIES property value):
# 它将属性设置为给定文件的传递值。与目标非常相似,文件在CMake中也有属性,允许对构建系统进行非常细粒度的控制
set_source_files_properties(${_source} PROPERTIES COMPILE_FLAGS -O2)
message(STATUS "Appending -O2 flag for ${_source}")
endforeach()
# we demonstrate the plain foreach syntax to query source properties
# which requires to expand the contents of the variable
message(STATUS "Querying sources properties using plain syntax:")
foreach(_source ${sources_with_lower_optimization})
# get_source_file_property(VAR file property):检索给定文件所需属性的值,并将其存储在CMake VAR 变量中
get_source_file_property(_flags ${_source} COMPILE_FLAGS)
message(STATUS "Source ${_source} has the following extra COMPILE_FLAGS: ${_flags}")