CMake 全称“cross platform make”,是开源、跨平台的自动化构建系统。CMake 由 Kitware 开发与维护,来自使用者的贡献使得 CMake 快速成长。
CMake 并不直接建构出最终的软件,而是依照平台、编译器产生标准的建构档(如 Unix Makefile 或 Visual Studio 的 projects/workspaces),然后再依一般的生成方式使用。和标准的 GNU 开发工具相比,CMake 的角色比 Make 更高阶,比较接近 Autotools,而且支持多种不同的平台与编译器。
虽然跨平台是 CMake 的重要特色,但由于 CMake 的简单与弹性,在单一平台上使用也很便利。
前言
- 每一个需要进行 CMake 操作的目录下面,都必须存在文件 CMakeLists.txt 。
- CMake 命令不区分大小写。习惯上,CMake 命令全小写,预定义变量全大写。
- 变量使用
${}
方式取值,但是在if
控制语句中是直接使用变量名。 -
command(parameter1 parameter2 …)
,参数使用括号括起,参数之间使用空格或分号分开。
常用预定义变量
CMake 的预定义变量
-
PROJECT_SOURCE_DIR
:工程根目录; -
PROJECT_BINARY_DIR
:运行 CMake 命令的目录。建议定义为${PROJECT_SOURCE_DIR}/build
下; -
CMAKE_INCLUDE_PATH
:环境变量,非 CMake 变量; -
CMAKE_LIBRARY_PATH
:环境变量; -
CMAKE_CURRENT_SOURCE_DIR
:当前处理的 CMakeLists.txt 文件所在路径; -
CMAKE_CURRENT_BINARY_DIR
:target 编译目录;- 使用
add_subdirectory
命令可以更改该变量的值; -
set(EXECUTABLE_OUTPUT_PATH <dir>)
命令不会对该变量有影响,但改变了最终目标文件的存储路径。
- 使用
-
CMAKE_CURRENT_LIST_FILE
:输出调用该变量的 CMakeLists.txt 的完整路径; -
CMAKE_CURRENT_LIST_LINE
:输出该变量所在的行; -
CMAKE_MODULE_PATH
:定义自己的 CMake 模块所在路径; -
EXECUTABLE_OUTPUT_PATH
:重新定义目标二进制可执行文件的存放位置; -
LIBRARY_OUTPUT_PATH
:重新定义目标链接库文件的存放位置; -
PROJECT_NAME
:返回由project
命令定义的项目名称; -
CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS
:用来控制if…else…
语句的书写方式。
系统信息预定义变量
-
CMAKE_MAJOR_VERSION
:CMake 主版本号,如 3.12.0 中的 3; -
CMAKE_MINOR_VERSION
:CMake 次版本号,如 3.12.0 中的 12; -
CMAKE_PATCH_VERSION
:CMake 补丁等级,如 3.12.0 中的 0; -
CMAKE_SYSTEM
:系统名称,例如 Windows-10.0.17134; -
CMAKE_SYSTEM_NAME
:不包含版本号的系统名,如 Windows; -
CMAKE_SYSTEM_VERSION
:系统版本号,如 10.0.17134; -
CMAKE_SYSTEM_PROCESSOR
:处理器架构,如 AMD64; -
UNIX
:在所有的类 UNIX 平台为 true,包括 macOS 和 Cygwin; -
WIN32
:在所有的 Win32 平台为 true,包括 Cygwin。
开关选项
-
BUILD_SHARED_LIBS
:控制默认的库编译方式;- 如果未进行设置,使用 add_library 时又没有指定库类型,默认编译生成的库都是静态库。
-
CMAKE_C_FLAGS
:设置 C 编译选项; -
CMAKE_CXX_FLAGS
:设置 C++ 编译选项。
常用命令
cmake_minimum_required
该语句一般放置在 CMakeLists.txt 的开头,用于说明 CMake 最低版本要求。
cmake_minimum_required(VERSION 3.5)
上述示例指 CMake 的版本号最低为 3.5。
project
project(<PROJECT-NAME>)
-
<PROJECT-NAME>
指工程名称。
该命令一般紧跟 cmake_minimum_required
命令之后,定义了工程的名称。但项目最终编译生成的可执行文件并不一定是这个项目名称,而是由另一条命令确定的,后面会介绍。
执行了该命令之后,将会自动创建两个变量:
-
<PROJECT-NAME>_BINARY_DIR
:二进制文件保存路径; -
<PROJECT-NAME>_SOURCE_DIR
:源代码路径。
project(Lab)
执行了上一条指令,即定义了一个项目名称 Lab
,相应的会生成两个变量:Lab_BINARY_DIR
、Lab_SOURCE_DIR
。
CMake 中预定义了两个变量:PROJECT_BINARY_DIR
和 PROJECT_SOURCE_DIR
。
在这个例子中:
-
PROJECT_BINARY_DIR
等价于Lab_BINARY_DIR
; -
PROJECT_SOURCE_DIR
等价于Lab_SOURCE_DIR
。
建议直接使用 PROJECT_BINARY_DIR
与 PROJECT_SOURCE_DIR
,这样即使项目名称发生了变化,也不会影响 CMakeLists.txt 文件。
关于 PROJECT_BINARY_DIR
与 PROJECT_SOURCE_DIR
这两个变量是否相同的问题,涉及到编译方法是内部编译还是外部编译。如果是内部编译,则这两个变量相同;如果是外部编译,则两个变量不同。此处对内部编译与外部编译做出介绍:
外部构建与内部构建
假设此时已经完成了 CMakeLists.txt 的编写,在 CMakeLists.txt 所在目录下,有两种执行 CMake 的方法:
cmake .\\
make
以及
mkdir build
cd .\\build
cmake ..\\
make
第一种方法是内部构建,第二种方法是外部构建。上述两种方法中,最大不同在于 CMake 与 Make 的工作路径不同。
内部构建方法中,CMake 生成的中间文件和可执行文件都会存放在项目目录中;外部构建方法中,中间文件与可执行文件都存放在 build 目录中。
建议使用外部构建方法。优点显而易见:最大限度地保持了代码目录的整洁,生成、编译与安装是不同于项目目录的其他目录中,在外部构建方法下,PROJECT_SOURCE_DIR
指向目录与内部构建相同,为 CMakeLists.txt 所在根目录;而 PROJECT_BINARY_DIR
不同,它指向 CMakeLists.txt 所在根目录下的 build 目录。
set
set(<variable> <value>… CACHE <type> <docstring> [FORCE])
示例:
set(CMAKE_INSTALL_PREFIX C:\\Program Files\\${PROJECT_NAME})
该示例显式地将 CMAKE_INSTALL_PREFIX
的值定义为 C:\\Program Files\\${PROJECT_NAME}
。如此,在外部构建情况下执行 make install
命令时,Make 会将生成的可执行文件拷贝到 C:\\Program Files\\${PROJECT_NAME}\\bin
目录下。
当然,可执行文件的安装路径 CMAKE_INSTALL_PREFIX
也可以在执行 cmake
命令的时候指定,cmake
参数如下:
cmake -D CMAKE_INSTALL_PREFIX="C:\\Program Files\\…"
如果 cmake
参数和 CMakeLists.txt 文件中都不指定该值的话,则该值为默认值(Windows 下为 C:\\Program Files\\${PROJECT_NAME}
,UNIX 下为 /usr/local
)。
add_subdirectory
add_subdirectory(source_dir [binary_dir]
[EXCLUDE_FROM_ALL])
-
source_dir
:源文件路径; -
[binary_dir]
:中间二进制与目标二进制文件存放路径; -
[EXECLUDE_FROM_ALL]
:将这个目录从编译过程中排除。
这个命令用于向当前工程添加存放源文件的子目录。
include_directories
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 …])
-
[AFTER|BEFORE]
:追加标志,指定控制追加或预先添加; -
[SYSTEM]
:指定该目录为系统包含目录; -
dir1, …, dir n
:添加的一系列头文件搜索路径。
向工程添加多个特定的头文件搜索路径,路径之间用空格分隔。类似于 GCC 中的编译参数 -l
,即指定编译过程中编译器搜索头文件的路径。当项目需要的头文件不在系统默认的搜索路径时,则指定该路径。
add_executable
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 …])
-
name
:可执行文件名; -
source1, …, source n
:生成该可执行文件的源文件。
该命令给出源文件,并指出需要编译出的可执行文件名。
示例 1:
add_executable(hello main.cpp)
该示例中,利用源文件 main.cpp
,编译出名为 hello
的可执行文件。
示例 2:
set(SRC_LIST main.cc
rpc/CRNode.cpp
rpc/Schd_types.cpp
task/TaskExecutor.cpp
task/TaskMoniter.cpp
util/Const.cpp
util/Globals.cc
)
add_executable(CRNode ${SRC_LIST})
该示例中,定义了该工程会生成一个名为 CRNode
的可执行文件,所依赖的源文件是变量 SRC_LIST
定义的源文件列表。
如果前面 project()
命令中定义的项目名称也是 CRNode
,没有什么问题,两者之间没有任何关系。
add_library
add_library(<name> [STATIC|SHARED|MODULE]
[EXCLUDE_FROM_ALL]
[source1] [source2 …])
-
name
:库文件名称; -
[STATIC|SHARED|MODULE]
:生成的库的文件类型(静态库/共享库); -
[EXCLUDE_FROM_ALL]
:表示该库不会被默认构建; -
source1, …, sourceN
:生成库所依赖的源文件。
示例:
add_library(hello SHARED ${LIB_hello_SRC})
link_directories
link_directories(directory1 directory2 …)
该命令用于添加外部库的搜索路径。
target_link_libraries
target_link_libraries(<target> … <item>…)
-
target
:目标文件; -
item
:链接外部库文件。
指定链接目标文件时需要链接的外部库,效果类似于 GCC 编译参数 -L
,解决外部库依赖的问题。
message
message([<mode>] "message to display" …)
-
mode
:确定消息的类型:模式 描述 (none)
重要信息 STATUS
附带信息 WARNING
CMake 警告,继续处理 AUTHOR_WARNING
CMake 警告(dev),继续处理 SEND_ERROR
CMake 错误,继续处理,但跳过生成过程 FATAL_ERROR
CMake 错误,停止处理和生成过程 DEPRECATION
如果分别启用 CMAKE_ERROR_DEPRECATED
或CMAKE_WARN_DEPRECATED
,则弃用 CMake 错误或警告,否则无消息 "message to display"
:需要显示的文字消息。
set_target_properties
set_target_properties(target1 target2 …
PROPERTIES prop1 value1
prop2 value2 …)
设置目标的某些属性,改变它们构建的方式。
该指令为一个目标设置属性,语法是列出所有用户想要变更的文件,然后提供想要设置的值。用户可以使用任何想用的属性与对应的值,并在随后的代码中调用 GET_TARGET_PROPERTY
命令取出属性的值。
影响目标输出文件的属性PROPERTIES
详述如下:
PREFIX、SUFFIX
-
PREFIX
覆盖了默认的目标名前缀(如 lib); -
SUFFIX
覆盖了默认的目标名后缀(如 .so)。
IMPORT_PREFIX、IMPORT_SUFFIX
与 PREFIX
、SUFFIX
是等价的属性,但针对的是 DLL 导入库(即共享库目标)。
OUTPUT_NAME
构建目标时,OUTPUT_NAME
用来设置目标的真实名称。
LINK_FLAGS
为一个目标的链接阶段添加额外标志。
LINK_FLAGS_<CONFIG>
将为配置 <CONFIG>
添加链接标志,如 Debug
、Release
、RelWithDebInfo
、MinSizeRel
。
COMPILE_FLAGS
设置附加的编译器标志,在构建目标内的源文件时用到。
LINKER_LANGUAGE
改变链接可执行文件或共享库的工具。默认值是设置与库中文件相匹配的语言。
CXX 与 C 是该属性的公共值。
VERSION、SOVERSION
VERSION
指定构建的版本号,SOVERSION
指定构建的 API 版本号。
构建或安装时,如果平台支持符号链接,且链接器支持 so 名称,那么将会创建恰当的符号链接。
如果只指定两者中的一个,缺失的另一个假定为具有相同版本号。
示例 1:
set_target_properties(hello_static PROPERTIES OUTPUT_NAME "hello")
示例 2:
set_target_properties(hello PROPERTEIES VERSION 1.2 SOVERSION 1)
该命令用于控制版本,VERSION
指代动态库版本,SOVERSION
指代 API 版本。
aux_source_directory
查找某个路径下的所有源文件,并将源文件列表存储到一个变量中。
aux_source_directory(<dir> <variable>)
示例:
aux_source_directory(. SRC_LIST)
该指令将当前目录下的文件列表全部存入变量 SRC_LIST
中。
install
install
命令可以按照对象的不同分为多种类型:目标文件、非目标文件、目录。
目标文件
install(TARGETS targets… [EXPORT <export-name>]
[[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
[DESTINATION <dir>]
[PERMISSIONS permissions…]
[CONFIGURATIONS [Debug|Release|…]]
[COMPONENT <component>]
[NAMELINK_COMPONENT <component>]
[OPTIONAL] [EXCLUDE_FROM_ALL]
[NAMELINK_ONLY|NAMELINK_SKIP]
] […]
[INCLUDES DESTINATION [<dir> …]]
)
-
TARGETS targets
:targets
即通过add_executable
或add_library
定义的目标文件,可能是可执行二进制文件、动态库、静态库; -
DESTINATION <dir>
:dir
即定义的安装路径。安装路径可以是绝对/相对路径。在绝对路径的情况下,CMAKE_INSTALL_PREFIX
就无效了。- 如果希望使用
CMAKE_INSTALL_PREFIX
定义安装路径,就需要使用相对路径,这时候安装后的路径就是${CMAKE_INSTALL_PREFIX}\\<dir>
。
- 如果希望使用
非目标文件
.sh 脚本文件,即为典型的非目标文件的可执行程序。
install(<FILES|PROGRAMS> files… DESTINATION <dir>
[PERMISSIONS permissions…]
[CONFIGURATIONS [Debug|Release|…]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL] [EXCLUDE_FROM_ALL])
使用方法和上述目标文件指令的 install
基本相同。唯一的区别是,安装非目标文件之后的权限还包括 OWNER_EXECUTE
、GOUP_EXECUTE
、WORLD_EXECUTE
,即 755 权限目录的安装。
目录
install(DIRECTORY dirs… DESTINATION <dir>
[FILE_PERMISSIONS permissions…]
[DIRECTORY_PERMISSIONS permissions…]
[USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
[CONFIGURATIONS [Debug|Release|…]]
[COMPONENT <component>] [EXCLUDE_FROM_ALL]
[FILES_MATCHING]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS permissions…]] […])
-
DIRECTORY dirs
:dirs
是所在源文件目录的相对路径。但必须注意,abc
与abc/
有很大区别:-
abc
:该目录将被安装为目标路径的abc
; -
abc/
:将该目录内容安装到目标路径,但不包括该目录本身。
-
示例:
install(DIRECTORY icons scripts/ DESTINATION share/myproj
PATTERN "CVS" EXCLUDE
PATTERN "scripts/*" PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ)
该指令的执行结果是:
- 将 icons 目录安装到
<prefix>/share/myproj
; - 将 scripts/ 中的内容安装到
<prefix>/share/myproj
; - 不包含目录名为 CVS 的目录;
- 对于 scripts/* 文件指定权限为
OWNER_EXECUTE
、OWNER_WRITE
、OWNER_READ
、GROUP_EXECUT
、GROUP_READ
。
基本控制语法
if
if…else… 语法格式有些类似于 Visual Basic .NET:
if(<expression>)
# then section.
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
#…
elseif(<expression2>)
# elseif section.
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
#…
else(<expression>)
# else section.
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
#…
endif(<expression>)
其中,一定要有 endif()
与 if()
对应。
if 基本用法
if(<expression>)
:expression
不为0
、OFF
、NO
、FALSE
、N
、IGNORE
、NOTFOUND
、空字符串,或者不含后缀-NOTFOUND
时,为真;if(NOT <expression>
:与上一条相反;if(<expr1> AND <expr2>)
if(<expr1> OR <expr2>
if(COMMAND command-name)
:如果command-name
是命令、宏或函数并可调用,为真;if(EXISTS path-to-file-or-directory)
:如果给定路径的文件或目录存在,为真;if(file1 IS_NEWER_THAN file2)
:当 file1 比 file2 新,或 file1/file2 中有一个不存在时为真,文件名需使用全路径;if(IS_DIRECTORY path-to-directory)
:当给定路径是目录时,为真。注意使用全路径;if(DEFINED <variable>)
:如果变量已被定义,为真;-
if(<variable|string> MATCHES regex)
:当给定变量或字符串能匹配正则表达式 regex 时,为真。此处的variable
直接使用变量名,而非${variable}
。-
示例:
if("hello" MATCHES "ell") message("true") endif("hello" MATCHES "ell")
-
数字比较表达式
if(<var> LESS <number>)
if(<var> GREATER <number>)
if(<var> EQUAL <number>)
字母表顺序比较
if(<var1> STRLESS <var2>)
if(<var1> STRGREATER <var2>)
if(<var1> STREQUAL <var2>)
示例 1:
判断平台差异。
if(WIN32)
message(STATUS "This is windows.")
else(WIN32)
message(STATUS "This is not windows.")
endif(WIN32)
上述代码可以控制不同平台进行不同控制。
也许 else(WIN32)
之类的语句阅读起来很不舒服,这时候可以加上语句:
set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON)
这时候上述结构就可以写成:
if(WIN32)
message(STATUS "This is windows.")
else()
message(STATUS "This is not windows.")
endif()
示例 2:
if(WIN32)
#do something related to WIN32
elseif(UNIX)
#do something related to UNIX
elseif(APPLE)
#do something related to APPLE
endif(WIN32)
while
while(<condition>)
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endwhile(<condition>)
类似于 endif
,while()
也需要 endwhile()
匹配。
真假判断条件可以参考 if
指令。
foreach
foreach
有多种使用形式的语法,且每个 foreach()
都需要一个 endforeach()
与之匹配。
列表语法
foreach(<loop_var> <arg1> <arg2> …)
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endforeach(<loop_var>)
示例:
aux_source_directory(. SRC_LIST)
foreach(F ${SRC_LIST})
message(${F})
endforeach(F)
该示例中,先将当前路径下的所有源文件列表赋值给变量 SRC_LIST
,然后遍历 SRC_LIST
中的文件,并持续输出信息,信息内容是当前路径下所有源文件的名称。
范围语法
foreach(<loop_var> RANGE <total>)
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endforeach(<loop_var>)
示例:
foreach(v RANGE 10)
message(${v})
endforeach(v)
该示例从 0 到 total
(此处为 10),以 1 为步进。此处输出为:012345678910
。
范围步进语法
foreach(<loop_var> RANGE <start> <stop> [<step>])
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endforeach(<loop_var>)
从 start
开始,到 stop
结束,以 step
为步进。
示例:
foreach(a RANGE 5 15 3)
message(${a})
endforeach(a)
此处输出为 581114
。
迭代语法
foreach
还可以循环访问生成的数字范围。这种迭代有三种类型:
- 指定单个数字时,范围将包含元素
[0, …, total]
(包括total
); - 指定两个数字时,范围将包含从第一个数字到第二个数字(包括)的元素;
- 第三个可选数字是用于从第一个数字迭代到第二个数字(包括)的增量。
foreach(<loop_var> IN [LISTS [<list1> …]]
[ITEMS [<item1> …]])
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endforeach(<loop_var>)
foreach
循环访问一个精确的项列表:
-
LISTS
:给要遍历的列表值变量命名,包括空元素(空字符串是零长度列表)。注意宏参数不是变量。 -
ITEMS
:结束参数解析并包含迭代中它后面的所有参数。