按:

这个系列与 cmake 有关的记录也好,笔记也好,成书也好,目前暂且归属在 cmake-hello 标记之下。

源码请访问:

https://github.com/hedzr/study-cmake
请注意源码仍在跟随我的笔记内容的迭代而更新中。

当前先期准备罗列内容,但今后可能会进一步填充以便令其不至于仅仅成为一个官方文档的译本。


此后各章节内容并不是按照一本参考手册的形式来编写的,而是基于一个模板型项目进行介绍,但尽力做到涵盖面超过 50% 而不是像一般的入门页面那样最多涉及到 10% 的内容。

如果你想要阅读喜闻乐见的入门页面以求快速开始的话,可以阅读此前的几个章节,那里是按照 Getting Started 的风格编写的。


这里是基本语法第二部分,包括变量的解说,以及一些附加内容。

变量

变量的创建无需声明语句,通过 set 指令可以一次性完成并为变量名设置一个值。

数据类型

值的数据类型不敏感,只有布尔值字符串字符串列表等几种类型。

布尔值

以下这些会被视为 FALSE:

  • OFF
  • FALSE
  • N
  • NO
  • 0
  • ”” (空串)
  • 没被指派值的变量
  • NOTFOUND
  • 任何结尾是 -NOTFOUND 的字符串

以下这些会被视为 TRUE:

  • ON
  • TRUE
  • Y
  • YE
  • YES
  • 1
  • 其他不归类为 FALSE 的字符串

字符串

字符串无需引号包围,其界定有赖于指令的括号包围。

数字表示的字面量依然是字符串,cmake 中没有所谓的数值类型。

1
2
3
4
set(A foo)
set(B bar)
set(C "this is foo bar")
set(D 3.14159)

以上全都是字符串。

带有空白的字符串必须以引号包围,防止被视作字符串列表——除非你想要的正是字符串列表。

字符串列表

在 cmake 中,字符串列表是不同于字符串的数据类型:由空白或者分号分隔的字符串形式被视为字符串列表。你可以将字符串列表想象为一个 token 数组。

请注意以下语句完全等效:

1
2
set(foo this is a list)
set(foo this;is;a;list)

foo 变量的实际值将会是 this,is,a,list 四个 token 的数组形式。

字符串列表有利于循环操作:

1
2
3
foreach(f ${foo})
    message(${f})
endforeach(f)

这将会遍历 foo 这个 token 数组后打印每个 token。

Quoted

有的时候用引号包围一个字符串有助于解决两个问题:

  1. 避免歧义
  2. 防止带有空白的字符串被分解为字符串列表类型

Escaped

在想要避免变量求值的情况下,可以使用转义语法:

1
message("\${foo} = ${foo}")

此外,你也可以使用 \t, \n 等 C 风格的转义字符。

定义变量

变量名是区分大小写的!

set 命令的格式如下:

1
2
3
4
5
6
7
8
# 设置变量
set(var value... [PARENT_SCOPE])

# 设置缓存条目
set(var value... [CACHE BOOL|FIELPATH|PATH|STRING|INTERNAL <docstring> [FORCE]])

# 设置环境变量
set(ENV{var} [value])

详细文档请查阅官网。

由于 function/macro 的演进原因,设置变量值时可以带上后缀 PARENT_SCOPE,这是专用于在某个 function/macro 中修改上级调用者的变量的特别手段。

检测变量名有否被定义过

1
if(DEFINED var|CACHE var|ENV{var})

变量求值

引用一个变量是通过 ${var} 的方式来展开的,这被称作变量的求值性展开。变量的展开可以递归进行,所以嵌套的 $ 表达式是合法且有效的。例如:

1
2
3
4
5
6
7
8
set(var hello)
set(foo var)

message(${foo})
message(${${foo}})
# 将会得到如下的输出:
# var
# hello

数学计算

cmake 中没有数值类型,因而也没有数值计算的运算符。

当需要数学计算时可以借助 math 指令来完成:

1
2
math(EXPR var "1 + 2 * 3")
message("var = ${var}")

通配符

TODO

Glob 指令不被建议使用。

正则式计算

string 指令具有正则式匹配的计算能力:

1
2
3
set(TEXT "ab,cc,df,gg")
string(REGEX MATCHALL "((.)\\2)" RESULT "${TEXT}")
message("Result: ${RESULT}") 

此外,if 指令也有相似的能力:

