CMake是非常流行的构建系统(更准确地讲,是构建系统的“生成器”)。尤其是在C++项目中,现代CMake(3.10+版本)几乎已经成为事实上的标准(standard de facto)。
在现代CMake中,使用目标(target)代替传统的目录层次作为构建的最小单位,避免将路径硬编码,构建命令再也不用与源码目录结构绑定;CMake只用一套语法,就可以在不同的平台上生成对应的构建命令(在linux上生成make、ninja, 在windows上生成vs),实现了跨平台编译;CMake在其顶层维护了一个全局变量空间,用于存储构建时各种编译选项与元数据,从而方便地切换编译器或编译同一份软件不同的版本。
如何组织构建目标
使用现代CMake的要点之一是以target为中心,而不是以目录为中心。然而,在网上随便搜索到的CMake样例却可能不符合这一原则。CMake的设计师花了很大的功夫,将旧版本兼容做的很好,所以到今天我们可以同时见到(甚至在同一个project中)两种组织方式,而它们都是可行的。但是围绕target的组织方式更稳健、更省心,应尽可能地优先采用。
Target指的是构建过程中产生的阶段性文件,一般来说可以是可执行文件(executable)或库(library)。使用以下constructor初始化一个target,第一个变量为target名(必须有),之后为其源文件(可选)。
add_executable(foo bar.cpp baz.cpp)
add_library(foo bar.cpp baz.cpp)
单独使用constructor时,虽然我们没有为此target指定编译器,依赖的头文件路径等,constructor还是有默认的参数、行为的(源于旧版本兼容):通常来说,默认编译器是cc,不依赖任何头文件,最后生成一个名为foo的可执行文件……
也可以通过以下命令更改constructor行为:
现代CMake命令 | 相当于旧版CMake中 | 作用 |
target_compile_definitions | add_definitions | 增加一个源文件可见的#define宏 |
target_compile_features | ||
target_compile_options | C_FLAGS, CXX_FLAGS变量 | |
target_include_directories | include_directories | |
target_link_libraries | link_libraries | |
target_sources | add_executable第二个变量开始 | 为 |
get_target_property | ||
set_target_property |
以上命令使用时,可以使用PUBLIC,PRIVATE,INTERFACE三个关键字限定命令中变量的作用域。其中PRIVATE修饰仅仅用于当前target构建的文件,INTERFACE修饰依赖当前target的其它target构建时需要的文件,而PUBLIC是两者融合,其修饰的文件即用于当前target,也用于依赖当前target的其它target。如
add_executable(foo bar.cpp)
target_include_directories(foo PRIVATE header.h)
最小样例
考虑只有一层目录,一个源文件的构建情况。在根目录中创建一个名为CMakeLists.txt的文本文件,并在其中写入以下内容。
cmake_minimum_requirement(VERSION 3.10.0)
project(dummy_project)
add_executable(foo bar.cpp)
一次完整的构建称作一个project。project中需要指定cmake版本(想想CMake日新月异的语法)和project名。不论有多少个还可以给project加一些说明,方便后续读者。
project(dummy_project VERSION 0.0.1 DESCRIPTION "a dummy project")
多文件夹构建组织形式
使用add_subdirectories()告知CMake除当前目录外,另需构建的子目录,子目录中也必须存在CMakeLists.txt文件。在现代CMake中,并没有目录的概念,“添加一个子目录”实际上是去执行子目录中CMakeList.txt所指定的target的构建。 当工程中存在多个target且它们之间有依赖关系时,使用add_dependencies()规定依赖顺序,高层的target会等待底层的target构建完毕后再构建,这在使能多线程构建时格外重要。
CMakeLists.txt与.cmake
有时会在源码目录下看到CMakeList.txt之外还有部分.cmake文件,如果熟悉make,可以类比为Makefile与.mk文件。当编写CMake文件时,总有一些变量、函数是可以共用的。这些公用部分就可以写入.cmake文件,之后被多个CMake文件引入。而.cmake文件本身不会被执行,避免影响构建过程。
.cmake文件不一定总是要和CMakeList.txt放置在同一目录下, 可以通过include()命令引入别处的.cmake文件
还可以将一个工程中所使用到的所有.cmake文件汇总到一个子文件夹下,并通过设置CMAKE_MODULE_PATH将它们一次性全部包含
# .cmake files locate under src_root/cmake
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
add_custom_target()与add_custom_command()
这是在默认的add_executable()与add_library()之外另一种添加target的方式。不需要使用冗杂的targetadd...命令,而是像写makefile一样直接写出构建命令。这样做的好处是可以精确控制构建过程中的每一步(如自定义编译、链接,而非直接.c到.exe一步到位),还可以调用任何除编译器之外的工具(如反汇编等)。
add_custom_target(Name [ALL] [command1 [args1...]]
[COMMAND command2 [args2...] ...]
[DEPENDS depend depend depend ... ]
[COMMENT comment]
[SOURCES src1 [src2...]])
add_custom_command(OUTPUT output1 [output2 ...]
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[MAIN_DEPENDENCY depend]
[DEPENDS [depends...]])
add_custom_target- 与command的区别:
- 前者是通常意义上的构建目标,可以类比到makefile中冒号左侧的文件名,最后可以生成一个文件
- 后者不会构建一个文件,而更侧重于“操作”的概念(可以是环境变量设置、字符串操作、删除文件等操作)。当一个target依赖某个command时,会在生成该文件之前先将其依赖的command执行一遍
- target与command可以互相依赖
CMake变量(variable)
变量在旧版CMake中起到传递参数的作用,在现代CMake中(大约从2017年开始)更鼓励使用target property和对应的set-, get_properties,但是习惯没那么容易扭转,在许多项目中仍大量地使用变量。
使用set()命令初始化变量,并使用${}操作符evaluate变量。
CMake中常用的变量形式有字符串和列表。字符串即形如“abc”等字符字面量,字符串中可以evaluate其它的变量,形如set(hello "hello ${name}")。
列表是多个以空格相隔的变量,形如 aa bb cc。列表可以隐式转化为字符串,直接当作字符串使用,这时它相当于各变量名以分号连接的字符串,形如“aa;bb;cc”。
内置的变量
比如 ,在最小样例中,设置project的VERSION之后,CMake内部会初始化以下变量:
- PROJECT_VERSION, _VERSION
- PROJECT_VERSION_MAJOR, _VERSION_MAJOR
哪怕在构建工程时从头到尾都没有使用,这些变量依旧存在于工程中,并且可以随时引用。它们称为CMake的内置变量 (internal variables)。
阅读一份CMakeLists.txt文件时,常会见到一些突然冒出来的宏定义,让人感到非常迷惑,因为它们之前从未被手动定义过。如果这些变量是全大写、由下划线连接的,就要考虑一下它们是不是内置变量!
“雪上加霜”的是,部分内置变量可能曾经一度作为普通变量,被定义在某个官方的.cmake文件中;却在之后新版本中改为内置变量。类似地、某些内置的宏定义可能之前就是个普通的宏定义。所以搞清楚某个变量、宏是否为内置之前,必须先确认使用的cmake版本,再去查阅该版本的官方文档。
举例
- CMAKE_SOURCE_DIR source tree根目录
- CMAKE_CURRENT_SOURCE_DIR 当前source目录
- CMAKE_BINARY_DIR 构建根目录
CMAKE_CURRENT_BINARY_DIR 当前构建目录
CMAKE_C_FLAGS 传递给C编译器的选项
- CMAKE__COMPILER 指定某种语言源文件的编译器
- CMAKE_C_COMPILE_OBJECT 指定到.o文件的编译命令
- CMAKE_C_LINK_EXECUTABLE 指定.o到elf的编译命令
字符串操作
设置变量
临时变量(只在cmake此文件时有效):set( ... [PARENT_SCOPE])
长期变量(存储在cache中):set( ... CACHE [FORCE]),其中类型type可选收下几种情况
BOOL
BooleanON/OFF
value.cmake-gui(1)
offers a checkboxFILEPATH
Path to a file on disk.cmake-gui(1)
offers a file dialog.PATH
Path to a directory on disk.cmake-gui(1)
offers a file dialog.STRING
A line of text.cmake-gui(1)
offers a text field or a drop-down selection if theSTRINGS
cache entry property is set.INTERNAL
A line of text.cmake-gui(1)
does not show internal entries. They may be used to store variables persistently across runs. Use of this type impliesFORCE
.
如果只是bool变量还可以使用option( "" [value]), 默认为OFF
获取文件名中的一部分字符串
get_filename_component( [CACHE])
- DIRECTORY = Directory without file name
- NAME = File name without directory
- EXT = File name longest extension (.b.c from d/a.b.c)
- NAME_WE (without extension)= File name with neither the directory nor the longest extension
- LAST_EXT = File name last extension (.c from d/a.b.c)
- NAME_WLE = File name with neither the directory nor the last extension
将CMake变量传入其它文件
configure_file( [NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS | FILE_PERMISSIONS ...] [COPYONLY] [ESCAPE_QUOTES] [@ONLY] [NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])
将一个文件(往往以.in结尾)按当前CMAKE中的变量生成到另一个文件中, 所有形似@VAR@
or ${VAR}
的变量都会被替换为对应的cmake变量值。
列表操作
list(APPEND demo_list aa bb cc)
使用正则表达式列出当前文件夹下文件列表
file(GLOB
[LIST_DIRECTORIES true|false] [RELATIVE ] [CONFIGURE_DEPENDS]
[...])
- LIST_DIRECTORIES: 列表中包含文件夹
- RELAITIVE 列出相对路径
现代CMake中不鼓励使用file(GLOB),这是因为该文件列表只在CMake中evalute一次,之后传递给底层构建系统(如make)。当源码目录中文件调整后,make依然只能获取cmake之前evaluate得到的文件目录,而对源码目录发生的变化一无所知。
CMake流程控制
打印消息
写的CMake总报错,但找不到错怎么办?为什么不试试万能的打印呢?
message()
FATAL_ERROR
CMake Error, stop processing and generation. 立即停下SEND_ERROR
CMake Error, continue processing, but skip generation. 停下但仍然继续生成正确的部分STATUS
The main interesting messages that project users might be interested in. Ideally these should be concise, no more than a single line, but still informative. 这一条是最常用的
循环
foreach(<loop_var> <items>)
<commands>
endforeach()
CMake函数(function)与宏(macro)
function与macro的目的都是为了复用代码
macro和function的区别
- macro是文本替换,与调用函数在一个scope上。其中任何set()指令都会影响到全局
- function是Subroutine,有自己的scope(内部定义的变量在外部看不到,必须显式地指定PARENT_SCOPE关键字)
function或macro中ARGV,ARGN等常量是什么?
ARGC是参数数量,ARGV#是各个参数,而ARGN传入的多余的参数。下面的这个例子写得很清楚:
cmake_minimum_required(VERSION 2.8)
function(use_llvm TARGET)
message("ARGC=\"${ARGC}\"")
message("ARGN=\"${ARGN}\"")
message("ARGV=\"${ARGV}\"")
message("ARGV0=\"${ARGV0}\"")
message("ARGV1=\"${ARGV1}\"")
endfunction()
add_custom_target(foo
COMMAND ls)
use_llvm(foo core bitwriter)
# Results:
# ARGC="3"
# ARGN="core;bitwriter"
# ARGV="foo;core;bitwriter"
# ARGV0="foo"
# ARGV1="core"
CMake GUI
mark_advanced([CLEAR] 各个变量...) 这个命令是用于cmake-gui的,设置各个变量是否默认出现在主界面(设置为advanced则不出现,clear后则默认出现,猜测其advanced的含义是类似于“高级设置”),使用命令行时,这个选项不起作用。