modern cmake

Posted by Leo on September 23, 2023

一、modern cmake

  • 构建项目
    1
    2
    
      cmake -S . -B build
      cmake --build build
    

    下任何一条命令都能够执行安装:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      # From the build directory (pick one)
      ~/package/build $ make install
      ~/package/build $ cmake --build . --target install
      ~/package/build $ cmake --install . # CMake 3.15+ only
    
      # From the source directory (pick one)
      ~/package $ make -C build install
      ~/package $ cmake --build build --target install
      ~/package $ cmake --install build # CMake 3.15+ only
    

    如果你使用 cmake –build 而不是直接调用更底层的构建系统(译者注:比如直接使用 make),你可以用 -v 参数在构建时获得详细的输出(CMake 3.14+),用 -j N 指定用 N 个 CPU 核心并行构建项目(CMake 3.12+),以及用 –target(任意版本的 CMake)或 -t(CMake 3.15+)来选择一个目标进行部分地构建。

  • 指定编译器 指定编译器必须在第一次运行时在空目录中进行。这种命令并不属于 CMake 语法,但你仍可能不太熟悉它。如果要选择 Clang:
    1
    
      CC=clang CXX=clang++ cmake ..
    
  • 指定生成器 你可以选择的构建工具有很多;通常默认的是 make。要显示在你的系统上 CMake 可以调用的所有构建工具,运行:
    1
    
      cmake --help
    

    你也可以用 -G”My Tool”(仅当构建工具的名字中包含空格时才需要引号)来指定构建工具。像指定编译器一样,你应该在一个目录中第一次调用 CMake 时就指定构建工具。如果有好几个构建目录也没关系,比如 build/ 和 buildXcode。你可以用环境变量 CMAKE_GENERATOR 来指定默认的生成器(CMake 3.15+)。

  • 设置选项 在 CMake 中,你可以使用 -D 设置选项。你能使用 -L 列出所有选项,或者用 -LH 列出人类更易读的选项列表。如果你没有列出源代码目录或构建目录,这条命令将不会重新运行 CMake(使用 cmake -L 而不是 cmake -L .)

  • 选项 CMake 支持缓存选项。CMake 中的变量可以被标记为 “cached”,这意味着它会被写入缓存(构建目录中名为 CMakeCache.txt 的文件)。你可以在命令行中用 -D 预先设定(或更改)缓存选项的值。CMake 查找一个缓存的变量时,它就会使用已有的值并且不会覆盖这个值。

  • 标准选项 大部分软件包中都会用到以下的 CMake 选项:

    • -DCMAKE_BUILD_TYPE= 从 Release, RelWithDebInfo, Debug, 或者*可能存在的更多参数中选择。
    • -DCMAKE_INSTALL_PREFIX= 这是安装位置。UNIX 系统默认的位置是 /usr/local,用户目录是 ~/.local,也可以是你自己指定的文件夹。
    • -DBUILD_SHARED_LIBS= 你可以把这里设置为 ON 或 OFF 来控制共享库的默认值(不过,你也可以明确选择其他值而不是默认值)
    • -DBUILD_TESTING= 这是启用测试的通用名称,当然不会所有软件包都会使用它,有时这样做确实不错。
  • 调试你的 CMake 文件 –trace 选项能够打印出运行的 CMake 的每一行。由于它过于冗长,CMake 3.7 添加了 –trace-source=”filename” 选项,这让你可以打印出你想看的特定文件运行时执行的每一行。如果你选择了要调试的文件的名称(在调试 CMakeLists.txt 时通常选择父目录,因为它的名字在任何项目中都一样),你就会只看到这个文件里运行的那些行。这很实用!

