15分钟学习CMake脚本(译)

Posted 42-curry

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了15分钟学习CMake脚本(译)相关的知识,希望对你有一定的参考价值。

在上一篇博客中我们提到了每一个CMake项目都需要包含一个 CMakeLists.txt 脚本。这个脚本定义了目标文件(targets),也可以做很多其他的事情,例如:寻找第三方库,生成C++头文件等。 CMake脚本有很大的灵活性。

 

技术分享图片

每当你集成一个外部库,以及添加其他平台的支持的大部分情况下,你需要编辑CMake脚本。我曾在并不动CMake脚本的情况下花了很长时间来编辑 CMakeLists.txt - 因为CMake脚本的文档很分散。但最终我搞懂了CMake脚本,这篇博文的目的就是让你尽快想我一样了解CMake脚本。

这篇博文不会覆盖所有的CMake命令(总共有数百个!),但这对CMake脚本的语法编程模型将是个颇为完整的指南。

Hello World

如果你创建一个包含以下内容的 hello.txt 文件:

message(“Hello World!”)        # A message to print

你能够在命令行中用 cmake -P hello.txt 来运行此文件。(-P 选项告诉cmake 运行指定脚本,但是不生成构建流水线(build pipeline))正如所期待的,运行此命令会输出 “Hello World!”。

$ cmake -P hello.txt
Hello world!

所有的变量都是字符串

在CMake中,所有的变量都是字符串。你可以在字符串常量(string literal)中使用 ${} 代入变量, 这被称作变量引用(variable reference)。修改 hello.txt 如下:

message("Hello ${NAME}!")       # Substitute a variable into the message

现在,如果我们在 cmake 命令中使用 -D 选项定义 NAME, hello.txt将会使用此变量:

$ cmake -DNAME=Newman -P hello.txt
Hello Newman!

当一个变量未被定义时,默认值是一个空字符串:

$ cmake -P hello.txt
Hello !

我们还可以在脚本中使用 set 命令定义变量。set 的第一个参数时变量名,第二个参数时变量的值:

set(THING "funk")
message("We want the ${THING}!")

只要字符串中没有空格或变量引用,set 参数中的引号是可选的。例如,set("THING" funk) 和上面第一行的命令是等价的。对于大多数CMake命令(if 和 while 命令除外)而言,是否使用引号只是风格问题。当参数只是变量名时,我倾向于不使用引号。

用前缀(prefixes)模拟数据结构

CMake中没有类(classes)的概念,但你可以通过定义一组有着同样前缀(prefixes)的变量来模拟数据结构。使用时,你可以用嵌套的变量引用( ${} )来查找组内变量。例如,下述脚本将会输出 “John Smith lives at 123 Fake St.”:

set(JOHN_NAME "John Smith")
set(JOHN_ADDRESS "123 Fake St")
set(PERSON "JOHN")
message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")

你甚至可以在set命令中使用变量引用。例如,如果 PERSON 变量的值仍然是 “JOHN”,下述代码将会把变量 JOHN_NAME 的值置为 “John Goodman”:

set(${PERSON}_NAME "John Goodman")

每条声明都是一条命令

在CMake中,每条声明都是一条接收一列字符串参数并且无返回值的命令。参数间由空格隔开。正如我们已经见到的, set 命令在文件中(file scope)定义了一个变量。

另一个例子是CMake中执行算术运算的 math 命令。math命令的第一个参数必须是 EXPR , 第二个参数是被赋值变量的名字,第三个参数是被求值表达式,这三个参数均为字符串。注意以下第三行代码,CMake在将参数传入 math 命令前先把字符串 MY_SUM 的值替换掉。

math(EXPR MY_SUM "1 + 1")                   # Evaluate 1 + 1; store result in MY_SUM
message("The sum is ${MY_SUM}.")
math(EXPR DOUBLE_SUM "${MY_SUM} * 2")       # Multiply by 2; store result in DOUBLE_SUM
message("Double that is ${DOUBLE_SUM}.")

几乎所有你需要做的事情都有一个对应的CMake 命令(https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html)。 string 命令可以帮助你完成正则表达式替换等高级字符串操作。file 命令能读写文件或者操作文件系统路径。

