【CMake学习笔记】| 常用基础指令总结(三)

本篇文章重点介绍 CMakeLists.txt 的基础语法和常用指令。

1、基础语法

cmake 其实仍然要使用 “cmake 语言和语法” 去构建,最简单的语法规则是:

  • 变量使用 ${} 方式取值,但是在 IF 控制语句中是直接使用变量名。

  • 指令(参数 1 参数 2...)

    参数使用括弧括起,参数之间使用空格或分号分开。以 ADD_EXECUTABLE 指令为例,如果存在main.c、func.c 源文件,就要写成:

ADD_EXECUTABLE(hello main.c func.c)

或者

ADD_EXECUTABLE(hello main.c;func.c)

指令是大小写无关的,参数和变量是大小写相关的。

2、常用指令

2.1 set

set 指令可以设置普通变量、缓存条目、环境变量三种变量的值,分别对应下述三种指令格式。

# 设置普通变量
set(<variable> <value>... [PARENT_SCOPE])

# 设置缓存条目
set(<variable> <value>... CACHE <type> <docstring> [FORCE])

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

set的值<value>...表示可以给变量设置 0个或者多个值,当设置多个值时(大于2个),多个值会通过分号连接符连接成一个真实的值赋值给变量,当设置 0 个值时,实际上是把变量变为未设置状态,相当于调用unset指令。

下面分别对三种变量的设置进行说明:

  • 设置普通变量

    指令格式如下:

    set(<variable> <value>... [PARENT_SCOPE])

    将变量variable设置为值<value>...,变量variable 的作用域为调用set指令的函数或者当前目录;如果后面增加了 PARENT_SCOPE 选项的话, 表示 在上层作用域/目录 设置variable 的值为 <value>, 但是当前作用域/目录 该<variable> 的值不变。在当前作用域。如果该变量分别被不带 PARENT_SCOPE 和带PARENT_SCOPE 的 set 指令定义,则该变量不受带PARENT_SCOPE选项的set指令的影响。即变量的值由不带PARENT_SCOPE 的 set 指令所定义。

    拓展

    取消常规变量指令格式:

    unset(<variable>)

    场景一:在函数内使用选项PARENT_SCOPE定义变量,在函数内使用该变量,并且使用set指令不带PARENT_SCOPE选项定义过该变量。

    # CMakeLists.txt
    cmake_minimum_required (VERSION 3.10.0)
    project (set_test)

    function (test_func arg1)
    set (normal_var_in_fn nohello)
    set (normal_var_in_fn ${arg1} PARENT_SCOPE)
    message (">>> in function, value = ${normal_var_in_fn}")
    endfunction (test_func )
    test_func (hello)

    cmake 中 function 的定义见后面,这里暂不讲述。函数内的变量值为不带PARENT_SCOPE选项的set指令所定义的,选项PARENT_SCOPE定义的变量作用域在上一层函数,当前函数的变量必须使用不带选项PARENT_SCOPE定义。

    输出结果如下:

    # 输出结果
    >>> in function, value = nohello

    场景二:在函数(示例中为test_func)内使用选项PARENT_SCOPE定义变量,在另一个函数(调用者,示例中为test_func_parent)内调用该函数。

    # CMakeLists.txt
    cmake_minimum_required (VERSION 3.10.0)
    project (set_test)

    function (test_func arg1)
    set (normal_var_in_fn nohello)
    set (normal_var_in_fn ${arg1} PARENT_SCOPE)
    message (">>> in function, value = ${normal_var_in_fn}")
    endfunction (test_func)

    function (test_func_parent arg1)
    test_func (${arg1})
    message (">>> in parent function, value = ${normal_var_in_fn}")
    endfunction (test_func_parent)

    test_func_parent (hello)

    调用者函数内有该变量的定义,选项PARENT_SCOPE将变量传递到上一层调用函数。

    输出结果如下:

    # 输出结果
    >>> in function, value = nohello
    >>> in parent function, value = hello

    场景三:在目录内使用选项PARENT_SCOPE,对应的作用域只能传递到上层目录,变量的传递过程与场景二)中函数的场景类似,不再赘述。注意一点:本例在testtest/sub下分别创建一个CMakeLists.txt文件。

    # test/sub/CMakeLists.txt
    cmake_minimum_required (VERSION 3.10.0)
    project (set_sub_test)

    set (normal_var_in_sub_dir sub_hello)
    set (normal_var_in_sub_dir hello PARENT_SCOPE)

    message (">>>>>> in sub directory, value = ${normal_var_in_sub_dir}")
    # test/CMakeLists.txt
    cmake_minimum_required (VERSION 3.10.0)
    project (set_test)

    add_subdirectory (sub)

    message (">>> in top level, value = ${normal_var_in_sub_dir}")

    输出结果如下:

    # 输出结果
    >>>>>> in sub directory, value = sub_hello
    >>> in top level, value = hello
  • 设置缓存变量

    缓存变量可以理解为当第一次运行 cmake 时,这些变量缓存到一份文件中(即编译目录下的 CMakeCache.txt)。当再次运行 cmake 时,这些变量会直接使用缓存值,可以利用 ccmake 或者 cmake-gui 等工具来重新赋值。缓存变量在整个 cmake 运行过程中都可以起作用。

    指令格式如下:

    set(<variable> <value>... CACHE <type> <docstring> [FORCE])

    将缓存变量variable设置为值<value>...,默认情况下缓存变量的值不会被覆盖,使用 FORCE 选项则会覆盖现有变量。缓存变量可以通过 CMAKE 的 GUI 界面的 add entry 按钮来增加。缓存变量的实质为可以跨层级进行传递的变量,类似于全局变量。使用 CACHE 的同时,要设定 <type> 和 <docstring> ,<type> 可以理解为所存入变量类型,<docstring> 为变量的描述。缓存变量的<type>主要有以下几类:

    缓存条目的几个注意事项:

    当使用 CACHE 关键字时,且缓存(cache)中没有该变量时,变量被创建并存入缓存(cache)中,如果原缓存(cache)中有该变量,也不会改变原缓存中该变量的值,除非后面使用FORCE

    • BOOL:布尔值ON/OFFCMAKE 的 GUI 界面对此类缓存变量会提供一个复选框。

    • FILEPATH:文件路径,CMAKE 的 GUI 界面对此类缓存变量会提供一个文件选择框。

    • PATH:目录路径,CMAKE 的 GUI 界面对此类缓存变量会提供一个目录选择框。

    • STRING/STRINGS:文本行,CMAKE 的 GUI 界面对此类缓存变量会提供一个文本框(对应STRING)或下拉选择框(对应STRINGS)。

    • INTERNAL:文本行,但是只用于内部,不对外呈现。主要用于运行过程中存储变量,因此使用该type意味着使用FORCE

    1. CACHE 与 PARENT_SCOPE 不能一起使用。

    2. 如果变量先前未定义或者使用了FORCE选项,则缓存条目会直接被赋值。

    3. 同一名称(例FOO)的一般变量和缓存变量可以同时存在,但在调用该变量时(${FOO})会在先取一般变量的值,一般变量中没有再取缓存变量的值。

    4. 可以在使用 cmake 构建的时候通过-D选项来给缓存条目赋值,这样CMakeLists.txt 内的set指令只会为缓存条目添加类型。

    5. 如果变量类型是目录或者文件路径,通过-D选项传入的若只是相对路径,那么set会给这个相对路径前添加当前的工作目录以变成绝对路径(如果已经是绝对路径则不会处理)。

    6. 当改变 cache 中的变量时,同名的一般变量会被删除。一般不建议使用相同名称的一般变量和缓存变量。

  • 设置环境变量

    指令格式如下:

    set(ENV{<variable>} [<value>])

    将环境变量设置为值<value>(注意没有...),接着使用$ENV{<variable>}会得到新的值。

    环境变量设置的几个注意事项

    1. 环境变量的初始化是调用进程时的初始化。我们可以使用 set() 指令设置环境变量,使用 unset() 指令取消环境变量。这些指令只影响正在运行的 CMake 进程,而不影响整个系统环境。更改环境变量的值,不会写入调用进程,也不会被后续的构建或检测进程看到。

    2. 如果<value>值为空或者ENV{<variable>}后没有参数,则该指令会清除掉当前环境变量的值。

    3. <value>后的参数会被忽略。

    4. 环境变量在 CMake 执行过程中具有全局范围,并且它们永远不会被缓存

