按:

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

源码请访问:

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

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


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

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


这里是基本语法第一部分,包括基本语法结构,流程控制等内容。

使用

cmake 默认载入当前目录中的 CMakeLists.txt 并展开脚本解释:

1
$ cmake

此时此命令等同于 cmake .

你可以建立 build 子目录来开始一个 Out-of-source Build:

1
2
3
mkdir build
cd build
cmake ..

此外,你可以直接执行一个脚本文件:

1
cmake -P inc-version.cmake

这时 cmake 工作得如同常见的脚本解释器。


随后的章节将会针对 cmake 脚本的语法进行介绍。

基本语法

cmake 脚本由一系列指令构成,所以从文法上看,cmake 是注释、空白、指令的集合。

注释

注释以 ‘#’ 开头,仅支持行注释,类似于 shell。

空白

cmake 脚本中的 空白,换行,tab 全被忽略。

由于每条指令总是以右括号结尾,所以 cmake 不需要 ‘;’ 这样的句末标识符,同时这也意味着 cmake 的词法分析器可以将一切空白全数抹除,而语义分析依然不受影响。

有的高级语言在被编译时,编译器必须将所有空白压缩为一个单独的词法单位,此时空白被当作有意义的分隔符。有的语言则有赖于换行字符作为语义分析的分界线。

指令

非注释的非空行,一律被视作指令。

行末如果有注释,注释被完全忽略。

指令都是由指令名及其参数构成,格式形如:

1
directive ( [param1 [param2 ...]] )

其中,参数可以为空。而且括号之间的内容允许被分列于多行中。如果未能识别到结束时的右括号,cmake 会持续地将后继行当作时指令参数的一部分进行分析。例如一个实例:

1
2
3
4
set(A v1
   v2
   v3 # v3 is another one
   )

这一实例的含义为建立和设置一个变量 ‘A’ 其值被设定为 “v1 v2 v3” 序列。‘A’ 是变量名。

行末的注释不会影响语义。

关于变量的细节后续章节会进一步阐述。

对于指令来说,只有参数列表的存在。

但对于特定指令来说,像变量名、变量值这样的语法单位还是存在的,尽管从具体指令的角度看它们仍然是指令的参数表。

指令尚可被按照功用细分为函数、宏、命令等各种细类。但这只是有助于你的理解,而 cmake 自身则是一律视作为指令。

指令名与变量名

cmake 脚本中的指令名(命令名,function 名,macro 名等),其大小写并不敏感,所以 “SET” 和 “set” 甚至 “SeT” 是等价的。

CMake 推崇你指令名一律小写,内建变量名一律大写。

至于由脚本编写者如你所建立的变量名则视个人偏好而定。

作为一个建议,作为 cmake 工具脚本库作者的你变量名采用大写,你对内建变量体系的扩展采用大写,你的内部变量小写并以 “_” 开头。

而对于 cmake 最终运用者来说,则任由自定。

变量名是区分大小写的!

合法的变量名由字母、数字和下划线构成。事实上 “-” 也是合法的,但为免乖离还是不宜使用。

流程控制

条件

完整的条件语句由 if , elseif , else , endif 指令组合构成:

1
2
3
4
5
6
7
if(condition)
  command_for_expr1_true(...)
elseif(condition)
  command_for_expr2_true(...)
else()
  command_for_all_false(...)
endif()

elseif 和 else 指令不是必须的构成,可以被省缺。

古代的 cmake 要求 else() 和 endif() 应该使用 if(condition) 中的 condition 片段,例如

1
2
3
if (EXISTS "${CMAKE_SOURCE_DIR}/.git")
else (EXISTS "${CMAKE_SOURCE_DIR}/.git")
endif (EXISTS "${CMAKE_SOURCE_DIR}/.git")

这很蠢而且很混淆:所以 else(EXISTS “${CMAKE_SOURCE_DIR}/.git”) 到底想表达哪一个含义呢?

所以现在的 cmake 不再有这样的要求了。

不过你需要知道上述传统,因为你仍然会遇到这样的古代风格的 if 语句,而且,它们现在仍然是正确的。

expr1 这样的条件表达式可以使用运算子来组合:

1
if((expr1) AND (expr2 OR (expr3)))

在条件表达式中,一个 token 名字会被自动尝试做变量求值,所以你没有必要显式使用 ${var} 表达式:

1
2
3
4
5
6
7
8
set(DEBUG TRUE)