二、CMake 行为准则(Do’s and Don’ts)

  • CMake 应避免的行为
    • 不要使用具有全局作用域的函数:这包含 link_directories、 include_libraries 等相似的函数。
    • 不要添加非必要的 PUBLIC 要求:你应该避免把一些不必要的东西强加给用户(-Wall)。相比于 PUBLIC,更应该把他们声明为 PRIVATE。
    • 不要在file函数中添加 GLOB 文件:如果不重新运行 CMake,Make 或者其他的工具将不会知道你是否添加了某个文件。值得注意的是,CMake 3.12 添加了一个 CONFIGURE_DEPENDS 标志能够使你更好的完成这件事。
    • 将库直接链接到需要构建的目标上:如果可以的话,总是显式的将库链接到目标上。
    • 当链接库文件时,不要省略 PUBLIC 或 PRIVATE 关键字:这将会导致后续所有的链接都是缺省的。
  • CMake 应遵守的规范
    • 把 CMake 程序视作代码:它是代码。它应该和其他的代码一样,是整洁并且可读的。
    • 建立目标的观念:你的目标应该代表一系列的概念。为任何需要保持一致的东西指定一个 (导入型)INTERFACE 目标,然后每次都链接到该目标。
    • 导出你的接口:你的 CMake 项目应该可以直接构建或者安装。
    • 为库书写一个 Config.cmake 文件:这是库作者为支持客户的体验而应该做的。
    • 声明一个 ALIAS 目标以保持使用的一致性:使用 add_subdirectory 和 find_package 应该提供相同的目标和命名空间。
    • 将常见的功能合并到有详细文档的函数或宏中:函数往往是更好的选择。
    • 使用小写的函数名: CMake 的函数和宏的名字可以定义为大写或小写,但是一般都使用小写,变量名用大写。
    • 使用 cmake_policy 和/或 限定版本号范围: 每次改变版本特性 (policy) 都要有据可依。应该只有不得不使用旧特性时才降低特性 (policy) 版本。