1
if(<variable|string> MATCHES regex)

还有一些场景(while)具有正则式的支持,你可以在使用时查阅某一指令的官方文档来获得详情。

CMake 的正则式具有如下的语法:

  • ^ 匹配一行或一字符串开头
  • $ 匹配一行或一字符串结尾
  • . 匹配单一字符或一个新行
  • [ ] 匹配括号中的任一字符
  • [^ ] 匹配不在括号内的任一字符
  • [-] 匹配指定范围内的字符
  • * 匹配0次或多次
  • + 匹配一次或多次
  • ? 匹配0次或一次
  • () 保存匹配的表达式并用随后的替换它

缓存

变量现在具有作用域范围。

你以前知道的都是全局变量,例如你在使用 set 做变量定义时总会建立一个全局变量。

但现在情况已有所不同:

  • 在 function 中的 set 语句只会声明一个局部变量(local-scope)。

  • 在一个 CMakeLists.txt 中声明的变量是 directory-scope 的,它们并不能自动被带入 add_subdirectory 所引入的脚本之中。

  • 通过 PARENT_SCOPE 后缀能够向上访问/操作/修改到上级作用域中的变量。

    例如上级目录中的 CMakeLists.txt 的脚本,或者 function 的调用者。

  • 通过 CACHE 方式 set 一个变量,则该变量可以跨越目录级别的作用域限定

  • 使用 mark_as_advance(var) 标记一个变量令其具有全局作用域。

所谓的缓存变量,技术上讲是指被持久化存储在 CMakeCache.txt 中的变量。它很有用,但优先级较低:普通的正常的同名变量会掩盖(shadow)缓存变量,所以你可以从命令行传递变量新值来改变它。由于 IDE 的存在,缓存变量也是让你可以提前准备变量默认值的一种手段。

示例1:

1
set(LIB_A_PATH "/some/default/path" CACHE PATH "Path to lib A")

如果你在反复操作一个缓存变量,请注意除了首次set之外,今后的 set 操作除非带有 FORCE 后缀,否则都不会被持久化到缓存中;此外,FORCE 还会从当前范围中删除普通变量,如果有的话。所以:

1
2
3
4
5
6
7
8
9
10
11
cmake_minimum_required(VERSION 2.4)
project(VariablesTest)

set(VAR "CACHED-init" CACHE STRING "A test")
message("VAR = ${VAR}")

set(VAR "NORMAL")
message("VAR = ${VAR}")

set(VAR "CACHED" CACHE STRING "A test" FORCE)
message("VAR = ${VAR}")

First Run的输出

1
2
3
VAR = CACHED-init
VAR = NORMAL
VAR = CACHED

第二轮的输出

1
2
3
VAR = CACHED
VAR = NORMAL
VAR = CACHED

属性

概述

什么是属性?

最恰当的思考方式是,一个Target对象的成员变量即为属性。

我们已经理解到现在每个 Target 都是一个对象,上面附着了一系列的东西,所以属性这种东西是其一:

1
2
3
4
5
6
7
8
9
10
11
12
13
add_library(${PROJECT_NAME})
add_library(libs::${PROJECT_NAME} ALIAS ${PROJECT_NAME})

target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_11)
target_sources(${PROJECT_NAME} PRIVATE src/${PROJECT_NAME}.cc)
target_include_directories(${PROJECT_NAME}
        PUBLIC
          $<INSTALL_INTERFACE:include>
          $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        PRIVATE
          ${ZLIB_INCLUDE_DIRS}
        )
target_link_libraries(${PROJECT_NAME} PRIVATE ${ZLIB_LIBRARIES})

至于什么 include_directories 啦,public_headers 啦,统统都是 target 上的一个属性。

set_property

1
2
set_property(TARGET TargetName
             PROPERTY CXX_STANDARD 11)

set_target_properties

当你使用 set_target_properties 指令时,大量的 target_xxx 指令都可以被放弃,因为 set_target_properties 能够将 target_xxx 指令一网打尽。这其实也很复杂,所以该怎么改写旧的 cmake 脚本以及如何运用 set_target_properties 指令需要时时参阅:Properties on Targets。实际的用法可以看如下的样本:

1
2
3
4
5
set_target_properties(app
    PROPERTIES
    LINK_FLAGS          -static
    LINK_FLAGS_RELEASE  -s
    )