控制流命令

甚至控制流语句也是命令。if/endif 命令让你根据条件执行被包围的命令。控制流中的空格不影响功能,但通常还是会缩进条件命令间的命令以提高可读性。如下的命令检查 CMake 的内建变量 WIN 是否被赋值:

if(WIN32)
    message("You‘re running CMake on Windows.")
endif()

CMake 还有 while/endwhile 命令来在条件为真时重复执行命令。如下循环将打印所有1000000以下的斐波那契数字:

set(A "1")
set(B "1")
while(A LESS "1000000")
    message("${A}")                 # Print A
    math(EXPR T "${A} + ${B}")      # Add the numeric values of A and B; store result in T
    set(A "${B}")                   # Assign the value of B to A
    set(B "${T}")                   # Assign the value of T to B
endwhile()

CMake 中的 if 和 while 条件的用法和其他编程语言不同。如上所示,要执行数值比较,你必须指定 LESS 作为一个字符串参数。如下文档(https://cmake.org/cmake/help/latest/command/if.html)详细解释了如何正确编写条件语句。

if 和 while 命令有些特殊-如果指定的变量没有引号包围,if 和 while 将直接使用这些变量的值。在以上的代码中,我利用了这一点。第三行写作 while(A LESS “1000000”)而不是 while (${A} LESS "1000000"),虽然两者是等价的。其他命令不会主动对变量求值。

列表(Lists)只是分号分隔的字符串

CMake 对没有用引号的参数有特别的替换规则。如果整个参数是一个没有引号的变量引用,而且变量的值中包含分号,那么 CMake 将在分号处分隔该参数并作为多个参数传入包含该变量的命令。例如,如下代码将向 math 命令传入三个参数:

set(ARGS "EXPR;T;1 + 1")
math(${ARGS})                    # Equivalent to calling math(EXPR T "1 + 1")

另一方面,引号包围的参数不会在被替换后被分号分隔。CMake 总是将引号字符串作为单独的参数传入,保持分号的完整性:

set(ARGS "EXPR;T;1 + 1")
message("${ARGS}")                              # Prints: EXPR;T;1 + 1

如果两个以上的参数被传入 set 命令,它们将会被分号连接起来,然后被赋值给指定变量。这实际上是用参数创建了一个列表(list):

set(MY_LIST These are separate arguments)
message("${MY_LIST}")                  # Prints: These;are;separate;arguments

 

 你能通过 list 命令来操作列表:

set(MY_LIST These are separate arguments)
list(REMOVE_ITEM MY_LIST "separate")       # Removes "separate" from the list
message("${MY_LIST}")                           # Prints: These;are;arguments

foreach/endforeach 命令接收多个参数并迭代除第一个参数以外的参数:

foreach(ARG These are separate arguments)
    message("${ARG}")                           # Prints each word on a separate line
endforeach()

你可以通过传入一个没有引号的变量引用给 foreach 来进行迭代一个列表。就像其他命令一样, CMake将以分号分隔该变量的值:

foreach(ARG ${MY_LIST})           # Splits the list; passes items as arguments
    message("${ARG}")             # Prints each item on a separate line
endforeach()

函数有作用域;宏没有

在 CMake 中,你可以用 function/endfunction 命令来定义一个函数。以下代码定义了一个将参数的数值翻倍并打印的函数 doubleIt:

function(doubleIt VALUE)
    math(EXPR RESULT "${VALUE} * 2")
    message("${RESULT}")
endfunction()

doubleIt("4")                           # Prints: 8

函数在自己的作用域中运行。函数中定义的局部变量不会污染调用者的作用域。如果你想要返回值,你可以传入你想要返回的变量,然后用特殊参数 PARENT_SCOPE 调用 set 命令:

function(doubleIt VARNAME VALUE)
    math(EXPR RESULT "${VALUE} * 2")
    set(${VARNAME} "${RESULT}" PARENT_SCOPE)    # Set the named variable in caller‘s scope
endfunction()

doubleIt(RESULT "4")    # Tell the function to set the variable named RESULT
message("${RESULT}")                    # Prints: 8

类似的,macro/endmacro 命令可定义一个宏。但是和函数不同,宏和它们的调用者在同一作用域工作。因此,宏中定义的所有变量在调用者的作用域中被set。我们可以用以下宏替代上述函数:

macro(doubleIt VARNAME VALUE)
    math(EXPR ${VARNAME} "${VALUE} * 2") # Set the named variable in caller‘s scope
endmacro()

doubleIt(RESULT "4")                    # Tell the macro to set the variable named RESULT
message("${RESULT}")                    # Prints: 8

函数和宏都可以接受任意数量的参数。未命名参数通过一个特殊变量 ARGN 作为列表暴露给函数。以下函数将所有接收的参数翻倍,并将每个参数分行打印:

function(doubleEach)
    foreach(ARG ${ARGN})                # Iterate over each argument
        math(EXPR N "${ARG} * 2")       # Double ARG‘s numeric value; store result in N
        message("${N}")                 # Print N
    endforeach()
endfunction()

doubleEach(5 6 7 8)                     # Prints 10, 12, 14, 16 on separate lines

包含其他脚本

CMake 变量都定义在文件域。include 命令在 调用文件的作用域 执行其他 CMake 脚本。这和 C/C++ 中的 #include 预处理命令很相似。include 命令通常用来调用其他脚本中定义一些常用的函数或者宏,此命令用 CMAKE_MODULE_PATH 变量中的值作为搜索路径。

find_package 命令搜索形如 Find*.cmake 的脚本,并且在同一作用域中运行这些脚本。这些脚本通常用来帮助寻找外部库。例如,如果在搜索路径中存在一个叫做 FindSDL2.cmake 的文件, find_package(SDL2)等价于 include(FindSDL2.cmake)。(注意,此处只是 find_package 多种用法中的一种)

另一方面,add_subdirectory 命令会创建一个新的作用域,然后在新作用域中运行指定文件夹下的CMakeLists.txt 脚本。此命令通常用来添加另一个 CMake 子项目,例如一个库或者是一个可执行文件,到本项目。如果不特别指定的话,子项目中定义的目标文件会被添加到构建流水线(build pipeline)中。子项目脚本中的变量不会污染本项目的作用域-除非 set 命令中使用了 PARENT_SCOPE 选项。

例如,在Turf(https://github.com/preshing/turf)项目中,运行 CMake 时如下脚本会被用到:

技术分享图片

读取和设置属性

CMake 使用 add_executable, add_library 或者 add_custom_target 命令来定义目标文件。目标文件被创建好之后,它们就有了属性(properties),你可以用 get_property 和 set_property 命令来操作这些属性。不像变量,目标文件在所有作用域中都是可见的,即使它们是在子项目中定义的。目标文件的所有属性也都是字符串。

add_executable(MyApp "main.cpp")        # Create a target named MyApp

# Get the target‘s SOURCES property and assign it to MYAPP_SOURCES
get_property(MYAPP_SOURCES TARGET MyApp PROPERTY SOURCES)

message("${MYAPP_SOURCES}")             # Prints: main.cpp

常见的目标文件属性还包括 LINK_LIBRARIES, INCLUDE_DIRECTORIES 和 COMPILE_DEFINITIONS。这些属性会被 target_link_libraries ,target_include_directories 和  target_compile_definitions 命令间接修改。在脚本结束的时候,CMake会用这些目标文件属性来生成构建流水线。

其他 CMake 实体(entities)也有自己的属性。每个文件作用域都有一个目录属性(directory properties)的集合。还有一个所有脚本都可见的全局属性(global properties)集合。每个 C/C++ 源文件也有一个源文件属性(source file properties)集合。

 

原文链接:https://preshing.com/20170522/learn-cmakes-scripting-language-in-15-minutes/

以上是关于15分钟学习CMake脚本(译)的主要内容,如果未能解决你的问题,请参考以下文章

OkEDU-Classroom-Desktop在win下编译记录

OkEDU-Classroom-Desktop在win下编译记录

如何在不再次运行配置脚本/cmake 的情况下修改安装路径

win32下编译glog

Window下编译qtpdfium

[译] 如何在React中写出更优秀的代码