三、基础知识简介

  • 最低版本要求 这是每个 CMakeLists.txt 都必须包含的第一行
    1
    
      cmake_minimum_required(VERSION 3.1)
    

    顺便提一下关于 CMake 的语法。命令 cmake_minimum_required 是不区分大小写的,所以常用的做法是使用小写。 CMake 的版本与它的特性(policies)相互关联,这意味着它也定义了 CMake 行为的变化。 从 CMake 3.12 开始,版本号可以声明为一个范围,例如 VERSION 3.1…3.15;这意味着这个工程最低可以支持 3.1 版本,但是也最高在 3.15 版本上测试成功过

    当你开始一个新项目,起始推荐这么写:

    1
    2
    3
    4
    5
    
      cmake_minimum_required(VERSION 3.7...3.21)
    
      if(${CMAKE_VERSION} VERSION_LESS 3.12)
          cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
      endif()
    
  • 设置一个项目 现在,每一个顶层 CMakelists 文件都应该有下面这一行:
    1
    2
    3
    
      project(MyProject VERSION 1.0
                      DESCRIPTION "Very nice project"
                      LANGUAGES CXX)
    

    现在我们看到了更多的语法。这里的字符串是带引号的,因此内容中可以带有空格。项目名称是这里的第一个参数。所有的关键字参数都可选的。VERSION 设置了一系列变量,例如 MyProject_VERSION 和 PROJECT_VERSION。语言可以是 C,CXX,Fortran,ASM,CUDA(CMake 3.8+),CSharp(3.8+),SWIFT(CMake 3.15+ experimental),默认是C CXX。在 CMake 3.9,可以通过DESCRIPTION 关键词来添加项目的描述。这个关于 project 的文档可能会有用。

    1
    
    你可以用 # 来添加注释。CMake 也有一个用于注释的内联语法,但是那极少用到。
    
  • 生成一个可执行文件 尽管库要有趣的多,并且我们会将把大部分时间花在其上。但是现在,先让我们从一个简单的可执行文件开始吧!
    1
    
      add_executable(one two.cpp three.h)
    

    这里有一些语法需要解释。one 既是生成的可执行文件的名称,也是创建的 CMake 目标(target)的名称(我保证,你很快会听到更多关于目标的内容)。紧接着的是源文件的列表,你想列多少个都可以。CMake 很聪明 ,它根据拓展名只编译源文件。在大多数情况下,头文件将会被忽略;列出他们的唯一原因是为了让他们在 IDE 中被展示出来,目标文件在许多 IDE 中被显示为文件夹

  • 生成一个库 制作一个库是通过 add_library 命令完成的,并且非常简单:
    1
    
      add_library(one STATIC two.cpp three.h)
    

    你可以选择库的类型,可以是 STATIC,SHARED, 或者MODULE.如果你不选择它,CMake 将会通过 BUILD_SHARED_LIBS 的值来选择构建 STATIC 还是 SHARED 类型的库。

    在下面的章节中你将会看到,你经常需要生成一个虚构的目标,也就是说,一个不需要编译的目标。例如,只有一个头文件的库。这被叫做 INTERFACE 库,这是另一种选择,和上面唯一的区别是后面不能有文件名。

    你也可以用一个现有的库做一个 ALIAS 库,这只是给已有的目标起一个别名。这么做的一个好处是,你可以制作名称中带有 :: 的库

  • 目标时常伴随着你 现在我们已经指定了一个目标,那我们如何添加关于它的信息呢?例如,它可能需要包含一个目录:
    1
    
      target_include_directories(one PUBLIC include)
    

    target_include_directories 为目标添加了一个目录。 PUBLIC 对于一个二进制目标没有什么含义;但对于库来说,它让 CMake 知道,任何链接到这个目标的目标也必须包含这个目录。其他选项还有 PRIVATE(只影响当前目标,不影响依赖),以及 INTERFACE(只影响依赖)。

    接下来我们可以将目标之间链接起来:

    1
    2
    
      add_library(another STATIC another.cpp another.h)
      target_link_libraries(another PUBLIC one)
    

    target_link_libraries 可能是 CMake 中最有用也最令人迷惑的命令。它指定一个目标,并且在给出目标的情况下添加一个依赖关系。如果不存在名称为 one 的目标,那他会添加一个链接到你路径中 one 库(这也是命令叫 target_link_libraries 的原因)。或者你可以给定一个库的完整路径,或者是链接器标志。最后再说一个有些迷惑性的知识:),经典的 CMake 允许你省略 PUBLIC 关键字,但是你在目标链中省略与不省略混用,那么 CMake 会报出错误。

    只要记得在任何使用目标的地方都指定关键字,那么就不会有问题。

    目标可以有包含的目录、链接库(或链接目标)、编译选项、编译定义、编译特性(见C++11 章节)等等。正如你将在之后的两个项目章节中看到的,你经常可以得到目标(并且经常是指定目标)来代表所有你使用的库。甚至有些不是真正的库,像 OpenMP,就可以用目标来表示。这也是为什么现代 CMake 如此的棒!

  • 更进一步 看看你是否能理解以下文件。它生成了一个简单的 C++11 的库并且在程序中使用了它。没有依赖。我将在之后讨论更多的 C++ 标准选项,代码中使用的是 CMake 3.8。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      cmake_minimum_required(VERSION 3.8)
    
      project(Calculator LANGUAGES CXX)
    
      add_library(calclib STATIC src/calclib.cpp include/calc/lib.hpp)
      target_include_directories(calclib PUBLIC include)
      target_compile_features(calclib PUBLIC cxx_std_11)
    
      add_executable(calc apps/calc.cpp)
      target_link_libraries(calc PUBLIC calclib)
    