小部分的 target_xxx 指令可能依然是重要的,不能被 set_target_properties 所简单地替代。例如 target_include_directories 和 target_link_libraries 都具有可见性限定(PUBLIC,PRIVATE,INTERACE),所以对于库作者来说这些可见性可能相当重要,目前在 cmake 3.19 中它们仍然无法使用 set_target_properties 来完成限定。

但小部分特例也是存在的,例如 INTERFACE 的替代品:

1
2
3
4
5
set_target_properties(Foo::bar PROPERTIES
    INTERFACE_COMPILE_FEATURES "cxx_std_14"
    INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include/"
    INTERFACE_SOURCES "${_IMPORT_PREFIX}/include/foo/bar.hpp"
)

参考

cmake-properties(7) — CMake 3.19.0 Documentation

环境变量

操作系统环境变量

set(ENV{var} ...) 的方式可以设置环境变量。

类似地,ENV{var} 表达式能够访问到操作系统的环境变量。

1
message("$ENV{USER}")

cmake 的环境变量

对于 cmake 来说,一系列特殊的内建变量被认为是 cmake 的所谓环境变量,你可以参考 cmake-env-variables (7) 来获得有关说明。

总结

CMake 的变量系统,缓存特性是设计得比较灾难的,这是一个长周期开源项目不得不面对的问题。

虽然我们会遇到很多一开始设计得很差的作品,后来虽然也成功了,但设计之初的败笔无法被修补所带来的问题往往会波及到后来的很多设计决定。但 CMake 身上发生的事情可能并不那么简单:那些一开始设计的很成功很规范严谨的项目,却又没能成功地延续下来的项目,实在是太多了。

CMake 一开始的设计是成功的吗?

这个问题,或许不会有人能得出足以令多数人认可的结论。

CMake 现在的设计是“现代”的吗?

真的,我不能回答你。这太困难了,想要回答它的话。

正因为 cmake 中的逻辑含混、概念含混的地方很多,因而后来的 cmake 才会大力推行 Modern 风格,并鼓励新的惯用法。

生成式表达式

CMake 会在一开始解析 Source Tree 中的 CMakeLists.txt 并生成构建配置文件,例如 makefile 等等。这是我们已经知道的阶段一的内容,然后我们会通过 cmake –build 命令执行构建阶段,这一阶段根据我们的开发进度将会被反复进行,而阶段一是一次性的,除非我们修改了 CMakeLists.txt 的脚本内容。

生成式表达式(Generator Expressions)是由 cmake 引入的新的特性。它的特别之处在于其求值动作发生在阶段一完成之后。或者说,生成式表达式在阶段一的结束时分才会被统一求值。之所以要这么做,是因为 cmake 需要解析完所有的 CMakeLists.txt 之后才能确定全部变量、Targets 等的配置结果,而生成式表达式的求值计算是需要这个配置结果的参与的,例如 Targets 的最终属性定义等。

生成式表达式的包含三种类型:

  1. 逻辑表达式
  2. 信息表达式
  3. 输出表达式

生成式表达式基本形式为 $<KEYWORD:value>,它会对 KEYWORD 求值。

逻辑表达式

逻辑表达式将被计算为 0 或 1,在 cmake 中这也是布尔量 TRUE 和 FALSE 的含义。由于这一原因,逻辑表达式常常被嵌入另一表达式中,例如:

1
$<$<CONFIG:Debug>:DEBUG_MODE>

$<CONFIG:Debug> 是一个逻辑表达式,它会测试当前的构建类型是 debug 还是 release。外层的表达式求值的结果为:如果构建类型为 debug,则结果为 DEBUG_MODE,否则为空。

全部有效的逻辑表达式语法请参考官网:逻辑表达式

最关键的两个逻辑表达式为:

1
2
3
$<0:...>      返回空串
$<1:...>      返回 ...
$<BOOL:...>   BOOL 表达式为1 则返回 ...;否则返回空串

$<$<CONFIG:Debug>:DEBUG_MODE> 的求值就是借助上述的基本形式求取外层值的。

其它常见的逻辑表达式还包括:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$<AND:?[,?]...>
1 if all ? are 1, else 0

The ? must always be either 0 or 1 in boolean expressions.

$<OR:?[,?]...>
0 if all ? are 0, else 1

$<NOT:?>
0 if ? is 1, else 1

$<STREQUAL:a,b>
1 if a is STREQUAL b, else 0