2.2 set_target_properties

set_target_properties 的作用是设置目标的属性,可以是目标文件输出的名称或者目录、目标文件的版本号。与之对应的,我们可以使用 get_target_properties 来获取目标文件某一属性对应的值。

指令格式如下:

set_target_properties(目标文件1 目标文件2 ...
PROPERTIES
属性1 属性值1 属性2 属性值2 ...)
2.2.1 内置属性
2.2.1.1 更改目标文件的输出名称(OUTPUT_NAME)

指令格式如下:

SET_TARGET_PROPERTIES (<old_name> PROPERTIES OUTPUT_NAME <new_name>)

示例如下:

ADD_LIBRARY (hello SHARED ${LIBHELLO_SRC})

#
 因为target不能同名,只能先生成一个临时的,然后更名
ADD_LIBRARY (hello_static STATIC ${LIBHELLO_SRC})

#
 更改输出文件名
# 将 hello_static 更名为 hello
SET_TARGET_PROPERTIES (hello_static PROPERTIES OUTPUT_NAME "hello")
2.2.1.2 设置版本号(VERSION)

指令格式如下:

# VERSION: 一般指代动态库版本
# SOVERSION: 指代API版本
SET_TARGET_PROPERTIES (<target> PROPERTIES
VERSION <version_number>
SOVERSION <soversion_number>
)

示例如下:

SET_TARGET_PROPERTIES (hello PROPERTIES VERSION 1.2 SOVERSION 1)
2.2.1.3 将目标文件保存到指定目录下

方式一:设置输出目录

我们可以设置动态库的保存目录,然后所有的动态库都会被保存到该目录下(静态库和二进制执行文件也是同理)。对应的属性如下:

  • CMAKE_RUNTIME_OUTPUT_DIRECTORY:二进制执行文件的输出目录
  • CMAKE_LIBRARY_OUTPUT_DIRECTORY:动态库的输出目录
  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY:静态库的输出目录
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)        # 动态库
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/static) # 静态库
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) # 执行文件

方式二:指向性保存

同样是指定输出目录,但是不同的动态库文件指定不同的输出目录。(静态库和二进制执行文件也是同理)。目标文件可以大致分为三种类型:二进制执行文件、动态库、静态库。保存不同目标文件所用到的属性不一样。具体分类如下:

  • RUNTIME_OUTPUT_DIRECTORY:二进制执行文件
  • LIBRARY_OUTPUT_DIRECTORY:动态库
  • ARCHIVE_OUTPUT_DIRECTORY:静态库

指令格式如下:

# <target> 需要安装的目标文件
# <folder_type> 属性类型
# <target_dir> 目标目录
SET_TARGET_PROPERTIES (<target> PROPERTIES <folder_type> <target_dir>)

以保存动态库为例:

# 将动态库 libhello.so 保存到 lib 目录下
# set_target_properties(hello PROPERTIES LIBRARY_OUTPUT_DIRECTORY "lib")

# 将目标文件保存到顶层CMakeLists.txt所处目录下的build/lib
set_target_properties(mul
PROPERTIES LIBRARY_OUTPUT_DIRECTORY
${PROJECT_SOURCE_DIR}/build/lib
)
2.2.1.4 指定Debug模式下目标文件名的后缀(DEBUG_POSTFIX)

为了区别不同模式下的不同文件,我们可以指定 Debug 模式下的目标文件名后缀为 _d,以用于区分 release 模式下生成的目标文件。

指令格式如下:

SET_TARGET_PROPERTIES (<target> PROPERTIES DEBUG_POSTFIX <suffix_name>)

示例如下:

# 指定debug模式下的目标文件名后缀为 _d,即如果是动态库文件,那就是 libhello_d.so
SET_TARGET_PROPERTIES (hello PROPERTIES DEBUG_POSTFIX _d)
2.2.2 自定义属性

set_target_properties 除了可以设置已有的属性,还可以为目标文件创造属性,并赋值。

指令格式:

SET_TARGET_PROPERTIES (<target> PROPERTIES <custom_property_name> <value>)
2.2.2.1 为一个目标创造属性

示例如下:

add_library(test_lib SHARED ${ALL_SRCS})
# 为目标文件 test_lib 创造一个 _STATUS_ 属性,属性值为 shared
set_target_properties(test_lib PROPERTIES _STATUS_ shared)

# 获取 test_lib 的 _STATUS_ 属性并保存到 var
get_target_property(var test_lib _STATUS_)
message("-------------------------------")
message("= test_lib的_STATUS_属性: ${var}")
message("-------------------------------")

输出结果如下:

-------------------------------
= test_lib的_STATUS_属性: shared
-------------------------------
2.2.2.2 为多个目标创造属性

一次为多个目标创造属性遵从按顺序创建并赋值。

add_library(test_lib SHARED ${ALL_SRCS})
add_library(test_lib_static STATIC ${ALL_SRCS})
# 为目标 test_lib 创建属性 _STATUS_ ,并赋值为 shared
# 为目标 test_lib_static 创建属性 _STATUS_STATIC_,并赋值为 static
set_target_properties(test_lib test_lib_static
PROPERTIES
_STATUS_ shared
_STATUS_STATIC_ static
)

get_target_property(var test_lib _STATUS_)
get_target_property(var_static test_lib_static _STATUS_STATIC_)
message("---------------------------------------------")
message("= test_lib 的 _STATUS_ 属性: ${var}")
message("= test_lib_static的_STATUS_STATIC_属性: ${var_static}")
message("----------------------------------------------")

输出结果如下:

---------------------------------------------
= test_lib       的   _STATUS_    属性: shared
= test_lib_static的_STATUS_STATIC_属性: static
----------------------------------------------
2.2.3 获取属性

get_target_properties 可以获取到某个目标已有的属性对应的值,并保存到指定变量中。这个属性可以是内置的,也可以是自己创建的。

指令格式:

get_target_property(<variable> <target> <target_property>)

示例可以参考 2.2.2 节“自定义属性”。

2.3 message

在 CMake 中可以通过 message 指令显示一条消息,常用于日志,指令格式如下:

message( [STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)

可以用下述可选的关键字指定消息的类型:

  • 无类型:重要消息

  • STATUS:非重要消息

  • WARNINGCMake 警告,会继续执行

  • AUTHOR_WARNINGCMake 警告(dev),会继续执行

  • FATAL_ERRORCMake 错误,终止所有处理过程

  • SEND_ERRORCMake 错误,继续执行,但是会跳过生成的步骤

CMake 的指令行工具会在构建时在 stdout 上显示 STATUS 消息,在 stderr 上显示其他所有消息。CMake 的 GUI 会在它的 log 区域显示所有消息。交互式的对话框(ccmake 和 CMakeSetup)将会在状态行上一次显示一条 STATUS 消息,而其他格式的消息会出现在交互式的弹出式对话框中。

CMake 警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。

# 输出变量的值
SET(USER_NAME, "BAOZHENG")
MESSAGE( STATUS "The user name = ${USER_NAME}.")

# 输出一般日志消息
message(STATUS "Can't detect runtime and/or arch")

# 输出警告 WARNING
message(WARNING "C compiler requires version 99 or newer.")

# 输出错误 FATAL_ERROR
message(FATAL_ERROR "FATAL: In-source builds are not allowed.
You should create a separate directory for build project.")

通过上述示例可以看出,使用大写或者小写指令均可生效,不会对结果有任何影响。

CMake 提供了 message 指令用于打印消息,它的作用相当于 C 语言中的 printfC++ 中的 std::cout 。我们可以使用 message 指令打印变量,或者其他提示信息,可以帮助我们了解 CMake 的构建过程,以及其中一些变量或者用户的变量的值。

2.4 aux_source_directory

在 CMake 中,aux_source_directory 是一个非常实用的指令,它允许开发者自动收集指定目录下的所有源文件(不包括子目录)并保存到相应的变量中。但如果在目录中存在子目录,那么它们的源文件就不会被自动包含,需要使用递归方式来处理。为了递归地包含子目录中的所有源文件,可以使用 CMake 的 file 指令和 GLOB 关键字,见下节。这个指令的基本格式如下:

aux_source_directory(<dir> <variable>)

其中,<dir> 是你想要搜索的目录,而 <variable> 是一个变量,用于存储找到的所有源文件的列表。

例如,如果你有一个名为 src 的目录,并希望将其中的所有源文件列入一个名为 MY_SOURCES_LIST 的变量中,你可以这样写:

aux_source_directory(src MY_SOURCES_LIST)

这样,MY_SOURCES_LIST 变量就会包含 src 目录下的所有源文件。

注意:尽管 aux_source_directory 指令在某些情况下非常有用,但它也有一些限制。首先,这个指令并不会检查文件的内容,它只是基于文件的扩展名来收集源文件。这意味着,如果你的目录中有其他非源代码文件,但它们的扩展名与常见的源文件扩展名相同,那么 aux_source_directory 也会将它们视为源文件并加入到列表中。使用 CMake 时,aux_source_directory 指令默认会收集 .c 和 .cpp 等 C/C++ 源文件。但对于 .java 文件,这个指令默认是不会处理的。这意味着,如果你的项目目录中同时存在 C++ 和 Java 文件,只有 C++ 文件会被自动收集。

这个指令非常适合小型项目,特别是当你确定源文件不会频繁更改时。它可以自动收集所有源文件,减少手动维护文件列表的工作量。然而,对于大型项目,尤其是多人协作的项目,源文件可能会经常更改,新文件可能会被添加,旧文件可能会被删除。在这种情况下,使用 aux_source_directory 可能会导致问题。因为当新文件被添加到目录中时,CMake 生成的构建系统并不知道它需要重新运行。这意味着我们在选择工具和方法时,应该根据项目的大小和需求来做决策。

2.5 file

如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦了。所以,在 CMake 中为我们提供了搜索文件的指令,他就是file(当然,除了搜索以外通过 file 还可以做其它事情)。指令格式如下:

file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
  • GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
  • GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。

示例如下:

搜索当前目录的 src 目录下所有的源文件,并存储到变量中。

file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)

CMAKE_CURRENT_SOURCE_DIR 宏表示当前访问的 CMakeLists.txt 文件所在的路径。

要搜索的文件路径和类型可加双引号,也可不加,也可写成如下形式:

file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")

file指令同时支持目录递归查找,递归查找 src 目录下及所有子目录中以 cmake 开头的文件并保存到CMAKE_FILES 变量里。

file(GLOB_RECURSE CMAKE_FILES "src/cmake*")

按照官方文档的说法,不建议使用 file 的 GLOB 指令来收集工程的源文件,原文解释如下

We do not recommend using GLOB to collect a list of source files from your source tree. If no CMakeLists.txt file changes when a source is added or removed then the generated build system cannot know when to ask CMake to regenerate.

大意就是,GLOB 收集的源文件增加或删除,而 CMakeLists.txt 没有发生修改时,CMake 不能识别这些文件。其实,当 CMakeLists.txt 使用 aux_source_directory 和file GLOB 查找工程源文件时,如果添加或删除源文件,都需要重新运行 CMake

file 指令除了搜索文件,还可对文件进行一系列操作,譬如读写文件、删除文件、文件重命名、拷贝文件、创建目录等等。让我们一起来学习这个功能强大的 file 指令。

2.5.1 写文件
2.5.1.1 写文件(覆盖)

使用 file 指令可以实现写(覆盖)文件,指令格式如下所示:

file(WRITE <filename> <content>...)

将写入名为 filename 的文件中。如果文件不存在,它将被创建;如果文件已经存在,WRITE 模式将覆盖它。注意文件可以使用绝对路径或相对路径指定,相对路径被解释为相对于当前源码路径。

示例如下:

# 给定内容生成 wtest.txt 文件
file(WRITE hello/wtest.txt "Hello World!n")

在 build 目录执行 cmake .. 代码之后,会在顶层目录下的 hello 目录中生成一个名为 wtest.txt 的文件,结果如下:

[root@localhost build]# cat ../hello/wtest.txt
Hello World!
2.5.1.2 写文件(追加)

使用 file 指令可以实现写(追加)文件,指令格式如下所示:

file(APPEND <filename> <content>...)

APPEND 模式表示将写入内容追加到文件末尾。注意文件可以使用绝对路径或相对路径指定,相对路径被解释为相对于当前源码路径。

示例如下:

file(WRITE hello/wtest.txt "Hello World!n") 
# 给定内容附加到已有 wtest.txt 文件最后
file(APPEND hello/wtest.txt "Append the contentn")

在 build 目录执行 cmake .. 代码之后,会在顶层目录下的 hello 目录中生成一个名为 wtest.txt 的文件,结果如下:

[root@localhost build]# cat ../hello/wtest.txt
Hello World!
Append the content
2.5.1.3 写文件(内容生成文件)

使用 file 指令可以实现由内容生成文件,指令格式如下所示:

file(GENERATE OUTPUT output-file
<INPUT input-file|CONTENT content>
[CONDITION expression])])
  • OUTPUT output-file:指定输出文件名,可以带路径(绝对路径或相对路径);

  • INPUT input-file:指定输入文件,通过输入文件的内容来生成输出文件;相对路径根据 CMAKE_CURRENT_SOURCE_DIR 的值进行处理。

  • CONTENT content:指定内容,直接指定内容来生成输出文件;

  • CONDITION expression:如果表达式 expression 条件判断为真,则生成文件、否则不生成。

同样,指定文件既可以使用相对路径、也可使用绝对路径,不过在这里,相对路径被解释为相对于当前源码的 BINARY_DIR 路径,而不是当前源码路径。

在当前源码路径顶层CMakeLists.txt 文件中,输入如下内容:

# 由前面生成的 wtest.txt 中的内容去生成 out1.txt 文件
file(GENERATE OUTPUT ${PROJECT_SOURCE_DIR}/hello/out1.txt INPUT ${PROJECT_SOURCE_DIR}/hello/wtest.txt)

# 由指定的内容生成 out2.txt
file(GENERATE OUTPUT out2.txt CONTENT "This is the out2.txt file")

# 由指定的内容生成 out3.txt,加上条件控制,用户可根据实际情况
# 用表达式判断是否需要生成文件,这里只是演示,直接是 1
file(GENERATE OUTPUT out3.txt CONTENT "This is the out3.txt file" CONDITION 1)

进入到 build 目录下执行 cmake ..,执行完 cmake ..之后会在 build 目录(也就是顶层源码的 BINARY_DIR)下生成了 out1.txt、out2.txt 和 out3.txt 三个文件,内容如下:

[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake hello Makefile out1.txt out2.txt out3.txt world
[root@localhost build]#
[root@localhost build]# cat out1.txt
Hello World!
Append the content
[root@localhost build]# cat out2.txt
This is the out2.txt file
[root@localhost build]# cat out3.txt
This is the out3.txt file
2.5.2 读文件
2.5.2.1 字节读取

使用 file 指令可以实现按字节读取文件,指令格式如下所示:

file(READ <filename> <variable>
[OFFSET <offset>] [LIMIT <max-in>] [HEX])

表示从名为 filename 的文件中读取内容并将其存储在 variable 中。可选择从给定的offset 开始,最多读取 max-in 字节。HEX 选项使数据转换为十六进制表示(对二进制数据有用)。

同样,指定文件既可以使用相对路径、也可使用绝对路径,相对路径被解释为相对于当前源码路径。 示例如下:

# file 读文件测试, 读取前面生成的 wtest.txt
file(READ "${PROJECT_SOURCE_DIR}/hello/wtest.txt" out_var)
message("out_var:"${out_var}) # 打印输出

# 读取 wtest.txt 文件:限定起始字节和大小
file(READ "${PROJECT_SOURCE_DIR}/hello/wtest.txt" out_var OFFSET 0 LIMIT 12)
message("out_var:"${out_var}) # 打印输出

file(READ "${PROJECT_SOURCE_DIR}/hello/wtest.txt" out_var OFFSET 0 LIMIT 12 HEX)
message("out_var:"${out_var}) # 打印输出

输出结果如下:

...
out_var:Hello World!
Append the content

Hello World!

48656c6c6f20576f726c6421
...
2.5.2.2 字符串形式读取

使用 file 指令可以完成字符串读取,指令格式如下所示:

file(STRINGS <filename> <variable> [<options>...])

表示从文件中解析 ASCII 字符串列表并将其存储在变量中,这个指令专用于读取字符串, 文件中的二进制数据将被忽略,回车符(r, CR)字符被忽略。

  • filename:指定需要读取的文件,可使用绝对路径、也可使用相对路径,相对路径被解释为相对于当前源码路径。

  • variable:存放字符串的变量。

  • options:可选的参数,可选择 0 个、1 个或多个选项,这些选项包括:

    • LENGTH_MAXIMUM :读取的字符串的最大长度;

    • LENGTH_MINIMUM :读取的字符串的最小长度;

    • LIMIT_COUNT :读取的行数;

    • LIMIT_INPUT :读取的字节数;

    • LIMIT_OUTPUT :存储到变量的限制字节数;

    • NEWLINE_CONSUME:把换行符也考虑进去;

    • NO_HEX_CONVERSION:除非提供此选项,否则 Intel Hex 和 Motorola S-record 文件在读取时会自动转换为二进制文件;

    • REGEX :只读取符合正则表达式的行;

    • ENCODING :指定输入文件的编码格式,目前支持的编码有:UTF-8UTF-16LE、 UTF-16BEUTF-32LEUTF-32BE。如果未提供 ENCODING 选项并且文件具有字节顺序标记, 则 ENCODING 选项将默认为尊重字节顺序标记。

示例如下:

# 从 input.txt 文件读取字符串
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var)
message("out_var:"${out_var})