四、变量与缓存

  • 本地变量 我们首先讨论变量。你可以这样声明一个本地 ( local ) 变量:
    1
    
      set(MY_VARIABLE "value")
    

    变量名通常全部用大写,变量值跟在其后。你可以通过 ${} 来解析一个变量,例如 ${MY_VARIABLE}. CMake 有作用域的概念,在声明一个变量后,你只可以在它的作用域内访问这个变量。如果你将一个函数或一个文件放到一个子目录中,这个变量将不再被定义。你可以通过在变量声明末尾添加 PARENT_SCOPE 来将它的作用域置定为当前的上一级作用域。

    列表就是简单地包含一系列变量:

    1
    
      set(MY_LIST "one" "two")
    

    你也可以通过 ; 分隔变量,这和空格的作用是一样的:

    1
    
      set(MY_LIST "one;two")
    

    有一些和 list( 进行协同的命令, separate_arguments 可以把一个以空格分隔的字符串分割成一个列表。需要注意的是,在 CMake 中如果一个值没有空格,那么加和不加引号的效果是一样的。这使你可以在处理知道不可能含有空格的值时不加引号。

    当一个变量用 ${} 括起来的时候,空格的解析规则和上述相同。对于路径来说要特别小心,路径很有可能会包含空格,因此你应该总是将解析变量得到的值用引号括起来,也就是,应该这样 “${MY_PATH}” 。

  • 缓存变量 CMake 提供了一个缓存变量来允许你从命令行中设置变量。CMake 中已经有一些预置的变量,像 CMAKE_BUILD_TYPE 。如果一个变量还没有被定义,你可以这样声明并设置它。
    1
    
      set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "Description")
    

    这么写不会覆盖已定义的值。这是为了让你只能在命令行中设置这些变量,而不会在 CMake 文件执行的时候被重新覆盖。如果你想把这些变量作为一个临时的全局变量,你可以这样做:

    1
    2
    
      set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "" FORCE)
      mark_as_advanced(MY_CACHE_VARIABLE)
    

    第一行将会强制设置该变量的值,第二行将使得用户运行 cmake -L .. 或使用 GUI 界面的时候不会列出该变量。此外,你也可以通过 INTERNAL 这个类型来达到同样的目的(尽管在技术上他会强制使用 STRING 类型,这不会产生任何的影响):

    1
    
      set(MY_CACHE_VARIABLE "VALUE" CACHE INTERNAL "")
    

    因为 BOOL 类型非常常见,你可以这样非常容易的设置它:

    1
    
       option(MY_OPTION "This is settable from the command line" OFF)
    
  • 环境变量 你也可以通过 set(ENV{variable_name} value) 和 $ENV{variable_name} 来设置和获取环境变量,不过一般来说,我们最好避免这么用。

  • 缓存 缓存实际上就是个文本文件,CMakeCache.txt ,当你运行 CMake 构建目录时会创建它。 CMake 可以通过它来记住你设置的所有东西,因此你可以不必在重新运行 CMake 的时候再次列出所有的选项。

  • 属性 CMake 也可以通过属性来存储信息。这就像是一个变量,但它被附加到一些其他的物体 ( item ) 上,像是一个目录或者是一个目标。一个全局的属性可以是一个有用的非缓存的全局变量。许多目标属性都是被以 CMAKE_ 为前缀的变量来初始化的。例如你设置 CMAKE_CXX_STANDARD 这个变量,这意味着你之后创建的所有目标的 CXX_STANDARD 都将被设为CMAKE_CXX_STANDARD 变量的值。 你可以这样来设置属性:
    1
    2
    3
    4
    5
    
      set_property(TARGET TargetName
               PROPERTY CXX_STANDARD 11)
    
      set_target_properties(TargetName PROPERTIES
                          CXX_STANDARD 11)
    

    第一种方式更加通用 ( general ) ,它可以一次性设置多个目标、文件、或测试,并且有一些非常有用的选项。第二种方式是为一个目标设置多个属性的快捷方式。此外,你可以通过类似于下面的方式来获得属性:

    1
    
      get_property(ResultVariable TARGET TargetName PROPERTY CXX_STANDARD)
    

    可以查看 cmake-properties 获得所有已知属性的列表。在某些情况下,你也可以自己定义一些属性