$<EQUAL:a,b>
1 if a is EQUAL b in a numeric comparison, else 0

$<CONFIG:cfg>
1 if config is cfg, else 0. This is a case-insensitive comparison. The mapping in MAP_IMPORTED_CONFIG_<CONFIG> is also considered by this expression when it is evaluated on a property on an IMPORTED target.

$<PLATFORM_ID:comp>
1 if the CMake-id of the platform matches comp, otherwise 0.

$<C_COMPILER_ID:comp>
1 if the CMake-id of the C compiler matches comp, otherwise 0.

$<CXX_COMPILER_ID:comp>
1 if the CMake-id of the CXX compiler matches comp, otherwise 0.

$<VERSION_GREATER:v1,v2>
1 if v1 is a version greater than v2, else 0.

$<VERSION_LESS:v1,v2>
1 if v1 is a version less than v2, else 0.

$<VERSION_EQUAL:v1,v2>
1 if v1 is the same version as v2, else 0.

$<C_COMPILER_VERSION:ver>
1 if the version of the C compiler matches ver, otherwise 0.

$<CXX_COMPILER_VERSION:ver>
1 if the version of the CXX compiler matches ver, otherwise 0.

$<TARGET_POLICY:pol>
1 if the policy pol was NEW when the ‘head’ target was created, else 0. If the policy was not set, the warning message for the policy will be emitted. This generator expression only works for a subset of policies.

$<COMPILE_FEATURES:feature[,feature]...>
1 if all of the feature features are available for the ‘head’ target, and 0 otherwise. If this expression is used while evaluating the link implementation of a target and if any dependency transitively increases the required C_STANDARD or CXX_STANDARD for the ‘head’ target, an error is reported. See the cmake-compile-features(7) manual for information on compile features.

它们的用途和用法不难以理解。

信息表达式

信息表达式被求值为某些文字。求值结果可以被直接使用,例如:

1
include_directories(/usr/include/$<CXX_COMPILER_ID>/)

其含义如同你猜想的那样,它可以被展开为具体值:/usr/include/GNU/ or /usr/include/Clang/ 等等。

你也可以组合使用它们:

1
$<$<VERSION_LESS:$<CXX_COMPILER_VERSION>,4.2.0>:OLD_COMPILER>

测试你的编译器版本号(CMAKE_CXX_COMPILER_VERSION)是不是小于 4.2.0,如果是则返回一个字串“OLD_COMPILER”。

有效的信息表达式有:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
$<CONFIGURATION>
Configuration name. Deprecated. Use CONFIG instead.

$<CONFIG>
Configuration name

$<PLATFORM_ID>
The CMake-id of the platform. See also the CMAKE_SYSTEM_NAME variable.

$<C_COMPILER_ID>
The CMake-id of the C compiler used. See also the CMAKE_<LANG>_COMPILER_ID variable.

$<CXX_COMPILER_ID>
The CMake-id of the CXX compiler used. See also the CMAKE_<LANG>_COMPILER_ID variable.

$<C_COMPILER_VERSION>
The version of the C compiler used. See also the CMAKE_<LANG>_COMPILER_VERSION variable.

$<CXX_COMPILER_VERSION>
The version of the CXX compiler used. See also the CMAKE_<LANG>_COMPILER_VERSION variable.

$<TARGET_FILE:tgt>
Full path to main file (.exe, .so.1.2, .a) where tgt is the name of a target.
返回 Target “tgt” 的主文件的完整路径。例如 '/home/ss/projects/aa/bin/aa.so'

$<TARGET_FILE_NAME:tgt>
Name of main file (.exe, .so.1.2, .a).

$<TARGET_FILE_DIR:tgt>
Directory of main file (.exe, .so.1.2, .a).

$<TARGET_LINKER_FILE:tgt>
File used to link (.a, .lib, .so) where tgt is the name of a target.

$<TARGET_LINKER_FILE_NAME:tgt>
Name of file used to link (.a, .lib, .so).

$<TARGET_LINKER_FILE_DIR:tgt>
Directory of file used to link (.a, .lib, .so).

$<TARGET_SONAME_FILE:tgt>
File with soname (.so.3) where tgt is the name of a target.

$<TARGET_SONAME_FILE_NAME:tgt>
Name of file with soname (.so.3).

$<TARGET_SONAME_FILE_DIR:tgt>
Directory of with soname (.so.3).