输出结果如下:

out_var:Hello World!
Append the content
2.5.3 文件重命名

使用 file 指令可以对文件进行重命名操作,指令格式如下:

file(RENAME <oldname> <newname>)
  • oldname:指的是原文件

  • newname:指的是重命名后的新文件

文件既可以使用绝对路径指定,也可以使用相对路径指定,相对路径被解释为相对于当前源码路径。

示例如下:

# 文件重命名
file(RENAME "${PROJECT_SOURCE_DIR}/input.txt" "${PROJECT_SOURCE_DIR}/output.txt")

输出结果如下:

[root@localhost multi_dir]# ls
app build CMakeLists.txt hello input.txt world
[root@localhost multi_dir]# cd build/
[root@localhost build]# rm -rf *
[root@localhost build]# cmake ..
CMake Warning (dev) at CMakeLists.txt:31:
Syntax Warning in cmake code at column 19

Argument not separated from preceding token by whitespace.
This warning is for project developers. Use -Wno-dev to suppress it.

-- The C compiler identification is GNU 8.5.0
-- The CXX compiler identification is GNU 8.5.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
...
-- Generating done (0.0s)
-- Build files have been written to: /backup/cmake/multi_dir/build
[root@localhost build]#
[root@localhost build]# cd ..
[root@localhost multi_dir]# ls
app build CMakeLists.txt hello output.txt world

由上述结果可知,源文件顶层目录中的 input.txt 文件被重命名为 output.txt 文件。

2.5.4 删除文件

使用 file 指令可以删除文件,指令格式如下:

file(REMOVE [<files>...])
file(REMOVE_RECURSE [<files>...])
  • REMOVE:将删除给定的文件,但不可以删除目录

  • REMOVE_RECURSE:将删除给定的文件或目录、以及非空目录

指定文件或目录既可以使用绝对路径、也可以使用相对路径,相对路径被解释为相对于当前源码路径。

示例如下:

# file 删除文件或目录测试
file(REMOVE "${PROJECT_SOURCE_DIR}/output.txt")
# 递归的执行删除文件指令, 等于rm -r
file(REMOVE_RECURSE "${PROJECT_SOURCE_DIR}/hello/wtest.txt" "${PROJECT_SOURCE_DIR}/empty-dir"
"${PROJECT_SOURCE_DIR}/Non_empty-dir")

输出结果如下:

[root@localhost multi_dir]# ls
app build CMakeLists.txt hello output.txt world
[root@localhost multi_dir]# mkdir empty
[root@localhost multi_dir]# mkdir non-empty
[root@localhost multi_dir]# cd non-empty/
[root@localhost non-empty]# touch file
[root@localhost non-empty]# echo 1 > file
[root@localhost non-empty]# cat file
1
[root@localhost non-empty]# cd ..
[root@localhost multi_dir]# ls
app build CMakeLists.txt empty hello non-empty output.txt world
[root@localhost multi_dir]#
[root@localhost multi_dir]# tree -L 2
.
├── app
│ └── main.c
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── hello
│ ├── Makefile
│ └── world
├── CMakeLists.txt
├── empty
├── hello
│ ├── CMakeLists.txt
│ ├── include
│ ├── src
│ └── wtest.txt
├── non-empty
│ └── file
├── output.txt
└── world
├── CMakeLists.txt
├── include
└── src

13 directories, 10 files
[root@localhost multi_dir]# cd build/
[root@localhost build]# rm -rf *
[root@localhost build]# cmake ..
CMake Warning (dev) at CMakeLists.txt:31:
Syntax Warning in cmake code at column 19

Argument not separated from preceding token by whitespace.
This warning is for project developers. Use -Wno-dev to suppress it.

-- The C compiler identification is GNU 8.5.0
-- The CXX compiler identification is GNU 8.5.0
...
[root@localhost build]# tree -L 2 ../
../
├── app
│ └── main.c
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── hello
│ ├── Makefile
│ └── world
├── CMakeLists.txt
├── hello
│ ├── CMakeLists.txt
│ ├── include
│ └── src
└── world
├── CMakeLists.txt
├── include
└── src

11 directories, 7 files

根据上述结果可知,通过 file 指令删除了 hello 目录下的 wtest.txtempty 目录及其下文件,以及 non-empty 目录。

2.5.5 计算文件的 hash 值

file 指令可以计算指定文件内容的加密散列(hash 值)并将其存储在变量中。指令如下所示:

file(<MD5|SHA1|SHA224|SHA256|SHA384|SHA512> <filename> <variable>)

MD5|SHA1|SHA224|SHA256|SHA384|SHA512 表示不同的计算 hash 的算法,必须要指定其中之一, filename 指定文件(可使用绝对路径、也可使用相对路径,相对路径被解释为相对于当前源码的 BINARY_DIR),将计算结果存储在 variable 变量中。示例如下:

# 计算文件的 hash 值
file(SHA256 "${PROJECT_SOURCE_DIR}/input.txt" out_var)
message("out_var:"${out_var})

输出结果如下:

out_var:8ac95a5bcd663f39823c7ed64bdd1aeeca43af08df19c29e6dcf5798cad92920
2.5.6 下载文件

file 指令可以实现下载指定文件,指令格式如下:

file(DOWNLOAD <url> [<file>] [<options>...])

DOWNLOAD 子指令将给定的 <url> 下载到本地 <file> 。

在 3.19 版本加入: 如果没有为 file(DOWNLOAD) 指定 <file>,则文件不会被保存。如果您想知道是否可以下载文件(例如,检查它是否存在)而不实际将其保存在任何地方,这将很有用。

示例如下:

file(DOWNLOAD https://github.com/Kitware/CMake/releases/download/v3.29.0-rc4/cmake-3.29.0-rc4-linux-x86_64.sh cmake-linux-x86.sh)

结果如下:

[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake cmake-linux-x86.sh hello Makefile world

根据上述结果可知,成功下载了文件 cmake-linux-x86.sh 。

2.5.7 创建目录

file 指令可以实现创建目录,指令格式如下:

file(MAKE_DIRECTORY [dir1 dir2 ...])

使用该指令即可实现在 CMakeLists.txt 的对应目录下产生自定义的文件夹。

示例如下:

file(MAKE_DIRECTORY custom_dir)

结果如下:

[root@localhost multi_dir]# ls
app build CMakeLists.txt custom_dir hello input.txt world

根据结果可知,成功创建了自定义文件夹 custom_dir

2.6 include_directories

在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在 CMake 中设置要包含的目录也很简单,通过一个指令就可以设置头文件位置,指令格式如下:

include_directories([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)

其中 dir1dir2 等参数是包含头文件的目录路径。这条指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的后面。

默认情况下,指定的目录将附加到当前目录列表中。可以通过将 CMAKE_INCLUDE_DIRECTORIES_BEFORE 设置为 ON 来更改此默认行为。通过显式使用 AFTER 或 BEFORE ,您可以在附加和前置之间进行选择,而与默认值无关。

如果给出 SYSTEM 选项,编译器将被告知这些目录在某些平台上是系统包含目录。发出此设置信号可能会产生一些效果,例如编译器跳过警告,或者在依赖项计算中不考虑这些固定安装系统文件 – 请参阅编译器文档。

注意

  • include_directories() 函数可以多次调用,每次都会追加新的包含目录。
  • 相对于项目根目录的路径可以使用 CMAKE_CURRENT_SOURCE_DIR 和 CMAKE_CURRENT_BINARY_DIR 变量。
  • 如果指定的目录不存在,CMake 会报错;如果包含目录中包含非头文件,可能会导致编译错误。

示例如下:

# hello文件内CMakeLists.txt包含语句
project(hello VERSION 1.0.0.0 DESCRIPTION "主项目" HOMEPAGE_URL https://www.example.com LANGUAGES CXX)

include_directories(./include)

set(ENV{HELLO} hello_custom)
set(DIR_SRCS ./src/hello.c)
set(LIBRARY_OUTPUT_PATH, ${PROJECT_SOURCE_DIR}/build/bin)

add_library(hello SHARED ${DIR_SRCS})

项目内文件夹构造:

[root@localhost hello]# tree -L 2
.
├── CMakeLists.txt
├── hello_custom
├── include
│ └── hello.h
├── src
│ └── hello.c
└── wtest.txt

3 directories, 4 files

该项目会在 ./include 目录下搜索头文件,并生成名为 hello 的动态库文件。

2.7 add_executable

add_executable 是 CMake 中的一个指令,用于创建一个可执行目标。该指令可以指定可执行文件的名称、源文件、依赖项和其他属性。指令格式如下:

add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])

其中:

  • name:可执行文件的名称
  • sourceX:可执行文件所需的源文件

注意

如果指定了 WIN32 ,则将在创建的目标上设置属性 WIN32_EXECUTABLE 。

如果指定了 MACOSX_BUNDLE ,则将在创建的目标上设置相应的属性。

如果指定了 EXCLUDE_FROM_ALL ,则将在创建的目标上设置相应的属性。

示例如下:

set(DIR_SRCS ./app/main.c)
add_executable(HelloWorld ${DIR_SRCS})
target_link_libraries(HelloWorld PUBLIC hello)

以上代码创建一个名为 HelloWorld 的可执行文件,该文件由 ./app/main.c 源文件组成,最后链接了动态库 hello 。

结果如下:

[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake hello HelloWorld Makefile world

add_executable 函数还支持以下可选项:

  • WIN32:指定可执行文件为 Windows 平台的 PE 格式
  • MACOSX_BUNDLE:指定可执行文件为 macOS 平台的应用程序包
  • POSITION_INDEPENDENT_CODE:指定可执行文件为位置无关代码
  • LINK_LIBRARIES:指定可执行文件的依赖项库

例如,以下代码创建一个名为 my_program 的可执行文件,该文件为 Windows 平台的 PE 格式,并依赖于 libstdc++ 库:

add_executable(my_program main.cpp foo.cpp
WIN32
LINK_LIBRARIES libstdc++
)

2.8 add_library

add_library 是 CMake 中用于创建库目标的指令。库目标可以是静态库、动态库或模块库。指令格式如下:

add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1 [source2 ...])

其中:

  • name:库目标的名称。
  • STATICSHARED 或 MODULE:指定库的类型。
  • EXCLUDE_FROM_ALL:可选参数,指示 CMake 不应将此库目标包含在任何默认的链接操作中。
  • source1source2 等:要包含在库中的源文件。

示例如下:

以下示例创建了一个名为 world 的静态库:

add_library(world STATIC ./src/world.c)

在 Linux 中,静态库名字分为三部分:lib+库名字+.a,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。在 Windows 中虽然库名和 Linux 格式不同,但也只需指定出名字即可。上面示例最终就会生成对应的静态库文件libworld.a

以下示例创建了一个名为 hello 的动态库:

add_library(hello SHARED ./src/hello.c)

在 Linux 中,动态库名字分为三部分:lib+库名字+.so,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。在 Windows 中虽然库名和 Linux 格式不同,但也只需指定出名字即可。上面示例最终就会生成对应的动态库文件libhello.so

结果如下:

[root@localhost build]# tree -L 2
.
├── CMakeCache.txt
├── CMakeFiles
│ ├── 3.26.5
│ ├── cmake.check_cache
│ ├── CMakeConfigureLog.yaml
│ ├── CMakeDirectoryInformation.cmake
│ ├── HelloWorld.dir
│ ├── Makefile2
│ ├── Makefile.cmake
│ ├── pkgRedirects
│ ├── progress.marks
│ └── TargetDirectories.txt
├── cmake_install.cmake
├── hello
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── libhello.so
│ └── Makefile
├── HelloWorld
├── Makefile
└── world
├── CMakeFiles
├── cmake_install.cmake
├── libworld.a
└── Makefile

8 directories, 17 files

通过上述结果可以看出,在 hello 文件夹和 world 文件夹下分别生成了动态库文件libhello.so 和静态库文件 libworld.a 。

2.9 target_link_libraries

target_link_libraries 指令是在 CMake 中用于指定目标依赖的库函数。它可以链接静态库和动态库。指令格式如下:

target_link_libraries(<target>  <PRIVATE|PUBLIC|INTERFACE> [item1 
<PRIVATE|PUBLIC|INTERFACE> [item2 [...]]]
[[debug|optimized|general] <item>] ...)
  • target: 指定要链接库的目标
  • PRIVATE|PUBLIC|INTERFACE:指定库文件的访问权限,默认为PUBLIC
  • item: 指定库的名称或路径,在指定的时候一般会掐头(lib)去尾(.so/.a)
  • debug/optimized/general: 可选参数,指定链接库的类型

注意:使用 target_link_libraries 指令可以链接动态库文件,也可以链接静态库文件。

示例代码:

target_link_libraries(HelloWorld PUBLIC hello)

结果显示:

# 查看 hello.c文件内容,可见hello.c文件中输出“hello.”字符串
[root@localhost build]# cat ../hello/src/hello.c
#include "hello.h"
#include <stdio.h>

void hello()
{
printf("hello.n");
}
# hello 文件夹下含有 libhello.so 动态库
[root@localhost build]# tree -L 2 .
.
├── CMakeCache.txt
├── CMakeFiles
│ ├── 3.26.5
│ ├── cmake.check_cache
│ ├── CMakeConfigureLog.yaml
│ ├── CMakeDirectoryInformation.cmake
│ ├── CMakeScratch
│ ├── HelloWorld.dir
│ ├── Makefile2
│ ├── Makefile.cmake
│ ├── pkgRedirects
│ ├── progress.marks
│ └── TargetDirectories.txt
├── cmake_install.cmake
├── hello
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── libhello.so
│ └── Makefile
├── HelloWorld
├── Makefile
└── world
├── CMakeFiles
├── cmake_install.cmake
├── libworld.a
└── Makefile

9 directories, 17 files
[root@localhost build]# ./HelloWorld
hello.
world.

根据可执行文件 HelloWorld 的执行结果可知,成功链接了动态库 libhello.so 文件。

注意:可执行文件执行以后,去加载动态库时,可能发生无法找到动态库的情况,为解决动态库无法加载问题,在 CMake 中可以在生成可执行程序之前,通过指令 link_directories(path) 指定要链接的动态库的位置;另外,指定静态库位置也可以使用这个指令。

2.10 option

option 是 CMake 中用于定义选项的指令。选项可以用来控制项目的构建过程,例如启用或禁用某些功能,指定编译选项等。指令格式如下:

option(<option_name> <help_text> [<initial_value>])
  • <option_name>:选项名称,必须以字母开头,后面可以跟字母、数字或下划线。
  • <help_text>:选项的帮助文本,将在 cmake --help 指令中显示。
  • <initial_value>:选项的初始值,可以是 ONOFF 或其他任意字符串。

示例如下:

# 项目顶层CMakeLists.txt文件
option(BUILD_DEBUG "Build with debug symbols" ON)

if(BUILD_DEBUG)
message(STATUS "BUILD_DEBUG:"${BUILD_DEBUG})
add_definitions(-DDEBUG)
endif()

该示例代码定义了一个名为 BUILD_DEBUG 的选项,其帮助文本为 “Build with debug symbols”,初始值为 ON。如果用户在指令行中指定 -D BUILD_DEBUG=OFF,则该选项将被关闭,并且不会添加 -DDEBUG 编译定义。

源码文件 ./app/main.c

#include <stdio.h>
#include "hello.h"
#include "world.h"

int main()
{
    #ifdef DEBUG
    printf("Program start:n");
    printf("DEBUG=%dn",DEBUG);
    #endif
    hello();
    world();
    #ifdef DEBUG
    printf("Program finish!n");
    #endif

    return 0;
}

结果如下:

# cmake不指定 -D BUILD_DEBUG,即使用默认选项
[root@localhost build]# cmake ..
...
-- BUILD_DEBUG:ON
...
[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake hello Makefile world
[root@localhost build]# make
[ 16%] Building C object world/CMakeFiles/world.dir/src/world.c.o
[ 33%] Linking C static library libworld.a
[ 33%] Built target world
[ 50%] Building C object hello/CMakeFiles/hello.dir/src/hello.c.o
[ 66%] Linking C shared library libhello.so
[ 66%] Built target hello
[ 83%] Building C object CMakeFiles/HelloWorld.dir/app/main.c.o
[100%] Linking C executable HelloWorld
[100%] Built target HelloWorld
[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake hello HelloWorld Makefile world
[root@localhost build]# ./HelloWorld
Program start:
DEBUG=1
hello.
world.
Program finish!

# 指令行中指定 -D BUILD_DEBUG=OFF
[root@localhost build]# cmake -D BUILD_DEBUG=OFF .. | grep BUILD_DEBUG
[root@localhost build]# make
[ 16%] Building C object world/CMakeFiles/world.dir/src/world.c.o
[ 33%] Linking C static library libworld.a
[ 33%] Built target world
[ 50%] Building C object hello/CMakeFiles/hello.dir/src/hello.c.o
[ 66%] Linking C shared library libhello.so
[ 66%] Built target hello
[ 83%] Building C object CMakeFiles/HelloWorld.dir/app/main.c.o
[100%] Linking C executable HelloWorld
[100%] Built target HelloWorld
[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake hello HelloWorld Makefile world
[root@localhost build]# ./HelloWorld
hello.
world.

根据上面结果可知,通过option 在 CMakeLists.txt 中设置一个选项 BUILD_DEBUG,并通过 add_definitions 定义一个宏,即相当于在源文件中生成 #define DEBUG ,则可直接在 C 的源文件中使用定义的 DEBUG 宏,源文件和打印结果即可证明 DEBUG 成功设置为1

如果用户在指令行中指定 -D BUILD_DEBUG=OFF,则该选项将被关闭,并且不会添加 -DDEBUG 编译定义。通过上述打印结果中无 DEBUG 语句即可说明。

2.11 add_definitions

add_definitions  指令用于在 CMake 构建过程中向编译器指令行添加预处理器定义,参数之间用空格分割。这些定义可以用来控制代码的行为,例如启用或禁用功能,或指定特定的编译选项。add_definitions的功能和C/C++中的#define是一样的,可直接通过CMakeLists.txt 文件定义源文件中使用的宏变量;指令格式如下:

add_definitions([<definitions>...])

其中 <definitions> 可以是任何有效的预处理器定义,例如:

  • -DDEBUG
  • -DNDEBUG
  • -DFOO=1
  • -DBAR="Hello, world!"

add_definitions 会将指定的预处理器定义添加到所有目标的编译指令行中,包括可执行文件、共享库和静态库。如果要将预处理器定义添加到特定的目标,可以使用 target_compile_definitions 指令。add_definitions 一般会与 option 指令一起使用。通过 option 定义的选项可以用于控制条件语句,例如 if() 和 else() 语句,从而决定是否向编译器指令行添加预处理器定义,从而实现更加灵活的使用编译语句。

示例可参考 2.10 option 节。

2.12 add_subdirectory

add_subdirectory 是 CMake 中一个重要的指令,用于将子目录添加到构建过程中。它可以帮助您将项目组织成多个模块,并分别处理每个模块的构建。指令格式如下:

add_subdirectory(<source_dir> [<binary_dir>])
  • source_dir 是子目录的源代码目录。
  • binary_dir 是子目录的构建目录。如果省略,则使用 source_dir 作为构建目录。

示例如下:

# 项目顶层CMakeLists.txt文件
cmake_minimum_required(VERSION 3.0)
...
add_subdirectory(hello)
add_subdirectory(world)
...
add_executable(HelloWorld ${DIR_SRCS})
target_link_libraries(HelloWorld PUBLIC hello)

hello  和 world 目录包含子项目的源代码,即两个库文件的源码。add_subdirectory(hello)  和 add_subdirectory(world) 指令将 hello 和 world 目录添加到构建过程中。add_subdirectory 指令会递归地处理子目录中的 CMakeLists.txt 文件。您可以使用 target_link_libraries 指令将子目录中的目标链接到顶层项目的目标

注意

  • 如果 source_dir 不存在,则 add_subdirectory 指令会失败。
  • 如果 source_dir 中没有 CMakeLists.txt 文件,则 add_subdirectory 指令会失败。

add_subdirectory 指令是 CMake 中一个强大的工具,用于组织项目和管理构建过程。通过使用 add_subdirectory 指令,您可以将项目分解成多个模块,并分别处理每个模块的构建。这可以提高项目的可维护性和可扩展性。有关多模块项目构建可以参考【CMake学习笔记】| 模块化项目管理(一)

2.13 configure_file

CMake 中的 configure_file 指令通过读取输入文件中的内容,将 CMakeLists.txt 文件中的变量转变为 C/C++ 中可识别的宏定义,然后存入另一个文件中。指令格式如下:

configure_file(<input> <output>
[NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS |
FILE_PERMISSIONS <permissions>...]
[COPYONLY] [ESCAPE_QUOTES] [@ONLY]
[NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])

主要参数说明:

  • input:输入的文件名,通常为 xxx-config.h.in

  • output:输出的文件名,通常为 xxx-config.h

  • COPYONLY:仅拷贝 input 文件里面的内容到 output 文件, 不进行变量的替换

  • ESCAPE_QUOTES:忽略反斜杠(C语言风格)的转义

  • @ONLY:在 <input> 文件中只使用 @VAR@ 的方式获取变量值,不适用 ${VAR} 的方式

  • NEWLINE_STYLE:指定output文件的换行风格,例如 Linux 以 n 作为换行,Windows 以 rn 作为换行。该参数后面要指明换行的规则,如UNIX|DOS|WIN32|LF|CRLF

注意: COPYONLY 和 NEWLINE_STYLE 是冲突的,不能同时使用;

示例1如下:

# 项目顶层目录新增config.h.in文件
[root@localhost build]# ls -l ../
total 12
drwxr-xr-x. 2 root root 20 Mar 19 01:17 app
drwxr-xr-x. 5 root root 131 Mar 19 01:18 build
-rw-r--r--. 1 root root 2794 Mar 19 01:18 CMakeLists.txt
-rw-r--r--. 1 root root 334 Mar 19 01:14 config.h.in
drwxr-xr-x. 2 root root 6 Mar 18 01:07 custom_dir
drwxr-xr-x. 5 root root 91 Mar 18 12:17 hello
-rw-r--r--. 1 root root 31 Mar 14 09:27 input.txt
drwxr-xr-x. 4 root root 54 Mar 18 09:03 world

# 查看config.h.in文件内容
[root@localhost build]# cat ../config.h.in
/**
* cmakedefine 会根据变量的值是否为真(类似 if)来变换为 #define VAR ... 或 #undef VAR
*/
#cmakedefine CMAKEDEFINE_VAR1 @CMAKEDEFINE_VAR1@
#cmakedefine CMAKEDEFINE_VAR2 ${CMAKEDEFINE_VAR2}

/**
* define 会直接根据规则来替换
*/
#define DEFINE_VAR1 @DEFINE_VAR1@
#define DEFINE_VAR2 ${DEFINE_VAR2}
[root@localhost build]#

# 顶层CMakeLists.txt文件新增内容
set(CMAKEDEFINE_VAR1 1)
set(CMAKEDEFINE_VAR2 0)

set(DEFINE_VAR1 1)
set(DEFINE_VAR2 0)

configure_file ("${PROJECT_SOURCE_DIR}/config.h.in" "${PROJECT_BINARY_DIR}/config.h")

结果如下:

# 在build目录下执行cmake ..
[root@localhost build]# cmake ..

# 在build目录下生成config.h文件
[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake config.h hello Makefile world

# 查看生成的config.h文件内容
[root@localhost build]# cat config.h
/**
* cmakedefine 会根据变量的值是否为真(类似 if)来变换为 #define VAR ... 或 #undef VAR
*/
#define CMAKEDEFINE_VAR1 1
/* #undef CMAKEDEFINE_VAR2 */

/**
* define 会直接根据规则来替换
*/
#define DEFINE_VAR1 1
#define DEFINE_VAR2 0


# 查看app/main.c文件的内容
[root@localhost build]# cat ../app/main.c
#include <stdio.h>
#include "hello.h"
#include "world.h"
#include "../build/config.h"

int main()
{
#ifdef DEBUG
printf("Program start:n");
printf("DEBUG=%dn",DEBUG);
#endif

#ifdef CMAKEDEFINE_VAR1
fprintf(stdout,"CMAKEDEFINE_VAR1 = %dn", CMAKEDEFINE_VAR1);
#endif

#ifdef CMAKEDEFINE_VAR2
fprintf(stdout,"CMAKEDEFINE_VAR2 = %dn", CMAKEDEFINE_VAR2);
#endif

#ifdef DEFINE_VAR1
fprintf(stdout,"DEFINE_VAR1 = %dn", DEFINE_VAR1);
#endif

#ifdef DEFINE_VAR2
fprintf(stdout,"DEFINE_VAR2 = %dn", DEFINE_VAR2);
#endif

hello();
world();
#ifdef DEBUG
printf("Program finish!n");
#endif

return 0;
}

# 编译生成可执行文件HelloWorld
[root@localhost build]# make
[ 16%] Building C object world/CMakeFiles/world.dir/src/world.c.o
[ 33%] Linking C static library libworld.a
[ 33%] Built target world
[ 50%] Building C object hello/CMakeFiles/hello.dir/src/hello.c.o
[ 66%] Linking C shared library libhello.so
[ 66%] Built target hello
[ 83%] Building C object CMakeFiles/HelloWorld.dir/app/main.c.o
[100%] Linking C executable HelloWorld
[100%] Built target HelloWorld

# 执行可执行文件 HelloWorld
[root@localhost build]# ./HelloWorld
Program start:
DEBUG=1
CMAKEDEFINE_VAR1 = 1
DEFINE_VAR1 = 1
DEFINE_VAR2 = 0
hello.
world.
Program finish!

根据上述输出结果可以看出,configure_file 指令直接将 config.h.in 文件中 cmakedefine 定义的变量通过 CMakeLists.txt 文件中设置的变量的值,转化为 config.h 文件中的宏供源文件中调用。

注意

  • 对于#cmakedefine var @var@或#cmakedefine var ${var}@@ 之间或${} 内的变量名称要与 cmakedefine 后的变量名称一样,否则替换不成功。
  • configure_file 要放在变量定义之后(验证发现OPTION定义的变量可以在configure_file之后)。

有时你可能会在 config.h.in 文件中看到 cmakedefine01 命令,如:#cmakedefine01 var,其含义为:如果 var 有定义,则实际生成的效果为:#define var 1;如果 var 未定义,生成的效果为:#define var 0。可通过如下示例2说明:

[root@localhost build]# cat ../config.h.in
/**
* cmakedefine 会根据变量的值是否为真(类似 if)来变换为 #define VAR ... 或 #undef VAR
*/
#cmakedefine CMAKEDEFINE_VAR1 @CMAKEDEFINE_VAR1@
#cmakedefine01 CMAKEDEFINE_VAR2

/**
* define 会直接根据规则来替换
*/
#define DEFINE_VAR1 @DEFINE_VAR1@
#define DEFINE_VAR2 ${DEFINE_VAR2}
[root@localhost build]# cmake ..
-- The C compiler identification is GNU 8.5.0
-- The CXX compiler identification is GNU 8.5.0
...
-- Build files have been written to: /backup/cmake/multi_dir/build
[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake config.h hello Makefile world
[root@localhost build]# cat config.h
/**
* cmakedefine 会根据变量的值是否为真(类似 if)来变换为 #define VAR ... 或 #undef VAR
*/
#define CMAKEDEFINE_VAR1 1
#define CMAKEDEFINE_VAR2 0

/**
* define 会直接根据规则来替换
*/
#define DEFINE_VAR1 1
#define DEFINE_VAR2 0

# 查看app/main.c文件的内容
[root@localhost build]# cat ../app/main.c
#include <stdio.h>
#include "hello.h"
#include "world.h"
#include "../build/config.h"

int main()
{
#ifdef DEBUG
printf("Program start:n");
printf("DEBUG=%dn",DEBUG);
#endif

#ifdef CMAKEDEFINE_VAR1
fprintf(stdout,"CMAKEDEFINE_VAR1 = %dn", CMAKEDEFINE_VAR1);
#endif

#ifdef CMAKEDEFINE_VAR2
fprintf(stdout,"CMAKEDEFINE_VAR2 = %dn", CMAKEDEFINE_VAR2);
#endif

#ifdef DEFINE_VAR1
fprintf(stdout,"DEFINE_VAR1 = %dn", DEFINE_VAR1);
#endif

#ifdef DEFINE_VAR2
fprintf(stdout,"DEFINE_VAR2 = %dn", DEFINE_VAR2);
#endif

hello();
world();
#ifdef DEBUG
printf("Program finish!n");
#endif

return 0;
}

# 编译生成可执行文件 HelloWorld
[root@localhost build]# make
[ 16%] Building C object world/CMakeFiles/world.dir/src/world.c.o
[ 33%] Linking C static library libworld.a
[ 33%] Built target world
[ 50%] Building C object hello/CMakeFiles/hello.dir/src/hello.c.o
[ 66%] Linking C shared library libhello.so
[ 66%] Built target hello
[ 83%] Building C object CMakeFiles/HelloWorld.dir/app/main.c.o
[100%] Linking C executable HelloWorld
[100%] Built target HelloWorld

# 执行可执行文件 HelloWorld
[root@localhost build]# ./HelloWorld
Program start:
DEBUG=1
CMAKEDEFINE_VAR1 = 1
CMAKEDEFINE_VAR2 = 0
DEFINE_VAR1 = 1
DEFINE_VAR2 = 0
hello.
world.
Program finish!

通过对比本节示例 1 和示例 2 ,可以发现 #cmakedefine01 与 #cmakedefine 的区别:通过 #cmakedefine 定义的变量如果在 CMakeLists.txt 文件中值设置为 0,则在生成的 config.h 文件中该变量对应的宏则未定义,源文件中无法使用该宏(即宏在源文件中未定义);而通过 #cmakedefine01 定义的变量如果在 CMakeLists.txt 文件中值设置为 0,则在生成的 config.h 文件中该变量对应的宏会被定义,且其值为 0 ,源文件中可以使用该宏(即宏在源文件中已被定义),只是值为 0 。

2.14 install

CMake 中的 install 指令用于生成项目的安装规则。install 用于指定在安装时运行的规则。它可以用来安装很多内容,可以包括目标二进制、动态库、静态库以及文件、目录、脚本等:

install(TARGETS <target>... [...])
install({FILES | PROGRAMS} <file>... [...])
install(DIRECTORY <dir>... [...])
install(SCRIPT <file> [...])
install(CODE <code> [...])
install(EXPORT <export-name> [...])

有时候,也会用到一个非常有用的变量CMAKE_INSTALL_PREFIX用于指定cmake install时的相对地址前缀。用法如下:

cmake -DCMAKE_INSTALL_PREFIX=/usr ..
2.14.1 目标文件的安装

指令格式如下:

install(TARGETS targets... [EXPORT <export-name>]
[[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
[DESTINATION <dir>]
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[NAMELINK_COMPONENT <component>]
[OPTIONAL] [EXCLUDE_FROM_ALL]
[NAMELINK_ONLY|NAMELINK_SKIP]
] [...]
[INCLUDES DESTINATION [<dir> ...]]
)

参数中的TARGET可以是很多种目标文件,最常见的是通过ADD_EXECUTABLE或者ADD_LIBRARY定义的目标文件,即可执行二进制、动态库、静态库

目标文件 内容 安装目录变量 默认安装文件夹
ARCHIVE 静态库 ${CMAKE_INSTALL_LIBDIR} lib
LIBRARY 动态库 ${CMAKE_INSTALL_LIBDIR} lib
RUNTIME 可执行二进制文件 ${CMAKE_INSTALL_BINDIR} bin
PUBLIC_HEADER 与库关联的PUBLIC头文件 ${CMAKE_INSTALL_INCLUDEDIR} include
PRIVATE_HEADER 与库关联的PRIVATE头文件 ${CMAKE_INSTALL_INCLUDEDIR} include

为了符合一般的默认安装路径,如果设置了DESTINATION参数,推荐配置在安装目录变量下的文件夹。

示例如下:

INSTALL(TARGETS myrun mylib mystaticlib
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

上面的例子会将:可执行二进制 myrun 安装到 ${CMAKE_INSTALL_BINDIR} 目录,动态库libmylib.so 安装到 ${CMAKE_INSTALL_LIBDIR}目录,静态库 libmystaticlib.a 安装到${CMAKE_INSTALL_LIBDIR}目录。

该命令的其他一些参数的含义:

  • DESTINATION:指定磁盘上要安装文件的目录;

  • PERMISSIONS:指定安装文件的权限。有效权限是 OWNER_READOWNER_WRITEOWNER_EXECUTEGROUP_READGROUP_WRITEGROUP_EXECUTEWORLD_READWORLD_WRITEWORLD_EXECUTESETUID 和 SETGID

  • CONFIGURATIONS:指定安装规则适用的构建配置列表( DEBUG 或 RELEASE 等 );

  • EXCLUDE_FROM_ALL:指定该文件从完整安装中排除,仅作为特定于组件的安装的一部分进行安装;

  • OPTIONAL:如果要安装的文件不存在,则指定不是错误。

注意一下 CONFIGURATIONS 参数,此选项指定的值仅适用于此选项之后列出的选项:例如,要为调试和发布配置设置单独的安装路径,请执行以下操作:

install(TARGETS target
CONFIGURATIONS Debug
RUNTIME DESTINATION Debug/bin)
install(TARGETS target
CONFIGURATIONS Release
RUNTIME DESTINATION Release/bin)

也就是说,DEBUG和RELEASE版本的DESTINATION安装路径不同,那么DESTINATION必须在CONFIGUATIONS后面。

2.14.2 普通文件的安装

指令格式如下:

install(<FILES|PROGRAMS> files...
TYPE <type> | DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL] [EXCLUDE_FROM_ALL])

FILES|PROGRAMS若为相对路径给出的文件名,将相对于当前源目录进行解释。其中,FILES为普通的文本文件,PROGRAMS指的是非目标文件的可执行程序(如脚本文件)

注意:如果未提供 PERMISSIONS 参数,默认情况下,普通的文本文件将具有OWNER_WRITEOWNER_READGROUP_READ 和 WORLD_READ 权限,即644权限;而非目标文件的可执行程序将具有OWNER_EXECUTEGROUP_EXECUTE, 和WORLD_EXECUTE,即 755 权限。

其中,不同的TYPEcmake也提供了默认的安装路径,如下表:

TYPE类型 安装目录变量 默认安装文件夹
BIN ${CMAKE_INSTALL_BINDIR} bin
SBIN ${CMAKE_INSTALL_SBINDIR} sbin
LIB ${CMAKE_INSTALL_LIBDIR} lib
INCLUDE ${CMAKE_INSTALL_INCLUDEDIR} include
SYSCONF ${CMAKE_INSTALL_SYSCONFDIR} etc
SHAREDSTATE ${CMAKE_INSTALL_SHARESTATEDIR} com
LOCALSTATE ${CMAKE_INSTALL_LOCALSTATEDIR} var
RUNSTATE ${CMAKE_INSTALL_RUNSTATEDIR} /run
DATA ${CMAKE_INSTALL_DATADIR}
INFO ${CMAKE_INSTALL_INFODIR} /info
LOCALE ${CMAKE_INSTALL_LOCALEDIR} /locale
MAN ${CMAKE_INSTALL_MANDIR} /man
DOC ${CMAKE_INSTALL_DOCDIR} /doc

请注意,某些类型的内置默认值使用DATAROOT目录作为前缀,以CMAKE_INSTALL_DATAROOTDIR变量值为内容。

该命令的其他一些参数的含义:

  • DESTINATION:指定磁盘上要安装文件的目录;

  • PERMISSIONS:指定安装文件的权限。有效权限是 OWNER_READOWNER_WRITEOWNER_EXECUTEGROUP_READGROUP_WRITEGROUP_EXECUTEWORLD_READWORLD_WRITEWORLD_EXECUTESETUIDSETGID

  • CONFIGURATIONS:指定安装规则适用的构建配置列表( DEBUG 或 RELEASE 等);

  • EXCLUDE_FROM_ALL:指定该文件从完整安装中排除,仅作为特定于组件的安装的一部分进行安装;

  • OPTIONAL:如果要安装的文件不存在,则指定不是错误;

  • RENAME:指定已安装文件的名称,该名称可能与原始文件不同。仅当命令安装了单个文件时,才允许重命名。

2.14.3 目录的安装

指令的格式如下:

install(DIRECTORY dirs...
TYPE <type> | DESTINATION <dir>
[FILE_PERMISSIONS permissions...]
[DIRECTORY_PERMISSIONS permissions...]
[USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>] [EXCLUDE_FROM_ALL]
[FILES_MATCHING]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS permissions...]] [...])

该指令将一个或多个目录的内容安装到指定的目的地,目录结构被逐个复制到目标位置。每个目录名称的最后一个组成部分都附加到目标目录中,但是可以使用后跟斜杠来避免这种情况,因为它将最后一个组成部分留空。这是什么意思呢?

比如,DIRECTORY 后面如果是 abc 意味着 abc 这个目录会安装在目标路径下,abc/ 意味着 abc 这个目录的内容会被安装在目标路径下,而 abc 目录本身却不会被安装。即,如果目录名不以/结尾,那么这个目录将被安装为目标路径下的 abc,如果目录名以 / 结尾,代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。 FILE_PERMISSIONS 和 DIRECTORY_PERMISSIONS 选项指定目标中文件和目录的权限。如果指定了 USE_SOURCE_PERMISSIONS 而未指定 FILE_PERMISSIONS,则将从源目录结构中复制文件权限。如果未指定权限,则将为文件提供在命令的 FILES 形式中指定的默认权限(644权限),而目录将被赋予在命令的 PROGRAMS 形式中指定的默认权限(755 权限)。可以使用PATTERNREGEX选项以精细的粒度控制目录的安装,可以指定一个通配模式或正则表达式以匹配输入目录中遇到的目录或文件PATTERN仅匹配完整的文件名,而REGEX将匹配文件名的任何部分,但它可以使用/和$模拟PATTERN行为

某些跟随PATTERN或REGEX表达式后的参数,仅应用于满足表达式的文件或目录。如:EXCLUDE选项将跳过匹配的文件或目录。PERMISSIONS选项将覆盖匹配文件或目录的权限设置。

示例如下:

install(DIRECTORY icons scripts/ DESTINATION share/myproj
PATTERN "CVS" EXCLUDE
PATTERN "scripts/*"
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ
GROUP_EXECUTE GROUP_READ)

这条命令的执行结果是:将 icons 目录安装到 share/myproj,将 scripts/ 中的内容安装到 share/myproj,两个目录均不包含目录名为 CVS 的子目录,对于 scripts/* 的文件指定权限为OWNER_EXECUTEOWNER_WRITEOWNER_READGROUP_EXECUTEGROUP_READ

2.14.4 安装时脚本的运行

指令格式如下:

install([[SCRIPT <file>] [CODE <code>]]
[COMPONENT <component>] [EXCLUDE_FROM_ALL] [...])

有时候需要在 install 的过程中打印一些语句,或者执行一些cmake指令,SCRIPT参数将在安装过程中调用给定的CMake脚本文件(即.cmake脚本文件),如果脚本文件名是相对路径,则将相对于当前源目录进行解释。CODE参数将在安装过程中调用给定的CMake代码。将代码指定为双引号字符串内的单个参数

示例如下:

install(CODE "MESSAGE("Sample install message.")")

这条命令将会在install的过程中执行cmake代码,打印语句。


原文始发于微信公众号(Linux二进制):【CMake学习笔记】| 常用基础指令总结(三)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/302063.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!