用 CMake 进行编程

  • 控制流程 CMake 有一个 if 语句,尽管经过多次版本迭代它已经变得非常复杂。这里有一些全大写的变量你可以在 if 语句中使用,并且你既可以直接引用也可以利用 ${} 来对他进行解析( if 语句在历史上比变量拓展出现的更早 )。这是一个 if 语句的例子:
    1
    2
    3
    4
    5
    6
    
      if(variable)
          # If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number
      else()
          # If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND`
      endif()
      # If variable does not expand to one of the above, CMake will expand it then try again
    

    如果你在这里使用 ${variable} 可能会有一些奇怪,因为看起来它好像 variable 被展开 ( expansion ) 了两次。在 CMake 3.1+ 版本中加入了一个新的特性 ( CMP0054 ) ,CMake 不会再展开已经被引号括起来的展开变量。也就是说,如果你的 CMake 版本大于 3.1 ,那么你可以这么写:

    1
    2
    3
    4
    5
    
      if("${variable}")
          # True if variable is not false-like
      else()
          # Note that undefined variables would be `""` thus false
      endif()
    

    这里还有一些关键字可以设置,例如:

    • 一元的: NOT, TARGET, EXISTS (文件), DEFINED, 等。

    • 二元的: STREQUAL, AND, OR, MATCHES ( 正则表达式 ), VERSION_LESS, VERSION_LESS_EQUAL ( CMake 3.7+ ), 等。

    • 括号可以用来分组

  • generator-expressions generator-expressions 语句十分强大,不过有点奇怪和专业 ( specialized ) 。大多数 CMake 命令在配置的时候执行,包括我们上面看到的 if 语句。但是如果你想要他们在构建或者安装的时候运行呢,应该怎么写? 生成器表达式就是为此而生。它们在目标属性中被评估( evaluate ):

    生成器表达式简单来说就是在CMake生成构建系统的时候根据不同配置动态生成特定的内容。比如:

    • 条件链接,如针对同一个编译目标,debug版本和release版本链接不同的库文件
    • 条件定义,如针对不同编译器,定义不同的宏

    所以可以看到,其中的要点是条件,之所以需要自动生成,那绝大多数时候肯定是因为开发者无法提前确定某些配置,不能提前确定那往往就是有条件的。

    生成器表达式的格式形如$<…>,可以嵌套,可以用在很多构建目标的属性设置和特定的CMake命令中。值得强调的是,生成表达式被展开是在生成构建系统的时候,所以不能通过解析配置CMakeLists.txt阶段的message命令打印,文末会介绍其调试方法。

    常用的生成器表达式

    1. 布尔生成器表达式 逻辑运算符 逻辑运算很多语言都是需要的,CMake生成器表达式中有这些:

      1. $:如果字符串为空、0;不区分大小写的FALSE、OFF、N、NO、IGNORE、NOTFOUND;或者区分大小写以-NOTFOUND结尾的字符串,则为0,否则为1
      2. $:逻辑与,conditons是以逗号分割的条件列表
      3. $:逻辑或,conditons是以逗号分割的条件列表
      4. $:逻辑非

        一般来说,条件是列表的,都是使用逗号进行分割,后面不再赘述。

      字符串比较

      1. $<STREQUAL:string1,string2>:判断字符串是否相等
      2. $<EQUAL:value1,value2>:判断数值是否相等
      3. $<IN_LIST:string,list>:判断string是否包含在list中,list使用分号分割

        注意这里的list是在逗号后面的列表,所以其内容需要使用分号分割。 变量查询 这个会是比较常用的,在实际使用的时候会根据不同CMake内置变量生成不同配置,核心就在于“判断”:

      4. $:判断目标是否存在
      5. $:判断编译类型配置是否包含在cfgs列表(比如"release,debug")中;不区分大小写
      6. $:判断CMake定义的平台ID是否包含在platform_ids列表中
      7. $:判断编译语言是否包含在languages列表中
    2. 字符串值生成器表达式 请注意,前面都是铺垫,这里才是使用生成器表达式的主要目的:生成特定的字符串。 比如官方的例子:基于编译器ID指定include目录:

      include_directories(/usr/include/$/) 根据编译器的类型,$会被替换成对应的ID(比如“GNU”、“Clang”)。

      条件表达式 这便是本文的核心了,主要有两个格式:

      1. $:如果条件为真,则结果为true_string,否则为空
      2. $<IF:condition,str1,str2>:如果条件为真,则结果为str1,否则为str2 这里的条件一般情况下就是前面介绍的布尔生成器表达式。 比如要根据编译类型指定不同的编译选项,可以像下面这样:
        1
        2
        3
        4
        
         set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
         set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
         set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2")
         set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
        

        但是使用生成器表达式可以简化成:

        1
        2
        
         add_compile_options("$<$<CONFIG:Debug>:-g;-O0>")
         add_compile_options($<$<CONFIG:Release>:-O2>)
        

        如果需要指定多个编译选项,必须使用双引号把生成器表达式包含起来,且选项之间使用分号。 后面这个方法适用于设置一些对所有编译器(取决于项目编译语言)都通用的编译选项,而需要设置一些编译器特有的选项时,通过设置指定编译器的编译选项(前一种方法)更加简洁明了。

      当然,可以用表达式判断编译器ID设置不同编译选项,不过明显有些为了用而用,这是没必要的。 转义字符 这比较好理解,因为有一些字符有特殊含义,所以可能需要转义,比如常用的$和\$,分别表示,和;。

      字符串操作 常用的有$、\$用于转换大小写。

      获取变量值 获取变量的值和前文提到的变量查询很类似,前面说的变量查询是判断是否存在于指定列表中或者等于指定值。语法格式是类似的,以CONFIG为例:

      获取变量值:$ 判断是否存在于列表中:\$

      编译目标查询 这里的查询是指获取编译目标(通过add_executable()、add_library()命令生成的)相关的一些信息,包括:

      $:获取编译目标的文件路径 \$:获取编译目标的文件名 \$:获取编译目标的基础名字,也就是文件名去掉前缀和扩展名

      在本专题前一篇文章中介绍合并静态库的时候,就用到$去获取静态库的路径。

    3. 调试 调试可以通过输出到文件的方式,在cmake执行完之后去检查是否符合预期,比如:

      1
      
       file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<CONFIG:Debug>:-g;-O0>,$<PLATFORM_ID>\n")
      

    一些生成器表达式的其他用途:

    限制某个项目的语言,例如可以限制其语言为 CXX 来避免它和 CUDA 等语言混在一起,或者可以通过封装它来使得他对不同的语言有不同的表现。 获得与属性相关的配置,例如文件的位置。 为构建和安装生成不同的位置。 最后一个是常见的。你几乎会在所有支持安装的软件包中看到如下代码:

    1
    2
    3
    4
    5
    6
    
      target_include_directories(
          MyTarget
      PUBLIC
          $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
          $<INSTALL_INTERFACE:include>
      )
    
    1
    
    注:表示在目标对于直接 BUILD 使用的目标包含的头文件目录为 ${CMAKE_CURRENT_SOURCE_DIR}/include,而安装的目标包含的头文件目录为 include,是一个相对位置(同时需要 install 对应的头文件才可以)。
    
  • 宏定义与函数 你可以轻松地定义你自己的 CMake function 或 macro 。函数和宏只有作用域上存在区别,宏没有作用域的限制。所以说,如果你想让函数中定义的变量对外部可见,你需要使用 PARENT_SCOPE 来改变其作用域。如果是在嵌套函数中,这会变得异常繁琐,因为你必须在想要变量对外的可见的所有函数中添加 PARENT_SCOPE 标志。但是这样也有好处,函数不会像宏那样对外“泄漏”所有的变量。接下来用函数举一个例子:

下面是一个简单的函数的例子:

1
2
3
4
5
6
7
function(SIMPLE REQUIRED_ARG)
    message(STATUS "Simple arguments: ${REQUIRED_ARG}, followed by ${ARGN}")
    set(${REQUIRED_ARG} "From SIMPLE" PARENT_SCOPE)
endfunction()

simple(This Foo Bar)
message("Output: ${This}")

如果你想要有一个指定的参数,你应该在列表中明确的列出,除此之外的所有参数都会被存储在 ARGN 这个变量中( ARGV 中存储了所有的变量,包括你明确列出的 )。CMake 的函数没有返回值,你可以通过设定变量值的形式来达到同样地目的。在上面的例子中,你可以通过指定变量名来设置一个变量的值。

  • 参数的控制 你应该已经在很多 CMake 函数中见到过,CMake 拥有一个变量命名系统。你可以通过 cmake_parse_arguments 函数来对变量进行命名与解析。如果你想在低于 3.5 版本的CMake 系统中使用它,你应该包含 CMakeParseArguments 模块,此函数在 CMake 3.5 之前一直存在与上述模块中。这是使用它的一个例子: ```cmake function(COMPLEX) cmake_parse_arguments( COMPLEX_PREFIX “SINGLE;ANOTHER” “ONE_VALUE;ALSO_ONE_VALUE” “MULTI_VALUES” ${ARGN} ) endfunction()

complex(SINGLE ONE_VALUE value MULTI_VALUES some other values)

1
2
3
4
5
6
7
在调用这个函数后,会生成以下变量:
```cmake
COMPLEX_PREFIX_SINGLE = TRUE
COMPLEX_PREFIX_ANOTHER = FALSE
COMPLEX_PREFIX_ONE_VALUE = "value"
COMPLEX_PREFIX_ALSO_ONE_VALUE = <UNDEFINED>
COMPLEX_PREFIX_MULTI_VALUES = "some;other;values"