$<TARGET_PDB_FILE:tgt>
Full path to the linker generated program database file (.pdb) where tgt is the name of a target.

See also the PDB_NAME and PDB_OUTPUT_DIRECTORY target properties and their configuration specific variants PDB_NAME_<CONFIG> and PDB_OUTPUT_DIRECTORY_<CONFIG>.

$<TARGET_PDB_FILE_NAME:tgt>
Name of the linker generated program database file (.pdb).

$<TARGET_PDB_FILE_DIR:tgt>
Directory of the linker generated program database file (.pdb).

$<TARGET_PROPERTY:tgt,prop>
Value of the property prop on the target tgt.

Note that tgt is not added as a dependency of the target this expression is evaluated on.

$<TARGET_PROPERTY:prop>
Value of the property prop on the target on which the generator expression is evaluated.

$<INSTALL_PREFIX>
Content of the install prefix when the target is exported via install(EXPORT) and empty otherwise.

它们的用途和用法不难以理解。

输出表达式

输出表达式(常常会通过某些输入)计算出另一个结果。

以 JOIN 为例:

1
$<JOIN:list,txt> 将 list 数组合并为一个字符串,且以txt为数组元素之间的分隔符

所以我们在使用如下表达式时:

1
-I$<JOIN:$<TARGET_PROPERTY:${PROJECT_NAME},INCLUDE_DIRECTORIES>, -I>

我们可能会得到这样的结果:

1
-I/Users/someone/peoject/study-cmake/z11-m1/libs/sm-lib/include -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/usr/include

有效的输出表达式有:

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
26
27
28
29
30
31
32
$<ANGLE-R>
A literal >. Used to compare strings which contain a > for example.

$<COMMA>
A literal ,. Used to compare strings which contain a , for example.

$<SEMICOLON>
A literal ;. Used to prevent list expansion on an argument with ;.

$<TARGET_NAME:...>
Marks ... as being the name of a target. This is required if exporting targets to multiple dependent export sets. The ... must be a literal name of a target- it may not contain generator expressions.

$<LINK_ONLY:...>
Content of ... except when evaluated in a link interface while propagating Transitive Usage Requirements, in which case it is the empty string. Intended for use only in an INTERFACE_LINK_LIBRARIES target property, perhaps via the target_link_libraries() command, to specify private link dependencies without other usage requirements.

$<INSTALL_INTERFACE:...>
Content of ... when the property is exported using install(EXPORT), and empty otherwise.

$<BUILD_INTERFACE:...>
Content of ... when the property is exported using export(), or when the target is used by another target in the same buildsystem. Expands to the empty string otherwise.

$<LOWER_CASE:...>
Content of ... converted to lower case.

$<UPPER_CASE:...>
Content of ... converted to upper case.

$<MAKE_C_IDENTIFIER:...>
Content of ... converted to a C identifier.

$<TARGET_OBJECTS:objLib>
List of objects resulting from build of objLib. objLib must be an object of type OBJECT_LIBRARY. This expression may only be used in the sources of add_library() and add_executable() commands.

调试

生成式表达式不可能用 message 的方式打印出来,因为 message 的计算太早了,而生成式表达式尚未确定 CMakeLists.txt 的所有内容,所以它还没有被求值。

既然如此,我们将需要另外的途径来对生成式表达式求值并调试该表达式。

file 指令具有我们想要的能力:

1
file(GENERATE OUTPUT <filename> CONTENT <string-with-generator-expression>)

例如:

1
file(GENERATE OUTPUT ${CMAKE_GENERATED_DIR}/abc.log CONTENT "$<INSTALL_INTERFACE:include> - $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>")

此外,使用定制命令的方法也可以:

1
2
3
add_custom_command(TARGET mytarget POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E echo 
  "target dir = $<TARGET_FILE_DIR:mytarget>")

但这种方法在复杂的环境中,在某些IDE中,可能不能良好地表现。

其它

与代码通信

cmake 可以和 c++ 源代码通讯。准确地说,cmake 可以使用 m4 风格将 .in 文件输出为源代码文件,从而形成变相的代码生成效果。详情请阅 configure_file 指令。

熟悉 autoconf 体系的人应该会对 m4 具有爱恨交加的记忆。我就是如此,刚开始时真的拿着 makefile.in 和 config.h.in 毫无办法。

它们不是简简单单的模版中的变量替换。