if(DEBUG)
  ...
endif()
if(${DEBUG})
  ...
endif()

此时两条 if 语句的效果是相同的。

条件表达式的有效形式

if 条件表达式有多种变体:

由于它是如此地弱智(或者说,ugly,或者说,简明易懂易推倒),如果你不能看到以下形式了解到其目的的话,请参考 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
if(<constant>)
if(<variable|string>)
if(NOT <condition>)
if(<cond1> AND <cond2>)
if(<cond1> OR <cond2>)
if(COMMAND command-name)
if(POLICY policy-id)
if(TARGET target-name)
if(TEST test-name)
if(EXISTS path-to-file-or-directory)
if(file1 IS_NEWER_THAN file2)
if(IS_DIRECTORY path-to-directory)
if(IS_SYMLINK file-name)
if(IS_ABSOLUTE path)
if(<variable|string> MATCHES regex)
if(<variable|string> LESS <variable|string>)
if(<variable|string> GREATER <variable|string>)
if(<variable|string> EQUAL <variable|string>)
if(<variable|string> LESS_EQUAL <variable|string>)
if(<variable|string> GREATER_EQUAL <variable|string>)
if(<variable|string> STRLESS <variable|string>)
if(<variable|string> STRGREATER <variable|string>)
if(<variable|string> STREQUAL <variable|string>)
if(<variable|string> STRLESS_EQUAL <variable|string>)
if(<variable|string> STRGREATER_EQUAL <variable|string>)
if(<variable|string> VERSION_LESS <variable|string>)
if(<variable|string> VERSION_GREATER <variable|string>)
if(<variable|string> VERSION_EQUAL <variable|string>)
if(<variable|string> VERSION_LESS_EQUAL <variable|string>)
if(<variable|string> VERSION_GREATER_EQUAL <variable|string>)
if(<variable|string> IN_LIST <variable>)
if(DEFINED <name>|CACHE{<name>}|ENV{<name>})
if((condition) AND (condition OR (condition)))

原则上说,这些条件表达式的各类变体适用于 if,while 等需要条件表达式的场景。

这些变体实际上代表着不同类别的运算,下面简要归类为:

逻辑运算

在条件表达式中的与或非:

1
2
3
4
if(NOT condition)
if(condition1 AND condition2)
if(condition1 OR condition2)
if(condition1 OR (condition2 AND condition3))

这样的表达式运算时合法的:

1
2
3
4
5
set(TTT 1)  # set to TRUE
set(SSS 0)  # set to FALSE
if (TTT AND NOT SSS)
  message(FATAL_ERROR "TTT AND NOT SSS")
endif ()

它将会输出错误诊断来表达分支已经被匹配:

1
2
3
4
CMake Error at cmake/diag-1.cmake:25 (message):
  TTT AND NOT SSS
Call Stack (most recent call first):
  CMakeLists.txt:1 (include)

比较运算

数值
1
2
3
4
5
6
7
8
if(variable LESS number)
if(string LESS number)

if(variable GREATER number)
if(string GREATER number)

if(variable EQUAL number)
if(string EQUAL number)
字符串
1
2
3
4
5
6
7
if(string STRLESS string)

if(variable STRGREATER string)
if(string STRGREATER string)

if(variable STREQUAL string)
if(string STREQUAL string)
正则式
1
2
if(variable MATCHES regex)
if(string MATCHES regex)
文件比较

请注意必须提供完整路径名。

1
2
if(EXISTS file-name)
if(EXISTS directory-name)

当 file1 比 file2 新,或者其中一个文件不存在时:

1
if(file1 IS_NEWER_THAN file2)

判断给定的path是否是绝对路径:

1
if(IS_ABSOLUTE path)

判断给定path是否为文件夹:

1
if(IS_DIRECTORY path-to-directory)

判断给定的path是否符号链接文件:

1
if(IS_SYMLINK file-name)
版本比较

cmake 认为版本号具有 major[.minor[.patch[.tweak]]] 的格式,这是等效于 semver 的一种格式,但并不支持 semver 的复杂格式。

小于

1
if(<variable|string> VERSION_LESS <variable|string>)

大于

1
if(<variable|string> VERSION_GREATER <variable|string>)

等于

1
if(<variable|string> VERSION_EQUAL <variable|string>)

小于等于

1
if(<variable|string> VERSION_LESS_EQUAL <variable|string>)

大于等于