如果你查看了官方文档,你会发现可以通过 set 来避免在 list 中使用分号,你可以根据个人喜好来确定使用哪种结构。你可以在上面列出的位置参数中混用这两种写法。此外,其他剩余的参数(因此参数的指定是可选的)都会被保存在 COMPLEX_PREFIX_UNPARSED_ARGUMENTS 变量中。

使用cmake_parse_arguments可以定义一个带有命名参数的函数或宏

1
2
cmake_parse_arguments(<prefix> <options> <one_value_keywords>
                      <multi_value_keywords> <args>...)
1
2
3
4
5
<prefix>前缀:解析出的参数都会按照prefix_参数名的形式形成新的变量。这些变量将保存参数列表中的相应值,如果找不到相关选项,则这些变量将是未定义的。
<options>可选值:无论选项是否在参数列表中,它们都将被定义为TRUE或FALSE(选项在参数中为true)。
<one_value_keywords>: 单值关键词列表,每个关键词仅仅对应一个值。
<multi_value_keywords>: 多值关键词列表,每个关键词可对应多个值。
<args>...参数: 一般传入${ARGN}即可。

example

与你的代码交互

  • 通过 CMake 配置文件 CMake 允许你在代码中使用 configure_file 来访问 CMake 变量。该命令将一个文件( 一般以 .in 结尾 )的内容复制到另一个文件中,并替换其中它找到的所有 CMake 变量。如果你想要在你的输入文件中避免替换掉使用 ${} 包含的内容,你可以使用 @ONLY 关键字。还有一个关键字 COPY_ONLY 可以用来作为 file(COPY 的替代字。 这个功能在 CMake 中使用的相当频繁,例如在下面的 Version.h.in 中: Version.h.in
    1
    2
    3
    4
    5
    6
    7
    
      #pragma once
    
      #define MY_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
      #define MY_VERSION_MINOR @PROJECT_VERSION_MINOR@
      #define MY_VERSION_PATCH @PROJECT_VERSION_PATCH@
      #define MY_VERSION_TWEAK @PROJECT_VERSION_TWEAK@
      #define MY_VERSION "@PROJECT_VERSION@"
    

    CMake lines:

    1
    2
    3
    4
    
      configure_file (
          "${PROJECT_SOURCE_DIR}/include/My/Version.h.in"
          "${PROJECT_BINARY_DIR}/include/My/Version.h"
      )
    

    在构建你的项目时,你也应该包括二进制头文件路径。如果你想要在头文件中包含一些 true/false 类型的变量,CMake 对 C 语言有特有的 #cmakedefine 和 #cmakedefine01 替换符来完成上述需求。

    你也可以使用( 并且是常用 )这个来生成 .cmake 文件,例如配置文件( 见 installing )。

  • 读入文件 另外一个方向也是行得通的, 你也可以从源文件中读取一些东西( 例如版本号 )。例如,你有一个仅包含头文件的库,你想要其在无论有无 CMake 的情况下都可以使用,上述方式将是你处理版本的最优方案。可以像下面这么写:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      # Assuming the canonical version is listed in a single line
      # This would be in several parts if picking up from MAJOR, MINOR, etc.
      set(VERSION_REGEX "#define MY_VERSION[ \t]+\"(.+)\"")
    
      # Read in the line containing the version
      file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/include/My/Version.hpp"
          VERSION_STRING REGEX ${VERSION_REGEX})
    
      # Pick out just the version
      string(REGEX REPLACE ${VERSION_REGEX} "\\1" VERSION_STRING "${VERSION_STRING}")
    
      # Automatically getting PROJECT_VERSION_MAJOR, My_VERSION_MAJOR, etc.
      project(My LANGUAGES CXX VERSION ${VERSION_STRING})
    

    如上所示, file(STRINGS file_name variable_name REGEX regex) 选择了与正则表达式相匹配的行,并且使用了相同的正则表达式来匹配出其中版本号的部分。