如果只是这么个字符串替换你以为我真的就会弄不懂吗?naive!我是对那些神仙一般的什么 HAVE_xxx,什么 #if MIPS_XXX 什么的毫无抵抗力啊,想要寻找一句参考文字都找不到,你回 1990 年去试试看,有办法就算我输(哦,不太安全,你回 1990 的中国的小山村里去试试看,加州生人就别来凑热闹了——现在的网路真的太痛苦了,写文字率性一丁点都会超危险——真的很想回到火星去)。

好,回到主题来。

现在没有那么复杂的 config.h 了,除非你会跨超多平台,否则你这辈子编程都可能不遇到 HAVE_xxx 问题。所以我们现在有更简单的实例来解释代码生成问题。

version.in 和版本迭代的管理

我们通过一个 .version.cmake 文件来维持整个 Source Tree 的版本迭代问题,并从这里出发衍生出一整套版本迭代的管理方法。

./.version.cmake

首先建立一个 .version.cmake 文件 ,内容为:

1
set(VERSION 0.3.1.2)

版本号是你自定的。这个文件内容尽可能简单,是为了让 shell 语句能够很方便地递增它,进一步地,CI 工具在通过 shell 指令操作 .version.cmake 后将能够完成 nightly 构建任务。

./CMakeLists.txt

在 Source Tree 的顶级目录中的 CMakeLists.txt 中首先会有如下的序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required(VERSION 3.9..3.19)
set(CMAKE_SCRIPTS "cmake")
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS}/modules;${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS};${CMAKE_SOURCE_DIR};${CMAKE_MODULE_PATH}")
include(version-def)
...
...
project(study-cmake
        VERSION ${VERSION}
        DESCRIPTION "study for cmake"
        LANGUAGES C CXX)
...
...
include(versions-gen)

Line 3 让 Source Tree 中的 ., ./cmake./cmake/modules 均能被自动搜索得到。

./cmake/version-def.cmake

于是我们可以建立 ./cmake/version-def.cmake 文件:

1
2
3
4
5
6
if (EXISTS ${CMAKE_SOURCE_DIR}/.version.cmake)
  include(.version)
else()
  message("version decl file ignored")
  set(VERSION 0.1.0.1)
endif ()

它会载入此前的 .version.cmake 文件中的 VERSION 定义。

请注意 前面的 CMakeLists.txt 中,project 指令会采纳这个 VERSION 变量值。

../cmake/version-gen.cmake

最后来说明我们怎么做代码生成。

首先看 version-gen.cmake 的文件内容的简化版:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
if (EXISTS "${CMAKE_SOURCE_DIR}/.git")
  execute_process(
          COMMAND git rev-parse --abbrev-ref HEAD
          WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
          OUTPUT_VARIABLE GIT_BRANCH
          OUTPUT_STRIP_TRAILING_WHITESPACE
  )

  execute_process(
          COMMAND git log -1 --format=%h
          WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
          OUTPUT_VARIABLE GIT_COMMIT_HASH
          OUTPUT_STRIP_TRAILING_WHITESPACE
  )
else (EXISTS "${CMAKE_SOURCE_DIR}/.git")
  set(GIT_BRANCH "")
  set(GIT_COMMIT_HASH "")
endif (EXISTS "${CMAKE_SOURCE_DIR}/.git")

# get_git_head_revision(GIT_REFSPEC GIT_SHA1)
string(SUBSTRING "${GIT_COMMIT_HASH}" 0 12 GIT_REV)
if (NOT GIT_COMMIT_HASH)
  set(GIT_REV "0")
endif ()

message(STATUS "- Git current branch: ${GIT_BRANCH}")
message(STATUS "- Git commit hash:    ${GIT_COMMIT_HASH}")
message(STATUS "- Git rev:            ${GIT_REV}")

if (CMAKE_GENERATED_DIR)
else ()
  message(FATAL " >> ERROR: please include target-dirs.cmake at first.")
  # we need CMAKE_GENERATED_DIR at present.
endif ()

if (EXISTS ${xVERSION_IN})
  message(STATUS "Generating version.h from ${xVERSION_IN} to ${CMAKE_GENERATED_DIR} - Version ${PROJECT_VERSION}...")
  configure_file(
          ${xVERSION_IN}
          ${CMAKE_GENERATED_DIR}/version.h
  )
  message(STATUS "Generated: ${CMAKE_GENERATED_DIR}/version.h")
endif ()

这里有很多指令,但功用并不困难,我们会承认 PROJECT_VERSION 变量值,它是来自于 VERSION 变量定义的(请回顾 .version.cmake 文件和 project 指令在 CMakeLists.txt 中的实际运用),此外我们也试图提取 git 中的版本号信息。

我们并不做决定,决定的策略被编写在 ./cmake/version.h.in 文件中,并在 configure_file 指令的加持下被生成为一个源代码文件 ${CMAKE_GENERATED_DIR}/version.h。这个文件所在的目录被加入到 include_directories 搜索路径中,所以我们可以在源代码的任何位置使用它:

1
2
// all.h
#include "version.h"
configure_file 指令

configure_file 指令的作用就是装入一个 input 文件,经过 m4 风格的字符串替代之后产生一个 output 文件。

如果你还在 autoconf 年代,那么你可以在 input 文件中写这样的形式:

1
#cmakedefine var ...

根据条件检测结果的不同,该语句在输出时将被修改为 #define var 或者 /* #undef var */ 的形式。

你还可以使用 #cmakedefine01 var ... 语句,它将被替换成 #define var 1#define var 0 的形式。

例如如下的 cmake 脚本:

1
2
3
4
5
option(ENABLE_BOOST "Enable Boost" ON)
if(ENABLE_BOOST)
  set(BOOST_STRING "foo")
endif()
configure_file(config.h.in config.h)

对于 config.h.in 内容为:

1
2
#cmakedefine01 ENABLE_BOOST
#cmakedefine BOOST_STRING "@BOOST_STRING@"

则输出 config.h 内容为:

1
2
#define ENABLE_BOOST 1
#define BOOST_STRING "foo"

请注意在in文件模版中的 @var@ 文字将被替换为 var 变量的具体值。

./cmake/version.h.in

言归正传,一个 version.h.in 的样本为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef ___VERSION_H
#define ___VERSION_H

#define xT(str)                str

/*  NB: this file is parsed by automatic tools so don't change its format! */
#define xMAJOR_VERSION      @PROJECT_VERSION_MAJOR@
#define xMINOR_VERSION      @PROJECT_VERSION_MINOR@
#define xPATCH_NUMBER       @PROJECT_VERSION_PATCH@
#define xRELEASE_NUMBER     @PROJECT_VERSION_TWEAK@

#define xPROJECT_NAME       xT("@PROJECT_NAME@")
#define xVERSION_STRING     xT("@PROJECT_VERSION@")
#define xARCHIVE_NAME       xT("@ARCHIVE_NAME@")

#define xGIT_BRANCH         xT("@GIT_BRANCH@")
#define xGIT_COMMIT_HASH    xT("@GIT_COMMIT_HASH@")

#endif //___VERSION_H

我们在这里只用到了 configure_file 的 字符串替换能力。

./cmake-debug-build/generated/version.h

而所生成的 version.h 可能会像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef ___VERSION_H
#define ___VERSION_H

#define xT(str)                str

/*  NB: this file is parsed by automatic tools so don't change its format! */
#define xMAJOR_VERSION      0
#define xMINOR_VERSION      3
#define xPATCH_NUMBER       1
#define xRELEASE_NUMBER     2

#define xPROJECT_NAME       xT("study-cmake")
#define xVERSION_STRING     xT("0.3.1.2")
#define xARCHIVE_NAME       xT("study-cmake-0.3.1.2")

#define xGIT_BRANCH         xT("master")
#define xGIT_COMMIT_HASH    xT("55ced0f")

#endif //___VERSION_H

运行其它程序

在上一节的实例中我们实际上演示了运行其它程序的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (EXISTS "${CMAKE_SOURCE_DIR}/.git")
  execute_process(
          COMMAND git rev-parse --abbrev-ref HEAD
          WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
          OUTPUT_VARIABLE GIT_BRANCH
          OUTPUT_STRIP_TRAILING_WHITESPACE
  )

  execute_process(
          COMMAND git log -1 --format=%h
          WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
          OUTPUT_VARIABLE GIT_COMMIT_HASH
          OUTPUT_STRIP_TRAILING_WHITESPACE
  )
else (EXISTS "${CMAKE_SOURCE_DIR}/.git")
  set(GIT_BRANCH "")
  set(GIT_COMMIT_HASH "")
endif (EXISTS "${CMAKE_SOURCE_DIR}/.git")

更多

更多其它内容将来或会补充。

🔚