1
if(<variable|string> VERSION_GREATER_EQUAL <variable|string>)
其它

判断给定的 command-name 是否属于命令(command)、function、macro:

1
if(COMMAND command-name)

判断给定的 variable-name 是否已经被定义过:

1
2
if(DEFINED variable-name)
if(DEFINED <name>|CACHE{<name>}|ENV{<name>})

判断某个 policy 是否已经被定义(为NEW状态)

1
if(POLICY policy-id)

True if the given name is an existing policy (of the form CMP<NNNN>).

它检测是否曾经有 cmake_policy(SET CMP0048 NEW) 这样的指令曾经被执行过。

判断某个字符串是否在某个字符串列表之中:

1
if(<variable|string> IN_LIST <variable>)

判断某个名字是否曾经被 add_executable(), add_library(), 或 add_custom_target() 这样的 Target 定义语句所声明过:

1
if(TARGET target-name)

判断一个名字是否曾被 add_test() 所声明过:

1
if(TEST test-name)

循环

CMake 有两种循环:

  1. foreach..endforeach
  2. while..endwhile

foreach

foreach 指令具有如下格式:

1
2
3
foreach(<loop_var> <items>)
  <commands>
endforeach()

并且支持如下的变体:

1
2
3
foreach(<loop_var> RANGE <stop>)
foreach(<loop_var> RANGE <start> <stop> [<step>])
foreach(<loop_var> IN [LISTS [<lists>]] [ITEMS [<items>]])

foreach 指令所引导的循环可以被用于遍历字符串列表:

1
2
3
4
5
6
set(V alpha beta gamma)
message(${V})

foreach(i ${V})
    message(${i})
endforeach()

其结果类似于:

1
2
3
4
alphabetagamma
alpha
beta
gamma

while

while 循环采用条件表达式来判定循环是否结束:

1
2
3
while(<condition>)
  <commands>
endwhile()

在循环体中,可以使用 break()continue() 指令,含义自现。

函数与宏

函数和宏被用于组建你的子程序。其中 function 的函数体中 set 建立的是 local 变量,而 macro 的 body 中 set 会影响到全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 定義名為 print1 的 macro 

macro(print1 MESSAGE)
    set(k ${MESSAGE})
    message(${MESSAGE})
endmacro(print1)

# 定義名為 print2 的 function
function(print2 MESSAGE)
    set(k ${MESSAGE})
    message(${MESSAGE})
endfunction(print2) 

print1("from print1")
print2("from print2")
message("k=${k}")

輸出結果為
from print1
from print2
k="from print1"

例子取自 CMake 入门/流程控制 - 维基教科书,自由的教学读本

debug_print_value 等

我们在 utils.cmake 中提供了一系列的函数与宏工具,其中 debug_print_value 的定义如同这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
macro(debug msg)
  message(STATUS "DEBUG ${msg}")
endmacro()

macro(debug_print_value variableName)
  debug("${variableName}=\${${variableName}}")
endmacro()

macro(dump_list listName)
  message("- List of ${listName} -------------")
  foreach (lib ${${listName}})
    message("                         ${lib}")
  endforeach (lib)
endmacro()

使用方法简单:

1
2
3
include(utils)
set(DEBUG 1)
debug_print_value(DEBUG)

debug_dump_cmake_variables

debug_dump_cmake_variables 是更复杂一点的函数实例,它能 dump 出全部的 cmake 变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function(debug_dump_cmake_variables)
  get_cmake_property(_variableNames VARIABLES)
  list(SORT _variableNames)
  foreach (_variableName ${_variableNames})
    if (ARGV0)
      unset(MATCHED)

      #case sensitive match
      # string(REGEX MATCH ${ARGV0} MATCHED ${_variableName})
      #
      #case insenstitive match
      string(TOLOWER "${ARGV0}" ARGV0_lower)
      string(TOLOWER "${_variableName}" _variableName_lower)
      string(REGEX MATCH ${ARGV0_lower} MATCHED ${_variableName_lower})

      if (NOT MATCHED)
        continue()
      endif ()
    endif ()
    message(STATUS "  - ${_variableName} = ${${_variableName}}")
  endforeach ()
endfunction()

debug_dump_cmake_variables 也支持筛选。所以它的用法如下:

1
2
debug_dump_cmake_variables()           # dump 全部 cmake 变量
debug_dump_cmake_variables("^Boost")   # dump 以 Boost 开头的变量

🔚