基础知识
CMake简介
CMake是一个开源的可扩展工具,用于独立于编译器的管理构建过程。CMake必须和本地构建系统联合使用,在每个源码目录中,需要编写CMakeLists.txt文件,以声明如何生成标准的构建文件(例如GNU Make的Makefiles,或者MSVS的解决方案)。
CMake支持所有平台的内部构建(in-source build)和外部构建(out-of-source build)。内部构建的源码目录和二进制目录为同一目录,即CMake会改变源码目录的内容。通过外部构建,可以针对单个源码树进行多重构建(Multiple builds )。
CMake会生成一个方便用户编辑的缓存文件,当其运行时,会定位头文件、库、可执行文件,这些信息被收集到缓存文件中。用户可以在生成本地构建文件之前编辑它。
CMake命令行支持自动或者交互式的运行。CMake还提供了一个基于QT的GUI,其名称为cmake-gui。注意此GUI同样依赖于环境变量的正确设置。
基本语法
CMakeLists.txt包含一系列的命令,每个命令都是 COMMAND (args…) 的形式,多个参数使用空白符分隔。CMake提供了很多预定义命令,你可以方便的扩展自己的命令。 CMake支持简单的变量,它们或者是 字符串 ,或者是 字符串的列表 。引用一个变量的语法是 ${VAR_NAME} 。
如果向一个命令传递列表变量,效果等同于向它逐个传递列表成员:
|
set ( V 1 2 3 ) # V的值是1 2 3
command ( $ { V } ) # 等价于command(1 2 3)
|
要把一个列表变量作为整体传递,只需要加上双引号即可:
|
command ( "${V}" ) # 等价于command("1 2 3")
|
CMake可以直接访问 环境变量 和 Windows注册表 ,前者使用语法 $ENV(VAR) ,后者使用语法 [HKEY_CURRENT_USER\\SOFTWARE\\path;key]
CMake的优势
- 支持多个底层构建工具,例如GNU Make、MSVC、XCode等等,可以生成这些构建工具需要的配置文件
- 通过分析环境变量、Windows注册表等,自动搜索构建所需的程序、库、头文件
- 支持创建复杂的命令
- 很方便的在共享库、静态库两种构建方式之间切换
- 自动生成、维护C/C++文件依赖关系,并且在大部分平台上支持并行构建
在开发跨平台软件时,CMake具有以下额外优势:
- 可以测试机器字节序和其它硬件特性
- 统一的构建配置文件
- 支持依赖于机器特定信息的配置,例如文件的位置
安装CMake
|
# Ubuntu
sudo apt - get install cmake
# Redhat
yum install cmake
# Mac OS X with Macports
sudo port install cmake
# Window https://cmake.org/files/v3.5/cmake-3.5.2-win32-x86.zip
|
HelloWorld
C++源码:
|
#include
using namespace std ;
int main ( ) {
return 0 ;
}
|
要通过CMake编译上述文件,需要在同一目录下放置CMakeLists.txt文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# 需要最小的CMake版本
cmake_minimum_required ( VERSION 3.3 )
# 工程的名称,会作为MSVS的Workspace的名字
project ( intellij_taste )
# 全局变量:CMAKE_SOURCE_DIR CMake的起始目录,即源码的根目录
# 全局变量:PROJECT_NAME 工程的名称
# 全局变量:PROJECT_SOURCE_DIR 工程的源码根目录的完整路径
# 全局变量:构建输出目录。默认的,对于内部构建,此变量的值等于CMAKE_SOURCE_DIR;否则等于构建树的根目录
set ( CMAKE_BINARY_DIR $ { CMAKE_SOURCE_DIR } / bin ) # ${}语法用于引用变量
# 全局变量:可执行文件的输出路径
set ( EXECUTABLE_OUTPUT_PATH $ { CMAKE_BINARY_DIR } )
# 全局变量:库文件的输出路径
set ( LIBRARY_OUTPUT_PATH $ { CMAKE_BINARY_DIR } )
# 设置头文件位置
include_directories ( "${PROJECT_SOURCE_DIR}" )
# 设置C++标志位
set ( CMAKE_CXX _FLAGS "${CMAKE_CXX_FLAGS} -std=c++11" )
# 设置源文件集合
set ( SOURCE_FILES main . cpp )
# 添加需要构建的可执行文件,第二个以及后续参数是用于构建此文件的源码文件
add_executable ( intellij_taste $ { SOURCE_FILES } )
|
在上述目录中执行下面两条命令,即可执行构建:
|
# 生成CMake配置文件
mkdir build && cmake . . && cd . .
# 在bin子目录中生成可执行文件,注意,亦可使用底层构建系统,例如make命令或者MSVC的IDE
# cmake --build [options] [-- [native-options]]
cmake -- build build -- - j3 # --表示把其余选项传递给底层构建工具
|
核心理念
CMake包含一系列重要的概念抽象,包括目标(Targets)、生成器(Generators)、命令(Commands)等,这些命令均被实现为C++类。理解这些概念后才能编写高效的CMakeLists文件。
下面列出这些概念之间的基本关系:
- 源文件:对应了典型的C/C++源代码
- 目标:多个源文件联合成为目标,目标通常是可执行文件或者库
- 目录:表示源码树中的一个目录,常常包含一个CMakeLists.txt文件,一或多个目标与之关联
- 本地生成器(Local generator):每个目录有一个本地生成器,负责为此目录生成Makefiles,或者工程文件
- 全局生成器(Global generator):所有本地生成器共享一个全局生成器,后者负责监管构建过程,全局生成器由CMake本身创建并驱动
CMake的执行开始时,会创建一个cmake对象并把命令行参数传递给它。cmake对象管理整体的配置过程,持有构建过程的全局信息(例如缓存值)。cmake会依据用户的选择来创建合适的全局生成器(VS、Makefiles等等),并把控制器转交给全局生成器(调用configure和generate方法)。
全局生成器负责管理配置信息,并生成所有Makefiles/工程文件。一般情况下全局生成器把具体工作委托给本地生成器执行,全局生成器为每个目录创建一个本地生成器。全局/本地生成器的分工取决于实现,例如:
- 对于VS,全局生成器负责生成解决方案文件,本地生成器负责每个目标的工程文件
- 对于Makefiles,全局生成器生成总体的Makefile,本地生成器则负责生成大部分Makefile
每个本地生成器包含一个cmMakefile对象,其中存放CMakeList.txt的解析结果。
CMake的每一个命令也被实现为C++类,该类主要包括两个成员:
成员 |
说明 |
InitialPass() |
接受当前目录的cmMakefile对象、命令参数作为入参。命令的执行结果存放在cmMakefile对象中 |
LastPass() |
在整个CMake工程所有命令的InitialPass()都执行后再执行。大部分命令不实现此方法 |
下图显示cmake、生成器、cmMakefile、命令等类型的关系:
目标
cmMakefile对象中存放的最重要的对象是目标(Targets),目标代表可执行文件、库、实用工具等。每个 add_library 、 add_executable 、 add_custom_target 命令都会创建一个目标。
库目标
下面的语句创建一个库目标:
|
# 创建一个静态库,包含两个源文件
add_library ( foo STATIC foo1 . c foo2 . c )
|
上述命令声明的foo可以作为库名称在工程的任何地方使用。CMake知道如何将此名称转换为库文件。命令的(可选的)第二个参数声明库的类型,有效值包括:
库类型 |
说明 |
STATIC |
目标必须构建为静态库 |
SHARED |
目标必须构建为共享库 |
MODULE |
目标必须构建为支持在运行时动态加载到可执行文件中的模块 对于除了Mac OS X之外的系统,此取值等价于SHARED |
如果不声明库类型,则CMake依据变量 BUILD_SHARED_LIBS 判断应该构建为共享库还是静态库,如果此变量不设置,构建为静态库。
可执行目标
与库目标类似,可执行目标也可以指定特定的选项,例如WIN32会导致操作系统调用WinMain而不是main函数。
读写目标属性
使用 set_target_properties 或者 get_target_properties 命令,或者更通用的 set_property 、 get_property 命令,可以读写目标的属性。 一个最常用的属性是 LINK_FLAGS ,可以指定链接标记。 使用 target_link_libraries 命令,可以指定目标需要链接的库的列表。列表的元素可以是库、库的全路径、通过add_library命令添加的库名称。
对于声明的每个库,CMake会跟踪其依赖的所有其它库,这种依赖关系需要用上述命令来设置:
|
add_library ( foo foo . cpp )
#foo库依赖于bar库
target_link_libraries ( foo bar )
add_executable ( foobar foobar . cpp )
#foobar显式依赖foo,隐式依赖bar,后两者都会被链接到foobar中
target_link_libraries ( foobar foo )
|
源文件
和Target类似,源文件也被建模为C++类,也支持读写属性(通过set_source_files_properties、get_source_files_properties或更加一般的命令)。最常用属性包括:
属性 |
说明 |
COMPILE_FLAGS |
针对特定源文件的编译器标记,可以包含-D、-I之类的标记 |
GENERATED |
指示此文件是否在构建过程中生成,这种文件在CMake首次运行时不存在,因而计算依赖关系时要特殊考虑 |
OBJECT_DEPENDS |
添加此源文件额外依赖的其它文件。CMake会自动分析C、C++的源文件依赖,因而此选项很少使用 |
WRAP_EXCLUDE |
CMake不直接使用该属性。但是某些命令和扩展读取该属性,判断何时/如何把C++类包装到其它语言,例如Python |
目录、生成器、测试、属性
其它偶尔可能用到的CMake类型包括Directory、Generator、Test、Property等。Directory、Generator、Test的实例同样(与目录、源文件类似)关联属性。
属性是一种键值存储,它关联到一个对象。读写属性最一般的方法是上面提到的get/set_property命令。所有可用的属性可以通过 cmake -help-property-list 得到。
目录的属性包括:
属性 |
说明 |
ADDITIONAL_MAKE_CLEAN_FILES |
指定一系列需要在mak clean时清除掉的文件的列表 默认的CMake会清除所有生成的文件 |
EXCLUDE_FROM_ALL |
指示此目录和子目录中所有的目标,是否应当从默认构建中排除 子目录的IDE工程文件/Makefile将从顶级IDE工程文件/Makefile中排除 |
LISTFILE_STACK |
最要在调试CMake脚本时用到,列出当前正在被处理的文件的列表 |
目录和生成器对象会在CMake处理你的源码树时自动创建。
变量和缓存条目(Cache Entries)
变量
CMakeLists中的变量和普通编程语言中的变量很类似,变量的值要么是单个值,要么是列表。CMake自动定义一系列重要的变量。
要引用变量,必须使用 ${VARNAME} 语法,要设置变量的值,需要使用set命令。 CMake中变量的作用域和普通编程语言略有不同,当你设置一个变量后,变量对当前CMakeLists文件、当前函数、 以及子目录的CMakeLists 、任何通过 INCLUDE 包含进来的文件 、任何 调用的宏或函数 可见。
当处理一个子目录、调用一个函数时,CMake创建一个新的作用域,其复制当前作用域全部变量,在子作用域中对变量的修改不会对父作用域产生影响。要修改父作用域中的变量,可以在set时指定特殊选项:
|
set ( name Alex PARENT_SCOPE )
|
变量的值可以是一个列表,这样的变量可以被展开为多个值:
|
set ( fruit apple peach strawberry )
foreach ( f $ { fruit } )
message ( "Do you want ${f}" )
endforeach ( )
|
缓存条目
有些时候你可能期望用户通过CMake的UI输入一些变量的值, 这时变量必须作为缓存条目。当CMake运行时,它会向二进制目录输出缓存文件(Cache file),缓存文件中的变量值通过CMake的UI展示给用户。
使用这种缓存的目的之一是,存储用户的选项,避免重新运行CMake时,反复要求用户输入相同的信息。
option 命令可以创建一个Boolean变量(ON/OFF)并将其存储在缓存中:
|
option ( USE _PNG "Do you want to use the png library?" )
|
用户可以通过UI设置USE_PNG的值,并且在未来这一值会保存在缓存中。使用CLion作为IDE时,可以在CMake窗口中点击Cache选项卡,查看或者编辑缓存条目:
除了 option 命令之外, find_file 也可以用来创建缓存条目。为 set 命令指定特殊参数,亦可:
|
# CACHE选项表示此变量作为缓存条目
# ON为默认值
# BOOL为变量类型,支持BOOL、PATH、FILEPATH、STRING
set ( USE_PNG ON CACHE BOOL "Do you want to use the png library?" )
|
缓存条目的 另外一个目的 是, 存储那些难以确定的关键变量,这些变量可能对用户不可见。通常这些变量是系统相关的,例如 CMAKE_WORDS_BIGENDIAN 。这类值可能需要CMake编译并运行一个程序来确定,一旦确定,即缓存。
位于缓存中的变量具有一个属性指示它是否为“进阶的”(advanced),默认的CMake GUI隐藏进阶条目。要标记一个缓存条目为进阶的,可以:
|
mark_as_advanced ( VAR_NAME )
|
某些情况下,你可能需要限制缓存条目的值范围在一个有限的集合中,这是可以设置条目的 STRINGS 属性,提供值列表。在GUI中,这种条目的字段会展示为下拉列表:
|
# 设置名为CRYPT_BACKEND的缓存条目的值为Open SSL
set ( CRYPT _BACKEND "Open SSL" CACHE STRING )
# 设置上述缓存条目的取值范围
set_property ( CACHE CRYPT_BACKEND PROPERTY STRINGS "Open SSL" "LibDES" )
|
即使变量存在于缓存,你仍然可以在CMakeLists中覆盖它(改变作用域中此变量的值)。只需要不带CACHE选项调用set命令,即可覆盖缓存中同名变量的值。
另一方面,一旦变量值已经缓存,你一般无法在CMakeLists中改变缓存的值(与上述覆盖是两回事)。也就是说,当缓存中有VARNAME时, set(VARNAME ON CACHE BOOL ) 不会有任何作用。要 强制改变缓存中的值并覆盖 当前作用域的值,可以联合使用 FORCE 和CACHE选项。
构建配置
构建配置允许工程使用不同方式构建:debug、optimized或者任何其它标记。CMake默认支持四种构建配置:
构建配置 |
说明 |
Debug |
启用基本的调试(编译器的)标记 |
Release |
基本的优化配置 |
MinSizeRel |
生成最小化的,但不一定是最快的代码 |
RelWithDebugInfo |
优化构建,但是同时携带调试信息 |
依据生成器的不同,CMake处理构建配置的方式有所差异,CMake尽可能遵循底层本地构建系统的约定,这意味着使用Makefiles、VS时构建配置影响构建的方式有所不同:
- VS支持构建配置的概念,在IDE中你可以选择Debug、Release配置,CMake只需要对接到VS的构建配置即可
- Makefile默认同时(CMake运行时)只能有一种配置被激活。使用 CMAKE_BUILD_TYPE 变量可以指定目标配置。如果此变量为空,则不给构建添加额外标记。如果此变量设置为上面四种构建配置之一,则相应的变量、规则——例如 CMAKE_CXX_FLAGS_ 被添加到compile line中。可以使用下面的方式来分别基于Debug、Release配置进行构建:
|
# 创建工程目录的两个兄弟目录,CD到其中分别执行:
ccmake . . / project - DCMAKE_BUILD_TYPE : STRING = Debug
ccmake . . / project - DCMAKE_BUILD_TYPE : STRING = Release
|
编写CMakeLists文件
CMake由CMakeLists.txt驱动,此文件包含构建需要的一切信息。
除了用于分隔命令参数,其余空白符一律被忽略。反斜杠可以用来指示转义字符。
基本命令
命令 |
说明 |
project |
顶层CMakeLists.txt中应当包含的第一个命令,声明工程的名字和使用的编程语言:
|
project ( projectname , [ CXX ] , [ C ] , [ JAVA ] , [ NONE ] )
|
如果不指定语言,默认CMake启用C/C++,如果指定为CXX则C语言的支持自动加入 对于工程中出现的每个project命令,CMake会创建一个顶级的IDE工程文件(或Makefile文件) 。此工程文件中会包含:
- 所有CMakeLists.txt中声明的目标
- 所有通过 add_subdirectory 命令添加的子目录。如果为命令指定EXCLUDE_FROM_ALL 选项,则此工程文件/Makefile不会包含到顶级工程文件/Makefile中,对于那种需要从主构建流传中排除的子工程(例如examples子工程),这个选项有用
|
set |
设置变量值或列表 |
remove |
从变量值的列表中移除一个单值 |
separate_arguments |
基于空格,把单个字符串分隔为列表 |
add_executable |
定义目标(可执行文件/库),以及目录由哪些源文件组成 对于VS,源文件将会出现在IDE中,但是默认的项目使用的头文件不会包含在IDE中,要改变此行为,只需要将头文件添加到源文件列表中 |
add_library |
流程控制命令
和普通编程语言一样,CMake支持条件、循环控制结构,同时支持子过程(macro、function)
if-else-endif
|
if ( FOO )
else ( FOO )
endif ( FOO )
# 上面把if的条件在else、endif中重复,这是可选的。因此我们可以简单的写作:
if ( FOO )
else ( )
endif ( )
|
在else、endif上重复条件,有助于if-else-endif匹配检查,特别是多层嵌套时。
elseif
CMake同样支持elseif:
|
if ( MSVC80 )
#...
elseif ( MSVC90 )
#...
elseif ( APPLE )
#...
endif ( )
|
条件表达式
条件命令支持受限的表达式语法,如下表所列:
语法 |
说明 |
if ( variable ) |
当if命令参数的值不是:0、FALSE、OFF、NO、NOTFOUND、*-NOTFOUND、IGNORE时,表达式的值为真,注意不区分大小写 variable可以不用${}包围 |
if ( NOT variable ) |
上面取反 variable可以不用${}包围 |
if ( variable1 AND variable2 ) |
逻辑与,所有逻辑操作支持用括号来提升优先级 |
if ( variable1 OR variable2 ) |
逻辑或 |
if ( num1 EQUAL num2 ) |
数字相等比较,其它操作符包括LESS、GREATER |
if ( str1 STREQUAL str2 ) |
字典序相等比较,其它操作符包括STRLESS、STRGREATER |
if ( v1 VERSION_EQUAL v2) |
marjor[.minor[.patch[.tweak]]] 风格的版本号相等比较,其它操作符包括VERSION_LESS、VERSION_GREATER |
if ( COMMAND commandname ) |
如果指定的命令可以调用 |
if ( DEFINED variable ) |
如果指定的变量被定义,不管它的值真假 |
if ( EXISTS file-name ) |
如果指定的文件或者目录存在 |
if ( IS_DIRECTORY name ) |
如果给定的name是一个目录 |
if ( IS_ABSOLUTE name ) |
如果给定的name是一个绝对路径 |
if ( n1 IS_NEWER_TAN n2 ) |
如果文件n1的修改时间大于n2 |
if ( variable MATCHES regex ) |
如果给定的变量或者字符串匹配正则式:
|
set ( name Alex )
if ( $ { name } MATCHES A . * x )
message ( $ { name } )
endif ( )
|
|
if ( string MATCHES regex ) |
操作符优先级
CMake操作符优先级从高到底:
- 括号分组:()
- 前缀一元操作符:EXISTS、COMMAND、DEFINED
- 比较操作符:EQUAL、LESS、GREATER及其变体,以及MATCHES
- 逻辑非:NOT
- 逻辑或于:AND、OR
foreach
|
foreach ( item list )
# do something with item
endforeach ( item )
|
此命令用于迭代一个列表,第一个参数是每次迭代使用变量的名称,其余参数为被迭代的列表
注意,在循环内部,你可以使用迭代变量 构造另外一个变量的名字 ,例如 ${NAME_OF_${item}}
while
此命令用于基于条件的迭代:
|
while ( $ { COUNT } LESS 2000 )
set ( TASK_COUNT , $ { COUNT } )
endwhile ( )
|
break
此命令用于中断foreach/while循环。
function
CMake中的函数很类似于C/C++函数。你可以向函数传递参数,除了依据形参名外,你还可以使用 ARGC 、 ARGV 、 ARGN 、 ARG0 、 ARG1 ...等形式,在函数内部访问入参。
函数内部是一个新作用域,类似于add_subdirectory生成的新作用域一样,函数调用前的作用域被拷贝并传递到函数内部,函数返回时,新作用域消失。
函数的第一个形参是函数的名称,其它参数构成传统的形参列表:
|
function ( println msg )
message ( $ { msg } "\n" )
set ( msg $ { msg } PARENT _SCOPE ) #设置父作用域中变量的值
endfunction ( )
println ( Hello )
|
return
此命令拥有从函数中返回,或者在listfile命令中提前结束。
macro
宏于函数类似,但是宏不会创建新的作用域。传递给宏的参数也不被作为变量看待,而是在执行宏前替换为字符串:
|
macro ( println msg ) #同样的,括号中第一个项目是宏的名称
message ( $ { msg } "\n" )
endmacro ( )
|
对于宏, ARGC、ARG0、ARG1等也可以使用。ARG0代表传递给宏的第一个参数。
检查CMake的版本
CMake是一个不断进化的工具,随着新版本的推出,会不断有新的命令被加入。很多时候,我们需要检查当前CMake版本是否支持某些特性。
我们可以使用if命令判断某个命令是否可用:
|
if ( COMMAND some_new_command )
#...
endif ( )
|
或者直接检查CMake的版本:
|
if ( $ { CMAKE_VERSION } VERSION _GREATER 1.6.1 )
endif ( )
|
另外,还可以声明要求的最低的CMake版本:
|
cmake_minimum_required ( VERSION 2.8 )
|
使用模块
所谓模块,仅仅是存放到一个文件中,一系列CMake命令的集合。我们可以用 include 命令将模块包含到CMakeLists.txt中。举例:
|
# 此模块用于查找TCL库
include ( FindTCL )
# 找到后,将其加入到链接依赖中
target_link_libraries ( FOO $ { TCL_LIBRARY } )
|
包含一个模块时,可以使用绝对路径,或者是基于CMAKE_MODULE_PATH的相对路径,如果此变量未设置,默认为CMake的安装目录的Modules子目录。
模块依据用途的不同可以分为:
类别 |
说明 |
查找类模块 |
查找软件元素——例如头文件、库——的位置 CMake提供了大量这类模块,如果目录库/头文件找不到,模块往往提供一个缓存条目,便于用户手工指定 下面是一个查找PNG模块的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# png库依赖于zlib
include ( FindZLIB ) # 查找zlib库
if ( ZLIB_FOUND ) # 往往在找到后设置LIBNAME_FOUND变量
# 查找头文件位置并存入变量
find_path ( PNG_PNG_INCLUDE_DIR png . h / usr / local / include / usr / include )
# 查找库文件位置并存入变量
find_library ( PNG_LIBRARY png / usr / lib / usr / local / lib )
if ( PNG_LIBRARY AND PNG_PNG_INCLUDE_DIR )
# 合并ZLIB头文件和库到PNG的
set ( PNG_INCLUDE_DIR $ { PNG_PNG_INCLUDE_DIR } $ { ZLIB_INCLUDE_DIR } )
set ( PNG_LIBRARIES $ { PNG_LIBRARY } $ { ZLIB_LIBRARY } )
# 设置已找到标记
set ( PNG_FOUND YES )
endif ( )
endif ( )
|
|
系统探测模块 |
探测系统的特性,例如浮点数长度、对ASCI C++刘的支持 很多这类模块具有Test、Check前缀,例如TestBigEndian、CheckTypeSize |
实用工具模块 |
用于添加额外的功能,例如处理一个CMake工程依赖于其它CMake工程的情况 |
策略(Policies)
由于某些原因,在版本升级后,CMake可能不提供完全的向后兼容。这意味着使用新版的CMake处理基于旧版本的CMakeLists.txt时会出现问题。CMake引入策略这一特性,帮助用户和开发者处理此向后兼容问题。
策略机制实现以下目标:
- 既有的工程能够用任何比CMakeLists作者使用的、更新版本的CMake构建。用户不应该需要修改CMakeLists代码,但是可能出现警告信息
- 新特性的修正,老接口的Bug修复应当被执行,而非因向后兼容性的要求而搁置
- 任何对CMake的改变,会导致CMakeLists文件必须更改的,应当加以文档说明。每个这样的改变应当具有唯一的标识符以便查阅文档,改变仅在工程提示自己支持的情况下才启用
- 最终将会移除向后兼容性的代码,不再支持古老版本的CMake。因此而构建失败的工程必须得到有价值的错误提示
CMake中的所有策略被分配一个 CMPNNNN 形式的名称,其中NNNN是一个整数值编号。策略同时支持 出于兼容性目的的旧行为,以及“正确的”新行为 。每个策略包含出现动机、新旧行为的详细说明文档。
设置策略
可以在工程中对每个策略进行配置,设置其值为NEW或者OLD,CMake将遵从测量设置,从而表现出不同的构建行为。
设置策略有几种方式,最简单的是设置策略为特定的CMake版本: cmake_policy(VERSION 2.6) 。这样所有2.6版本之前引入的策略都被标记为NEW,而2.6之后引入的策略则标记为“未设置”,以便产生警告信息。
注:cmake_minimum_required命令同样会设置策略,因此仅在需要定制子目录的策略时才以VERSION选项调用cmake_policy命令。
以SET选项调用cmake_policy可以明确的设置单个策略。以CMP0002为例,该策略的新行为要求所有逻辑目标具有全局独特的名字。下面的命令可以抑制存在重复目标名时的警告信息:
|
cmake_policy ( SET CMP0002 OLD )
|
链接到库
|
# 设置库的寻找目录
link_directories ( / path / to )
add_executable ( myexe myexe . c )
target_link_libraries ( myexe A B )
# 或者
add_executable ( myexe myexe . c )
# 使用绝对路径
target_link_libraries ( myexe / path / to / libA . so / path / to / libB . so )
|
链接到系统库
类Unix操作系统的系统库常常位于/usr/lib或者/lib目录。这些目录被链接器作为隐含的库搜索目录,因此 find_library(M_LIB m) 将从/usr/lib/libm.so定位到Math库。
问题是,某些平台会依据体系结构的不同,提供库的不同版本:
|
# IRIX机器
/ usr / lib / libm . so # ELF o32
/ usr / lib32 / libm . so # ELF n32
/ usr / lib64 / libm . so # ELF 64
# Solaris
/ usr / lib / lim . so # sparcv8架构
/ usr / lib / sparcv9 / lim . so # sparcv9架构
|
find_library命令名不知道各种体系结构特定的系统如何定义上面的目录规则,因此此命令可能找到不匹配的体系结构的库文件。
此问题的一个解决办法是让链接器自动寻找库所在目录(不使用link_directories或者指定绝对路径),不幸的是,此办法无法区分库的动态、静态版本。CMake实际使用的妥协做法是:
- 存在于隐含库搜索目录中的库,且链接器支持类似-Bstatic的选项来指定使用静态库,使用-l选项传递库名称
- 其它情况下,传递库绝对路径给链接器
共享库和可加载模块
共享库和可加载模块有利于重用:
- 缩短compile/link/run周期
- 共享库重新构建时,依赖于它的共享库/可执行文件甚至不需要重新构建
- 减少磁盘和内存消耗,因为同一共享库只需要一份
相比静态库,共享库更像是可执行文件,大部分系统要求共享库上具有可执行权限。和可执行文件一样,共享库可以链接到其它共享库。
对于静态库来说,一个object文件是最小单元;而共享库(包括其依赖)本身是一个最小单元。链接器可以从静态库中挑出需要的object文件,但是对于共享库及其依赖的其它共享库,都需要存在。
共享库和静态库的另外一个不同是库的声明顺序,指定静态库时顺序很重要,因为大部分链接器仅仅遍历库列表一次来寻找符号,依赖其它静态库的静态库必须放在列表前面。
当决定在工程使用共享库时,开发者必须面对几个问题。
共享库导出哪些符号
在大部分UNIX系统中,默认所有符号被导出。在Windows系统中,开发者必须明确告知编译器哪些符号被导入(使用符号时)/导出(创建符号时)
当从UNIX移植项目到Windows平台时,你可以:
- 创建一个额外的.def文件,或者
- 使用微软的C/C++语言扩展—— __declspec(dllexport) 、 __declspec(dllimport) 声明的符号分别被导出、导入
如果一个源文件在创建、使用一个库时都需要使用,则必须使用宏来处理。CMake在Windows下构建共享库(DDL)时,会自动定义宏 ${LIBNAME}_EXPORTS 。我们可以利用此宏:
|
#if defined(WIN32)
#if defined(vtkCommon_EXPORTS)
#define VTK_COMMON_EXPORT __declspec(dllexport)
#else
#define VTK_COMMON_EXPORT __declspec(dllimport)
#endif
#else
#define VTK_COMMON_EXPORT
#endif
|
这样,VTK_COMMON_EXPORT在UNIX中为空白;在Windows下构建共享库时为__declspec(dllexport)。
UNIX和Windows存在一个重要的和符号需求相关的差异:Windows上的DLL需要完全解析,也就是在创建时必须链接所有符号;而UNIX允许共享库在运行时从可执行文件或者其它共享库中获取符号。因而在UNIX中,CMake会给可执行目标一个标记,允许它被共享库调用。
另外一个需要提及的关于C++全局对象的陷阱是,加载或者链接了C++共享库的main函数,必须基于C++的编译器来链接,否则cout之类的全局对象可能在使用时尚未初始化。
共享库位置
由于链接到共享库的可执行文件必须在运行时能找到这些库,特殊的环境变量或者链接器标记必须被使用。
不同系统都提供了工具,用以查看可执行文件实际上使用的是哪个库:
- UNIX系统的 ldd 命令:显示可执行文件使用哪些库。在Mac OS X上使用 otool -L
- Windows系统的 depends 程序,功能类似
在很多UNIX系统中,可以使用环境变量 LD_LIBRARY_PATH 来告诉应用程序到哪里寻找库,而在Windows中,环境变量 PATH 同时用来寻找DLL和可执行文件。CMake会默认把运行时库的路径信息存放到可执行文件中,因此前述环境变量并不必须。但是某些时候你可能需要关闭这个特性,设置 CMAKE_SKIP_RPATH=false 即可。
共享库版本化
使用共享库时,运行时加载的库,应当与链接时期望的库的“版本”一致,即功能上没有不兼容的变化。
如何识别这种变化并没有一致的规范,某些UNIX系统通过soname来版本化共享库,所谓soname就是在共享库名称后附加可选的数字后缀,例如libx.so.1。仅当共享库的接口发生不兼容变化时soname才改变,如果libx从1.0到1.9维持了一致性的接口,它们的soname应该一致。注意soname和文件名不是一回事,1.3版本的libx的文件名可能叫libx.so.1.3,但是它的soname可能是libx.so.1。
soname是共享库文件的一个头字段。当可执行文件和共享库链接时,共享库的soname也存放到可执行文件中,因此在运行时加载时,可执行文件可以根据soname来寻找版本匹配的共享库文件。
当安装libx库到系统时,可以建立符号链接 libx.so -> libx.so.1 ,当libx出现不兼容升级时,则修改前述符号链接,例如 libx.so -> libx.so.2 。这样,新的程序总是和最新的libx版本进行链接。另一方面,这种符号链接用法可以扩展为,将某个soname链接到特定文件,例如 libx.so.1 -> libx.so.1.3 ,如果1.3版本有一个BUG在1.3.2中修复,可以修改前述符号链接为 libx.so.1 -> libx.so.1.3.2 ,这样基于libx.so.1链接的可执行文件在获得BUG修复的同时,能够找到正确的共享库。
CMake支持这种基于soname的版本号编码机制,只要底层平台支持soname,可以设置共享库目标的属性:
|
# VERSION,指定一个版本号,用于创建文件名
# SOVERSION,指定一个版本号,用于生成SONAME头
set_target_properties ( x PROPERTIES VERSION 1.2 SOVERSION 4 )
|
设置上述属性后,安装共享库时会产生如下文件和符号链接:
|
libx . so . 1.2
libx . so . 4 -> libx . so . 1.2
libx . so -> libx . so . 4
|
如果仅指定两个版本号中的一个,那么另外一个自动与之相同。
安装文件
软件通常被安装到和源码、构建树无关的位置上。CMake提供一个 install 命令,来说明一个工程如何被安装。正确使用这个命令后:
- 对于基于Makefile的生成器,用户只需要执行 make install 或者 nmake install 即可完成安装
- 对于基于GUI的平台,例如XCode、VS,用户只需要构建INSTALL目标
对install的每一次调用都会指定某些安装规则,这些规则会依据命令调用的顺序被执行。
install命令
install命令提供了若干“签名”(类似于子命令),签名作为第一个参数传入,可用的签名包括:
签名 |
说明 |
install(TARGETS...) |
安装工程中目标对应的二进制文件 |
install(FILES...) |
一般性的文件安装,包括头文件、文档、软件需要的数据文件 |
install(PROGRAMS...) |
安装不是由当前工程构建的文件,例如Shell脚本,与FILES签名类似,只是文件被授予可执行权限 |
install(DIRECTORY...) |
安装一个完整的目录树,例如包含了图标、图片的资源目录 |
install(SCRIPT...) |
指定一个用户提供的、在安装过程中(典型的是pre-install、post-install)执行的CMake脚本 |
install(CODE...) |
与SCRIPT类似,只是脚本以内联字符串形式提供 |
前四个签名都用于创建文件的安装规则,需要安装的目标、目录、文件紧接着签名列出。其余和安装相关的信息,以关键字参数的形式附加,大部分签名支持以下关键字:
关键字 |
说明 |
DESTINATION |
说明在何处放置被安装的文件,后面必须紧跟一个目录,此目录可以指定为绝对路径。如果使用相对路径,则相对于安装时指定的前缀,前缀可能由缓存条目CMAKE_INSTALL_PREFIX 指定。前缀的默认值:
- UNIX:/usr/local
- Windows:系统盘符:\Program Files\工程名称
|
PERMISSIONS |
说明如何设置被安装文件的权限(UNIX文件模式),仅在需要覆盖签名默认权限的情况下使用,可用的权限为:[OWNER|GROUP|WORLD][READ|WRITE|EXECUTE]、SETUID、SETGID 某些平台不完整支持上述权限,这种情况下自动忽略此关键字 |
CONFIGURATIONS |
指定规则应用到的构建配置(Release、Debug...)的列表 没有应用到的构建配置,不会执行此命令调用产生的规则 |
COMPONENT |
指定规则应用到的组件。某些工程把安装划分为多个组件,以便分别打包 例如某个工程可能包含三个组件:
- Runtime:包含运行软件需要的文件
- Development:包含基于软件进行开发需要的文件
- Documentation:包含软件的手册和帮助文档
没有应用到的组件,不会执行此命令调用产生的规则 默认情况下,会安装所有组件,因而此关键字不产生任何影响。如果要安装特定组件,必须手工调用安装脚本 |
OPTIONAL |
指示如果期望的待安装文件不存在时,不是一个错误,仅仅忽略之 |
TARGETS签名
以此签名调用instal命令,以便构建过程中创建的库、可执行文件。详细调用格式为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
install ( TARGETS
targets . . . # 基于add_executable/add_library创建的目标的列表
[
# 通过TARGETS签名安装的文件可以分为三类:
# ARCHIVE 静态库(UNIX/Cygwin/MinGW的.A、Windows的.LIB)
# DLL的可链接(Linkable)导入库(Cygwin/MinGW的.DLL.A、Windows的.LIB)
# LIBRARY 可加载模块、共享库(.SO)
# RUNTIME 可执行文件、动态链接库(.DLL)
# 如果指定下面一行的某个关键字,则后续的关键字仅针对特定类型的文件,否则针对所有文件
[ ARCHIVE | LIBRARY | RUNTIME | FRAMEWORK | BUNDLE | PRIVATE_HEADER | PUBLIC_HEADER | RESOURCE ]
[ DESTINATION < div > ]
[ PERMISSIOS permissions . . . ]
[ CONFIGURATIONS [ Debug | Release | . . . ] ]
[ COMPONENT component ]
[ OPTIONAL ]
[ EXPORT < export name > ]
# 下面的关键字仅用于LIBRARY类型,仅针对支持namelink、版本化共享库的平台
# 对于符号链接lib.so -> lib.so.1,后者是soname,前者称为namelink,namelink用于在链接时-l选项找到共享库的位置
# NAMELINK_ONLY导致仅仅共享库的namelink被安装;NAMELINK_SKIP导致除了namelink之外的文件被安装
# 如果不指定,那么namelink、共享库的文件都被安装
[ NAMELINK_ONLY | NAMELINK_SKIP ]
] [
. . . #仅需要针对不同类型(ARCHIVE|LIBRARY|RUNTIME...)分别设置关键字时,才会出现
]
)
|
注意上面代码中关于文件分类的规则,把同属于共享库的.SO、.DLL分别划分到LIBRART、RUNTIME是有意的设计,因为Windows平台下,DLL通常和EXE存放在一个目录,这样才能确保DLL能够被找到并加载。下面的调用确保共享库目标mySharedLib产生的所有文件在所有平台上均安装到期望的位置:
|
install ( TARGETS myExecutable mySharedLib myStaticLib myPlugin
RUNTIME DESTINATION bin COMPONENT Runtime
LIBRARY DESTINATION lib COMPONENT Runtime
ARCHIVE DESTINATION lib / myproject Component Development #静态库只有在二次开发时才需要
)
|
FILES签名
很多工程可能需要安装与目标无关的任何文件,这时可以使用一般目的的FILES签名:
|
install ( FILES files . . . #需要被安装的文件的列表,如果是相对路径,相对于当前Source目录
DESTINATION < dir > #目标位置,如果是相对路径,相对于安装Prefix
[ PERMISSIOS permissions . . . ] #默认权限644
[ CONFIGURATIONS [ Debug | Release | . . . ] ]
[ COMPONENT component ]
[ RENAME < name > ] #为文件指定新的名称,要求文件列表只有一个元素
[ OPTIONAL ]
)
|
PROGRAMS签名
某些工程可能安装额外的助手程序——Shell脚本或者Python脚本。这时可以使用PROGRAMS签名。此签名和FILES一样,只是默认权限为755。
DIRECTORY签名
有时候我们需要安装包含了大量资源文件的整个目录,此时使用DIRECTORY签名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
install ( DIRECTORY dirs . . . # 需要被安装的目录的列表,如果是相对路径,相对于当前Source目录
# 目标位置,此目录确保被创建。如果设置为share/myproject,则:
# data/icons 被安装到/share/myproject/icons,注意输入目录的所有祖先目录被忽略
# data/ 被安装到/share/myproject,注意结尾的斜杠,会导致此目录下所有内容被安装,因此data/类似于data/*
DESTINATION < dir >
# 默认权限:文件与FILES一样644,目录与PROGRAMS一样755,下面两个关键字用于修改默认行为
[ FILE_PERMISSIOS permissions . . . ]
[ DIRECTORY_PERMISSIOS permissions . . . ]
# 和文件来源保持一致的权限
[ USE_SOURCE_PERMISSIOS ]
[ CONFIGURATIONS [ Debug | Release | . . . ] ]
[ COMPONENT component ]
[
# 排除某些文件,或者为某些文件指定特殊的权限
# PATTERN用于UNIX风格通配符匹配;REGEX用于正则式匹配
[ PATTERN < pattern > | REGEX < regex > ]
# 是否把匹配的文件排除,不安装
[ EXCLUDE ]
# 设置匹配文件的权限
[ PERMISSIOS permissions . . . ]
]
[
. . . #排除或者chmod其它匹配文件
]
)
|
SCRIPT/CODE签名
拷贝文件到安装树下(Installation tree)不是安装过程的唯一内容,有时候需要执行特定的逻辑。这时可以使用SCRIPT或者CODE签名:
|
install ( SCRIPT scr . cmake ) # scr.cmake为某个CMake脚本名称
install ( CODE "message(Hello)" ) #直接跟着脚本内容
|
注意脚本不是在CMakeLists.txt处理过程中,而是在安装过程中执行,因而在脚本中不能访问CMakeLists.txt定义的变量。尽管如此, CMAKE_INSTALL_PREFIX 、 CMAKE_INSTALL_CONFIG_NAME 、 CMAKE_INSTALL_COMPONENT 会被设置为真实的安装前缀、构建配置、组件类型。
安装依赖的共享库
OS自带的、第三方提供的或者工程本身生成的共享库,是某些可执行文件能够运行的前提条件。由OS提供的自然不需要额外安装;工程本身产生的库由add_library命令说明,一般通过install命令安装到系统。需要额外考虑的是第三方库。
CMake提供两个模块,用于简化共享库的处理。
GetPrerequisites.cmake
使用该模块的 get_prerequisites() 函数,可以分析一个可执行文件的依赖。将可执行文件的路径传递给此函数,其会输出运行此文件必须的依赖库的列表, 包括传递性依赖 。该函数使用各平台上的Native工具:dumpbin(Windows)、otool(Mac)、ldd(Linux)进行依赖分析。
BundleUtilities.cmake
使用该模块的 fixup_bundle() 函数,可以依据可执行文件的相对位置,拷贝和修复共享库(依赖)。
对于Mac的bundle应用,需要的共享库会被嵌入到bundle中,并调用install_name_tool生成一个自包含bundle。
对于Widnows,需要的共享库会被拷贝到exe所在目录,可执行文件运行时会自动寻找并加载。
要使用fixup_bundle()函数,首先安装某个可执行目标,然后创建一个可以在安装时执行的CMake脚本,在此脚本中调用:
|
include ( BundleUtilities )
# 安装树中的可执行文件的路径
set ( bundle "${CMAKE_INSTALL_PREFIX}/myExecutable@CMAKE_EXECUTABLE_SUFFIX@" )
# 无法通过依赖分析到达的依赖库的列表
set ( other _libs "" )
# 可以寻找到前置依赖库的目录的列表
set ( dirs "@LIBRARY_OUTPUT_PATH@" )
# 调用
fixup_bundle ( "${bundle}" "${other_libs}" "${dirs}" )
|
导入和导出目标
CMake 2.6开始,支持在两个CMake工程之间导入导出目标。
导入
导入目标这一机制,用于将项目外部的磁盘文件转换为 逻辑的CMake目标 。在调用add_executable、add_library命令时,传递 IMPORTED 选项,即可定义导入目标。CMake 不会为导入目标生成构建文件 ,导入目标仅仅用于便利的引用外部的可执行文件和库。
下面的例子定义了一个导入的可执行文件,仅仅将其作为命令调用:
|
# 声明一个名为generator的导入目标
add_executable ( generator IMPORTED )
# 设置目标的实际位置
set_property ( TARGET generator PROPERTY IMPORT _LOCATION "/path/to/generator" )
# 调用自定义命令,即添加一条定制的构建规则
# 底层构建系统执行类似这样的命令/path/to/generator /project/binary/dir/generated.c
add_custom_command ( OUTPUT generated . c COMMAND generator generated . c )
|
下面的例子定义了一个导入的库,并与之链接:
1
2
3
4
5
6
7
8
9
10
11
12
|
add_library ( foo IMPORTED )
# Linux
set_property ( TARGET foo PROPERTY IMPORTED _LOCATION "/path/to/libfoo.a" )
# Windows下需要同时导入.lib和.dll
set_property ( TARGET foo PROPERTY IMPORTED _LOCATION "/path/to/libfoo.dll" )
set_property ( TARGET foo PROPERTY IMPORTED _IMPLIB "/path/to/libfoo.lib" )
# 具有多个构建配置的库,可以作为单个目标导入
set_property ( TARGET foo PROPERTY IMPORTED_LOCATION _RELEASE "/path/to/libfoo.a" )
set_property ( TARGET foo PROPERTY IMPORTED_LOCATION _DEBUG "/path/to/debug/libfoo.a" )
add_executable ( myexe src1 . c )
target_link_libraries ( myexe foo )
|
导出
尽管导入机制很有用,但是作为导入者来说,你必须知道目标在磁盘的位置。
使用导出机制,可以在提供目标文件的同时,提供一个文件,帮助其它工程导入。联合使用 install(TARGETS) 和 install(EXPORTS) 可以在安装目标的同时,把CMake文件也安装到机器上:
|
add_executable ( generator generator . c )
# EXPORT选项导致生成一个助手文件,该文件是一个CMake脚本,可以让其它工程方便的导入generator
install ( TARGET generator DESTINATION lib / myporj / generators EXPORT myproj - targets )
# 安装助手文件
install ( EXPORT myproj - targets DESTINATION lib / myproj )
|
助手文件的内容可以是:
|
# get_filename_component命令拥有得到一个全路径的某个部分
# 第一个参数:结果变量;第二个参数:待解析的路径;第三个参数,需要得到的部分,可以是DIRECTORY/NAME/EXT/PATH...
# CMAKE_CURRENT_LIST_FILE当前正在处理文件的路径
get_filename_component ( _self "${CMAKE_CURRENT_LIST_FILE}" PATH )
# 解析出安装前缀的绝对路径
get_filename_component ( PREFIX "${_self}/../.." ABSOLUTE )
# 添加导入目标
add_executable ( generator IMPORTED )
# 通过计算出的路径引用目标
set_property ( TARGET generator PROPERTY IMPORTED _LOCATION "${PREFIX}/lib/myproj/generators/generator" )
|
注意上面这个脚本依据自身位置动态计算出目标位置,即使移动安装目录,也不会失效。
其它工程只需要包含助手文件即可:
|
include ( / lib / myproj / myproj - targets . cmake )
# generator已经导入
add_custom_command ( OUTPUT generated . c COMMAND generator generated . c )
|
注意,单个助手文件可以容纳多个目标,甚至这些目标不在同一个目录中: