CMake 简明教程笔记,新手只看这一篇就够了

172 阅读19分钟

前言

CMake 是个一个开源跨平台自动化建构系统。CMake 并不直接建构出最终的软件,而是通过 CMakeLists.txt 等输入文件来产生特定平台的标准的构建文件(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再依这些标准的建构文件生成软件。这使得熟悉某个集成开发环境(IDE)的开发者可以用标准的方式建构他的软件。

如何阅读本篇博客

本篇博客更像是 CMake 使用手册,旨在对新手提供快速了解 CMake 的基本语法和使用指南。篇幅较长,阅读时,CMake 语法和命令部分,可走马观花,博客做好了相应的目录和跳转官方文档的索引。其它章节,都有对应的演示程序(示例代码在博客末尾附有链接),查看自己感兴趣的部分即可。

CMake 语法

注:本章节基于官方文档中 cmake-language 翻译整理而来,旨在提供快速了解 CMake 基本语法的指南,为确保信息的准确性和完整性,请直接阅读原文。

组织

CMake 输入文件(源文件)是以“CMake 语言”编写的CMakeLists.txt文件或以.cmake结尾的文件。

项目中的 CMake 语言源文件组织如下:

  • 目录 (CMakeLists.txt),
  • 脚本<script>.cmake),以及
  • 模块<module>.cmake)。

目录

CMakeLists.txt当 CMake 处理项目源代码树时,入口点是在顶级源目录中调用的源文件。此文件可能包含整个构建规范,或者使用add_subdirectory()命令将子目录添加到构建中。该命令添加的每个子目录还必须包含一个CMakeLists.txt文件作为该目录的入口点。对于每个CMakeLists.txt处理文件的源目录,CMake 都会在构建树中生成一个相应的目录,作为默认的工作和输出目录。

脚本

可以使用带 -P 选项的 cmake(1) 命令行工具以脚本模式处理单个 <script>.cmake 源文件。脚本模式仅运行给定 CMake 语言源文件中的命令,不会生成构建系统。它不允许定义构建目标或操作的 CMake 命令。

模块

目录脚本中的 CMake 语言代码可以使用 include() 命令在包含上下文的范围内加载 <module>.cmake 源文件。有关 CMake 发行版中包含的模块的文档,请参阅cmake-modules(7) 手册页。项目源树还可以提供自己的模块并在 CMAKE_MODULE_PATH  变量中指定它们的位置。

句法

编码

CMake 语言源文件可以用 7 位 ASCII 文本编写,以实现在所有受支持平台上的最大可移植性。换行符可以编码为 \n\r\n,但在读取输入文件时将转换为 \n

源文件

CMake 语言源文件由零个或多个由换行符分隔的命令调用以及可选的空格和注释组成。如下,CMake 源文件由一行行命令、空行、注释组成。

cmake_minimum_required(VERSION 3.20)

project(hello CXX)

if(FALSE AND (1+1>2))
    message("满足条件")
else()
    message("不满足条件")
endif()

# 括号参数
message([[This is the first line in a bracket ${variable} argument with bracket length 1]])
add_executable(hello hello.cpp)

命令

  • 命令调用

    命令调用是一个名称,后跟用空格分隔的括号括起来的参数

    add_executable(hello world.c)
    

    命令名称不区分大小写。参数中嵌套的未加引号的括号必须匹配。每个 () 都作为参数提供给命令调用。如if()命令来包含条件

    if(FALSE AND (1+1>2))   #(1+1>2) 执行结果作为参数
        message("满足条件")
    else()
        message("不满足条件")
    endif()
    
  • 命令参数

    命令调用中存在三种类型的参数:括号参数、引号参数或非引号参数。使用括号参数来处理多行文本或特殊字符,使用引号参数来处理包含空格或需要变量展开的内容,使用非引号参数来表示简单的参数值。

    • 括号参数

      • 写法:左括号写作 [ 后跟零个或多个 = 后跟 [。相应的右括号写为 ],后跟相同数量的 =,后跟 ]
      • 括号参数可以跨越多行。
      • 括号参数中的内容不会进行变量展开或转义处理。
      • 用于处理多行文本或包含特殊字符的参数。
      # 括号参数
      message(MAJOR_VERSION:${CMAKE_MAJOR_VERSION}) # MAJOR_VERSION:3
      message([[MAJOR_VERSION:${CMAKE_MAJOR_VERSION}]]) # MAJOR_VERSION:${CMAKE_MAJOR_VERSION}
      
    • 引号参数

      • 引号参数被包含在一对双引号("")之间。
      • 引号参数中的内容会进行变量展开和转义处理。
      • 用于包含包含空格或特殊字符的参数。
      # 引号参数
      message("MAJOR_VERSION: ${CMAKE_MAJOR_VERSION}") # MAJOR_VERSION: 3
      
    • 非引号参数

      • 不带引号的参数不包含在任何引用语法中。
      • 不能包含任何空格、()#"\,除非用反斜杠转义
      • 非引号参数内容会进行变量展开和转义处理。
      # 非引号参数
      foreach(arg
          NoSpace
          Into;Five;Arguments
          Escaped\;Semicolon;
          hh\"hh
          )
        message("${arg}")
      endforeach()
      
      NoSpace
      Into
      Five
      Arguments
      Escaped;Semicolon
      hh"hh
      

变量引用

变量引用的形式为 ${<variable>},在引用参数非引用参数中进行求值。变量引用会被指定变量或缓存项的值替换,如果两者都未设置,则会被空字符串替换。变量引用可以嵌套,并从内向外求值,例如:${outer_${inner_variable}_variable}

# 变量引用
message("CMAKE_MINOR_VERSION:${CMAKE_MINOR_VERSION}")
set(HELLO "HELLO")
set(HELLO_WROLD "Hello, World!")
message("${HELLO}")
message("${${HELLO}_WROLD}")
CMAKE_MINOR_VERSION:29
HELLO
Hello, World!

注释

注释以 # 字符开头,该字符不在括号参数引号参数内,也不作为非引号参数的一部分用 \ 转义。注释有两种类型:括号注释和行注释。

  • 行注释

    行注释以#符号开头,可以在一行的任何位置使用,用于注释掉该行代码或添加注释说明。单行注释后的内容将被忽略。

    # This is a line comment \n hh # [].
    message("First Argument\n" # This is a line comment :)
            "Second Argument") # This is a line comment.
    # set(CMAKE_CXX_STANDARD 14)
    # set(CMAKE_CXX_STANDARD_REQUIRED True)
    
  • 括号注释(多行注释)

    括号注释是将一段文本用#[[]]包裹起来,可以跨越多行,括号注释内的内容会被忽略。

    #[[This is a bracket comment.
    It runs until the close bracket.]]
    message("First Argument\n" #[[Bracket Comment]] "Second Argument")
    

控制结构

条件块

if()/elseif()/else()/endif() 命令界定了需要有条件执行的代码块。

# 条件语句
set(NUM 10)

if(NUM LESS 5)
    message("NUM 小于 5")
elseif(NUM EQUAL 5)
    message("NUM 等于 5")
else()
    message("NUM 大于 5")
endif()

循环

foreach()/endforeach()和 while()/endwhile()命令界定了要在循环中执行的代码块。在这样的代码块中 break()命令可用于提前终止循环,而continue()命令可用于立即开始下一次迭代。

  • 使用foreach()endforeach()

    # 循环语句
    set(my_list 1 2 3 4 5)
    
    foreach(item IN LISTS my_list)
        if(item STREQUAL "5")
            message("遇到了5,终止循环")
            break()
        endif()
    
        if(item STREQUAL "4")
            message("遇到了${item},跳过这次迭代")
            continue()
        endif()
    
        message("当前项为: ${item}")
    endforeach()
    
  • 使用while()endwhile()

    set(counter 0)
    
    while(counter LESS 5)
        math(EXPR counter "${counter}+1")
        if(counter EQUAL 3)
            message("遇到了3,跳过此次迭代")
            continue()
        endif()
        message("当前计数: ${counter}")
    endwhile()
    

命令定义

macro()/endmacro(), 和 function()/endfunction()命令界定了要记录的代码块,以便以后作为命令调用。

  • 使用macro()endmacro()

    macro(print_message message_text)
        message("消息内容: ${message_text}")
    endmacro()
    
    # 调用宏
    print_message("这是一个测试消息")
    
  • 使用function()endfunction()

    function(add_numbers num1 num2)
        math(EXPR result "${num1} + ${num2}")
        message("结果: ${result}")
    endfunction()
    
    # 调用函数
    add_numbers(5 3)
    

变量

变量是 CMake 语言中的基本存储单位。它们的值始终是字符串类型,尽管某些命令可能会将字符串解释为其他类型的值。 set()unset() 命令显式设置或取消设置变量,但其他命令也具有修改变量的语义。变量名称区分大小写,并且可以包含几乎任何文本,但我们建议坚持仅由字母数字字符加上 _- 组成的名称。

变量具有动态作用域。每个变量 “set” 或 “unset” 都会在当前作用域中创建一个绑定:

  • 块范围

block() 命令可以为变量绑定创建新的范围。

  • 功能范围

function() 命令创建的命令定义创建命令,这些命令在调用时会在新的变量绑定范围中处理记录的命令。变量 “set” 或 “unset” 绑定在此作用域中,并且对当前函数及其内的任何嵌套调用可见,但在函数返回后不可见。

  • 目录范围

源树中的每个目录都有自己的变量绑定。在处理目录的 CMakeLists.txt 文件之前,CMake 会复制父目录中当前定义的所有变量绑定(如果有),以初始化新的目录范围。当使用 cmake -P 处理时,CMake 脚本会将变量绑定到一个“目录”范围内。

不在函数调用内的变量 “set” 或 “unset” 绑定到当前目录范围。

  • 持久缓存

CMake 存储一组单独的“缓存”变量或“缓存条目”,其值在项目构建树中的多次运行中保持不变。缓存条目具有仅通过显式请求修改的隔离绑定范围,例如通过 set()unset() 命令的 CACHE 选项。

在评估变量引用时,CMake 首先在函数调用堆栈(如果有)中搜索绑定,然后回退到当前目录范围中的绑定(如果有)。如果找到 “set” 绑定,则使用其值。如果找到 “未设置” 绑定,或者未找到绑定,CMake 将搜索缓存条目。如果找到缓存条目,则使用其值。否则,变量引用的计算结果为空字符串。 $CACHE{VAR} 语法可用于直接查找缓存条目。

cmake-variables(7) 手册记录了 CMake 提供的许多变量,或者在由项目代码设置时对 CMake 有意义的变量。

CMake 预设了一些常用变量,这些变量通常会在编写 CMakeLists.txt 文件时使用到,这些变量可在cmake-variables(7) 手册中进行查询。

CMAKE_MAJOR_VERSION:cmake 主版本号
CMAKE_MINOR_VERSION:cmake 次版本号
CMAKE_C_FLAGS:设置 C 编译选项
CMAKE_CXX_FLAGS:设置 C++ 编译选项
PROJECT_SOURCE_DIR:工程的根目录
PROJECT_BINARY_DIR:运行 cmake 命令的目录
CMAKE_CURRENT_SOURCE_DIR:当前 CMakeLists.txt 所在路径
CMAKE_CURRENT_BINARY_DIR:目标文件编译目录
EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置
LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置
UNIX:如果为真,表示为UNIX-like的系统,包括AppleOSX和CygWin
WIN32:如果为真,表示为 Windows 系统,包括 CygWin
APPLE:如果为真,表示为 Apple 系统
CMAKE_SIZEOF_VOID_P:表示void*的大小(例如为4或者8),可以使用其来判断当前构建为32位还是64CMAKE_CURRENT_LIST_DIR:表示正在处理的CMakeLists.txt文件所在目录的绝对路径
CMAKE_ARCHIVE_OUTPUT_DIRECTORY:用于设置ARCHIVE目标的输出路径
CMAKE_LIBRARY_OUTPUT_DIRECTORY:用于设置LIBRARY目标的输出路径
CMAKE_RUNTIME_OUTPUT_DIRECTORY:用于设置RUNTIME目标的输出路径

环境变量

环境变量与普通变量类似,但有以下区别:

  • 范围

    环境变量在 CMake 进程中具有全局范围。它们永远不会被缓存。

  • 参考

    变量引用的形式为 $ENV{<variable>},使用 ENV 运算符。

  • 初始化

    CMake 环境变量的初始值是调用进程的初始值。可以使用 set()unset() 命令更改值。这些命令仅影响正在运行的 CMake 进程,而不影响整个系统环境。更改的值不会写回调用进程,并且后续构建或测试进程不会看到它们。

    请参阅 cmake -E env 命令行工具以在修改后的环境中运行命令。

  • 检查

    查看cmake -E环境命令行工具显示当前所有环境变量。

cmake-env-variables(7) 手册记录了对 CMake 有特殊含义的环境变量。

列表

在CMake中,你可以使用列表 list 来存储和操作一组数据。

# 创建一个包含字符串的列表
set(my_list "apple" "banana" "cherry")

# 遍历列表中的元素并输出
foreach(item IN LISTS my_list)
    message("Item: ${item}")
endforeach()

# 向列表添加元素
list(APPEND my_list "date")

# 输出更新后的列表
message("Updated List: ${my_list}")

# 获取列表长度
list(LENGTH my_list list_length)
message("List Length: ${list_length}")

# 获取列表指定索引处的元素
list(GET my_list 1 element)
message("Element at index 1: ${element}")

# 删除列表中的元素
list(REMOVE_ITEM my_list "banana")

# 输出更新后的列表
message("List after removing 'banana': ${my_list}")

尽管 CMake 中的所有值都存储为字符串,但在某些上下文中,例如在评估非引号参数期间,字符串可能会被视为列表,在这种情况下,通过;将字符串拆分为列表元素。

# set() 命令将多个值作为列表存储到目标变量中
set(srcs a.c b.c c.c)
message("srcs: ${srcs}")

set(x a "b;c")
message("x: ${x}")
srcs: a.c;b.c;c.c
x: a;b;c

CMake 命令

CMake 命令可在 CMake 开发文档中查看,使用搜索可快速找到自己要使用的指令。

注:本章节基于官方文档中 cmake-commands 部分常用命令整理而来,为确保信息的准确性和完整性,请直接阅读原文。

脚本命令

set

将普通变量、缓存变量或环境变量设置为给定值。

  • 普通变量
    • set(<variable> <value>... [PARENT_SCOPE])
      • <variable>: 这是要设置的变量的名称。
      • <value>...: 这是要为变量设置的值。可以设置一个或多个值。
      • [PARENT_SCOPE]: 这是一个可选的参数,用于将变量设置到父作用域中。如果指定了PARENT_SCOPE,则该变量会被设置到调用set命令的作用域的父作用域中。
  • **缓存变量:**设置一个变量,并将其缓存起来,以便在后续的CMake运行中保留该变量的值
    • set(<variable> <value>... CACHE <type> <docstring> [FORCE])
      • <variable>: 要设置的变量的名称。
      • <value>...: 变量的值。可以是一个或多个值,用空格分隔。如果只有一个值,可以直接写在<value>位置;如果有多个值,可以列出多个值。
      • CACHE <type>: 将变量缓存起来,以便在后续的CMake运行中保留该变量的值。<type>指定了变量的类型,可以是以下几种之一:
        • BOOL: 布尔类型。
        • STRING: 字符串类型。
        • FILEPATH: 文件路径类型。
        • PATH: 路径类型。
        • INTERNAL: 内部类型,不会显示在CMake GUI中,用于内部使用。
      • <docstring>: 用于描述这个变量的文档字符串,会在CMake GUI中显示。可以提供有关变量用途和说明的文本。
      • [FORCE]: 可选参数,用于强制设置变量的值,即使变量之前已经被设置过。如果使用了FORCE选项,将会覆盖之前的值。
  • **环境变量:**将环境变量设置为给定值。调用 $ENV{} 取值。
    • set(ENV{<variable>} [<value>])
      • ENV{<variable>}: 这部分用于指定要设置的环境变量的名称。在ENV{}中使用<variable>表示环境变量的名称。
      • [<value>]: 这部分是可选的,用于指定要为环境变量设置的值。如果提供了<value>,则会将环境变量设置为指定的值;如果没有提供<value>,则会将环境变量设置为空。

unset

取消设置变量、缓存变量或环境变量。

  • 取消设置普通变量或缓存变量
    • unset(<variable> [CACHE | PARENT_SCOPE])
      • <variable>: 要取消设置的变量的名称。
      • [CACHE]: 可选参数,如果指定了CACHE,则表示取消设置的是一个缓存变量。取消设置缓存变量将会清除该变量的缓存值。
      • [PARENT_SCOPE]: 可选参数,如果指定了PARENT_SCOPE,则表示在父作用域中取消设置变量。默认情况下,unset()只会在当前作用域中取消设置变量,添加PARENT_SCOPE参数可以在父作用域中取消设置变量。
  • **取消设置环境变量:**此命令仅影响当前的 CMake 进程,而不影响调用 CMake 的进程,也不会影响整个系统环境,也不会影响后续构建或测试进程的环境
    • unset(ENV{<variable>})

      <variable>删除Environment Variables。后续调用$ENV{<variable>}将返回空字符串

message

记录一条消息。

  • 一般消息
    • message([<mode>] "message text" ...)
      • <mode> 是一个可选参数,用于指定消息的类型。常见的类型包括:
        • STATUS:用于输出一般信息。
        • WARNING:用于输出警告信息。
        • AUTHOR_WARNING:用于输出作者级别的警告信息。
        • SEND_ERROR:用于输出错误信息,但继续执行。
        • FATAL_ERROR:用于输出致命错误信息,会导致CMake过程终止。
      • "message text" 是要显示的消息内容,可以是字符串或变量。

include

从文件或模块加载并运行 CMake 代码,这样可以将其他文件中定义的变量、函数、宏等引入到当前脚本中,方便代码的组织和复用。

  • include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>] [NO_POLICY_SCOPE])
    • <file|module>:要包含的文件或模块的路径。可以是相对路径或绝对路径。
    • OPTIONAL:可选参数,如果指定了 OPTIONAL,那么即使被包含的文件不存在,也不会报错。
    • RESULT_VARIABLE <var>:可选参数,如果指定了 RESULT_VARIABLE,那么变量 <var> 将被设置为已包含的完整文件名,或者如果包含失败,则设置为 NOTFOUND
    • NO_POLICY_SCOPE:可选参数,如果指定了 NO_POLICY_SCOPE,则在被包含的文件中不应用策略范围。

if

有条件地执行一组命令。

概要

if(<condition>)
  <commands>
elseif(<condition>)# optional block, can be repeated  <commands>
else()# optional block  <commands>
endif()

根据条件语法评估子句condition的参数。如果结果为真,则 执行块中的。否则,以相同方式处理可选块。最后,如果没有为真, 则执行可选块中的else

list

对以分号分隔的列表进行的操作。

概要

Reading
  list(LENGTH <list> <out-var>)
  list(GET <list> <element index> [<index> ...] <out-var>)
  list(JOIN <list> <glue> <out-var>)
  list(SUBLIST <list> <begin> <length> <out-var>)

Search
  list(FIND <list> <value> <out-var>)

Modification
  list(APPEND <list> [<element>...])
  list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>)
  list(INSERT <list> <index> [<element>...])
  list(POP_BACK <list> [<out-var>...])
  list(POP_FRONT <list> [<out-var>...])
  list(PREPEND <list> [<element>...])
  list(REMOVE_ITEM <list> <value>...)
  list(REMOVE_AT <list> <index>...)
  list(REMOVE_DUPLICATES <list>)
  list(TRANSFORM <list> <ACTION> [...])

Ordering
  list(REVERSE <list>)
  list(SORT <list> [...])

function

定义函数以作为命令调用。

函数一般定义形式如下

function(<name> [<arg1> ...])
  <commands>
endfunction()

定义一个名为 <name> 的函数,它接受名为 <arg1>, ... 的参数,调用函数执行 <commands>语句;在调用该函数之前它们不会被执行。

file

文件操作命令。该命令专用于需要访问文件系统的文件和路径操作。对于其他路径操作,仅处理语法方面,请查看 cmake_path()命令。

概要

Reading
  file(READ <filename> <out-var> [...])
  file(STRINGS <filename> <out-var> [...])
  file(<HASH> <filename> <out-var>)
  file(TIMESTAMP <filename> <out-var> [...])
  file(GET_RUNTIME_DEPENDENCIES [...])

Writing
  file({WRITE | APPEND} <filename> <content>...)
  file({TOUCH | TOUCH_NOCREATE} <file>...)
  file(GENERATE OUTPUT <output-file> [...])
  file(CONFIGURE OUTPUT <output-file> CONTENT <content> [...])

Filesystem
  file({GLOB | GLOB_RECURSE} <out-var> [...] <globbing-expr>...)
  file(MAKE_DIRECTORY <directories>...)
  file({REMOVE | REMOVE_RECURSE } <files>...)
  file(RENAME <oldname> <newname> [...])
  file(COPY_FILE <oldname> <newname> [...])
  file({COPY | INSTALL} <file>... DESTINATION <dir> [...])
  file(SIZE <filename> <out-var>)
  file(READ_SYMLINK <linkname> <out-var>)
  file(CREATE_LINK <original> <linkname> [...])
  file(CHMOD <files>... <directories>... PERMISSIONS <permissions>... [...])
  file(CHMOD_RECURSE <files>... <directories>... PERMISSIONS <permissions>... [...])

Path Conversion
  file(REAL_PATH <path> <out-var> [BASE_DIRECTORY <dir>] [EXPAND_TILDE])
  file(RELATIVE_PATH <out-var> <directory> <file>)
  file({TO_CMAKE_PATH | TO_NATIVE_PATH} <path> <out-var>)

Transfer
  file(DOWNLOAD <url> [<file>] [...])
  file(UPLOAD <file> <url> [...])

Locking
  file(LOCK <path> [...])

Archiving
  file(ARCHIVE_CREATE OUTPUT <archive> PATHS <paths>... [...])
  file(ARCHIVE_EXTRACT INPUT <archive> [...])

option

提供用户使用的布尔选项,可以用于控制编译流程,类似于 C 语言中的宏条件编译。

  • option(<variable> "<help_text>" [value])
    • <variable>:选项的名称。
    • <help_text>:对选项的描述、解释或备注。
    • [value]:选项的初始化值(除了 ON 之外,其他值都视为 OFF),OFF 为默认。

configure_file

在构建过程中执行文件转换。它将一个输入文件复制到一个输出文件,并在复制过程中替换变量和执行其他修改。

  • 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>:输入文件的路径。如果是相对路径,它相对于 CMAKE_CURRENT_SOURCE_DIR 处理。
    • <output>:输出文件或目录的路径。如果指定的是现有目录,输出文件将与输入文件同名放置在该目录中。如果路径中包含不存在的目录,它们会被创建。
    • 选项
      • NO_SOURCE_PERMISSIONS:从输入文件不传递权限到输出文件。复制的文件权限默认为标准的 644 值(-rw-r–r–)。
      • USE_SOURCE_PERMISSIONS:将输入文件的权限传递给输出文件。如果没有给出 NO_SOURCE_PERMISSIONS、USE_SOURCE_PERMISSIONS 或 FILE_PERMISSIONS 中的任何一个关键字,这已经是默认行为。
      • FILE_PERMISSIONS <permissions>...:忽略输入文件的权限,使用指定的 <permissions> 替代输出文件的权限。
      • COPYONLY:仅复制文件,不替换任何变量引用或其他内容。此选项不能与 NEWLINE_STYLE 一起使用。
      • ESCAPE_QUOTES:使用反斜杠(类似 C 语言)转义任何替换的引号。
      • @ONLY:仅限于替换形式为 @VAR@ 的变量引用。这对于配置使用 ${VAR} 语法的脚本非常有用。
      • NEWLINE_STYLE <style>:指定输出文件的换行符样式。使用 UNIX 或 LF 表示 \n 换行,使用 DOS、WIN32 或 CRLF 表示 \r\n 换行。此选项不能与 COPYONLY 一起使用。

项目命令

cmake_minimum_required

指定最低版本的 cmake。

  • cmake_minimum_required(VERSION <min>[...<policy_max>] [FATAL_ERROR])
    • <min>:指定项目所需的最低 CMake 版本。
    • [...<policy_max>]:可选部分,用于指定一些策略的最大版本。
    • [FATAL_ERROR]:可选部分,如果指定了此参数并且 CMake 版本低于指定的最低版本,则会产生致命错误并终止 CMake 运行。

project

设置项目名称。

  • project(<PROJECT-NAME> [<language-name>...])
    • <PROJECT-NAME>:指定项目的名称。
    • [<language-name>...]:可选部分,用于指定项目中使用的编程语言。这个参数可以是一个或多个语言名称,例如 C、CXX(C++)、Fortran 等。
  • project(<PROJECT-NAME> [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]] [DESCRIPTION <project-description-string>] [HOMEPAGE_URL <url-string>] [LANGUAGES <language-name>...])
    • <PROJECT-NAME>:这是你的项目的名称。这个名称将被用作许多内置变量的基础。
    • VERSION <major>[.<minor>[.<patch>[.<tweak>]]]:这是一个可选参数,用于指定项目的版本号。你可以指定主版本号,次版本号,补丁版本号,以及微调版本号。
    • DESCRIPTION <project-description-string>:这是一个可选参数,用于提供项目的描述。
    • HOMEPAGE_URL <url-string>:这是一个可选参数,用于指定项目主页的URL。
    • LANGUAGES <language-name>...:这是一个可选参数,用于指定项目中使用的编程语言。如果省略,CMake将默认启用C和C++。

add_executable

使用指定的源文件编译可执行文件。

  • add_executable(<name> <options>... <sources>...)
    • <name>:指定要创建的可执行文件的名称。
    • <options>...:可选项,用于指定与可执行文件相关的选项,例如编译标志等。
    • <sources>...:指定用于构建可执行文件的源文件列表。

示例如下

// 创建一个名为 my_app 的可执行文件,使用 main.cpp 和 helper.cpp 作为源文件来构建它
add_executable(my_app main.cpp helper.cpp)

add_subdirectory

将子目录添加到构建中。

  • add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
    • source_dir:这是你想要添加的源目录的路径。这个目录应该包含一个CMakeLists.txt文件。
    • binary_dir:这是一个可选参数,用于指定二进制文件的输出目录。如果省略,CMake将使用**source_dir**相对于当前源目录的相对路径。
    • EXCLUDE_FROM_ALL:这是一个可选参数。如果指定,那么这个子目录将不会被**all目标包含,也就是说,当你运行make all或者直接运行make**时,这个子目录不会被构建。
    • SYSTEM:这是一个可选参数。如果指定,那么这个子目录中的目标将被视为系统目标。这意味着,对于这个目录中的目标,任何与其相关的头文件目录都会被编译器视为系统头文件目录。

target_link_libraries

指定一个目标需要链接的库。

  • target_link_libraries(<target> <PRIVATE|PUBLIC|INTERFACE> <item>... [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
    • <target>:这是你想要链接库的目标。它可以是任何已经被CMake识别的目标,例如一个可执行文件,库,或者是另一个目标。
    • <PRIVATE|PUBLIC|INTERFACE> <item>...:这些参数用于指定链接的库以及链接的类型。<item>是你想要链接的库的名称。链接类型可以是PRIVATEPUBLIC,或者**INTERFACE**:
      • PRIVATE:库只会被目标自身使用,不会被链接到目标的消费者。
      • PUBLIC:库既会被目标自身使用,也会被链接到目标的消费者。
      • INTERFACE:库不会被目标自身使用,但会被链接到目标的消费者。

target_include_directories

为特定目标添加头文件搜索路径。

  • target_include_directories(<target> [SYSTEM] [AFTER|BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
    • <target>:目标的名称(使用 add_executable() 或 add_library() 创建的目标)。
    • [SYSTEM]:可选参数。如果指定,目录将被视为系统头文件目录(用于系统头文件)。
    • [AFTER|BEFORE]:可选参数。确定目录是追加还是插入到现有的头文件搜索路径中。
    • <INTERFACE|PUBLIC|PRIVATE>:指定后续参数的作用域。
      • PRIVATE 和 PUBLIC 会影响目标本身。
      • INTERFACE 影响链接到该目标的其他目标。
    • [items1...]:要添加的头文件搜索路径。

add_library

使用指定的源文件作为库添加到项目中。

  • 普通库
    • add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...)
      • <name>:库的名称,必须在整个项目中是全局唯一的。
      • <type>:可选参数,指定库的类型。
        • STATIC:静态库,用于链接到其他目标。
        • SHARED:动态库,可以在运行时被其他目标链接。
        • MODULE:插件模块,不会被其他目标链接,但可以在运行时使用类似 dlopen 的功能动态加载。
        • 如果不指定 <type>,默认为 STATIC 或 SHARED,取决于 BUILD_SHARED_LIBS 变量的值。
      • [EXCLUDE_FROM_ALL]:可选参数,如果指定,该库不会被默认构建,除非其他目标显式地依赖于它。
      • <sources>:源文件列表。
  • **对象库:**这种类型的库只编译源文件,不会将生成的目标文件打包或链接为库。其他由 add_library 或 add_executable 创建的目标可以使用表达式 $<TARGET_OBJECTS:objlib> 引用这些对象。
    • add_library(<name> OBJECT <sources>...)
      • <name>: 对象库的名称,用于在项目中标识这个对象库。
      • OBJECT: 指定这是一个对象库,而不是静态库或共享库。
      • <sources>...: 用于构建对象库的源文件列表。
  • **接口库:**与普通库不同,接口库不会编译任何源文件,也不会在磁盘上生成库文件。它主要用于定义其他目标的使用要求,而不产生实际的库文件。
    • add_library(<name> INTERFACE)
      • <name>:这是你想要创建的库的名称。这个名称将被用作许多内置变量的基础。
      • INTERFACE:这个关键字指定了库的类型为接口库。接口库不直接构建任何二进制文件,而是用于收集和传递使用需求,例如编译器标志,包含目录,链接库等。这些需求可以通过**target_link_libraries**命令传递给其他目标。

include_directories

将包含目录添加到构建中。

  • include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])
    • [AFTER|BEFORE]:这是一个可选参数,用于控制新添加的目录是应该被放在已有的包含目录之前还是之后。默认情况下,新添加的目录会被放在已有的包含目录之后。
    • [SYSTEM]:这是一个可选参数,如果指定,那么添加的包含目录将被视为系统目录。这意味着,对于这些目录中的头文件,编译器在处理警告时会更加宽容。
    • dir1 [dir2 ...]:这些是你想要添加的包含目录。你可以为一个项目指定多个包含目录。

target_compile_definitions

为指定目标文件添加编译选项。

  • target_compile_definitions(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
    • <target>:目标文件的名称,可以是可执行文件或库文件。
    • <INTERFACE|PUBLIC|PRIVATE>:用于指定编译选项的作用域。
      • INTERFACE:选项将应用于目标的接口(用于链接到目标的其他目标)。
      • PUBLIC:选项将应用于目标本身和链接到它的其他目标。
      • PRIVATE:选项仅应用于目标本身。
    • [items1...]:要添加的宏定义。

CMake 常用项目配置方式

下面使用一个简单的示例程序,来演示 CMake 配置项目的几种常见方式。示例程序的文件和目录结构如下

// 目录结构
.
├── CMakeLists.txt
├── hello.cpp
├── include
│   └── log.h
└── src
    └── log.cpp
    
// hello.cpp 文件 
#include <iostream>
#include "include/log.h"

int main()
{
    std::cout << "hello world" << std::endl;
    log("This is a log message.");
}

// log.h 文件
#include <iostream>
#include <ctime>

using namespace std;
void log(const string& message);

// log.cpp 文件
#include "../include/log.h"

void log(const string& message) {
    
    // 获取当前日期和时间
    time_t now = time(0);
    struct tm *localTime = localtime(&now);

    // 格式化日期和时间
    char buffer[80];
    strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", localTime);

    // 输出日志信息
    cout << "[" << buffer << "] " << message << endl;
}

相对路径方式

使用相对路径的方式配置项目,log.cpp、hello.cpp 引入头文件时需保证头文件路径正确。常见于一些演示 demo。

%E6%88%AA%E5%B1%8F2024-04-30_11.29.50.png

子目录中添加 cmake 脚本的方式

使用 include() 命令引入子目录中 .cmake 配置文件。项目庞大复杂是,可基于目录管理各个部分。

调整目录结构如下

// 目录结构
.
├── CMakeLists.txt
├── hello.cpp
└── log
    ├── include
    │   └── log.h
    ├── log.cmake
    └── src
        └── log.cpp

log 目录下添加 log.cmake 配置文件

// log.cmake 文件
// 参与编译的源文件设置给 log_sources 变量
set(log_sources log/src/log.cpp)

在工程根目录下的 CMakeLists.txt 文件里,引入子目录的配置文件

%E6%88%AA%E5%B1%8F2024-04-30_13.37.22.png

CMakeLists.txt 嵌套方式

子目录配置自己的 CMakeLists.txt 文件,在主工程的 CMakeLists.txt 里面引入子目录编译生成的库。项目配置最常用的方式,每个子目录单独管理自身。

目录结构如下

.
├── CMakeLists.txt
├── hello.cpp
└── log
    ├── CMakeLists.txt
    ├── include
    │   └── log.h
    └── src
        └── log.cpp

log 目录下添加 CMakeLists.txt 配置文件

# 指定源文件,创建 log_lig 库
add_library(log_lib src/log.cpp)

在工程根目录下的 CMakeLists.txt 文件里,引入 log_lig

%E6%88%AA%E5%B1%8F2024-05-07_15.26.39.png

Object Libraries 方式

使用 add_library(<name> OBJECT <sources>...) 命令。Object Library (最低版本 3.12)是一个特殊的库类型,它将目标文件编译成一个库,但不会生成最终的链接文件,这意味着你可以在后续的 add_libraryadd_executable 命令中,将 Object Library 作为源文件进行链接,从而生成最终的可执行文件或库文件。

目录结构如下

.
├── CMakeLists.txt
├── hello.cpp
└── log
    ├── CMakeLists.txt
    ├── include
    │   └── log.h
    └── src
        └── log.cpp

log 目录下添加 CMakeLists.txt 配置文件

# 对象库只编译源文件,不会将生成的目标文件打包或链接为库
add_library(log_lib OBJECT src/log.cpp)

target_include_directories(log_lib PUBLIC include)

在工程根目录下的 CMakeLists.txt 文件里,将 Object Library 作为源文件进行链接,可以看到项目编译后并没有生成 .a 静态库

%E6%88%AA%E5%B1%8F2024-05-07_15.37.49.png

CMake 静态库与动态链接库

C++ 中的静态库与动态库

在C++编程中,静态库和动态库是常用的库文件类型,用于组织和共享代码。这里是它们的简要介绍

**静态库(Static Library):**静态库是在编译时与可执行文件链接的库。它包含了一组预编译的目标文件,这些目标文件在链接时会被整合到可执行文件中。

lib<name>.a/lib

  • 特点
    • 静态库的代码会被复制到可执行文件中,使得可执行文件变得独立。
    • 静态库的大小会增加可执行文件的大小。
    • 静态库在链接时被静态地链接到可执行文件中。
  • 优点
    • 静态库的部署简单,不需要额外的库文件。
    • 可以避免动态库版本兼容性的问题。

动态库(Dynamic Library):动态库是在运行时加载到内存中的库。它包含了可执行代码,但在程序启动时并不被复制到可执行文件中。

lib<name>.so/dll 文件

  • 特点
    • 动态库在运行时被加载到内存中,并可以被多个程序共享。
    • 多个程序可以共享同一个动态库的实例。
    • 动态库的更新会影响所有使用它的程序。
  • 优点
    • 节省内存,因为多个程序可以共享同一个动态库的实例。
    • 可以通过更新动态库来修复 bug 或增加功能,而无需重新编译可执行文件。

调用库

静态库调用方式

  1. 引入头文件
  2. 链接静态库
  3. 生成可执行二进制文件

%E6%88%AA%E5%B1%8F2024-04-30_15.09.59.png

动态库调用方式

  1. 引入头文件
  2. 声明库目录
  3. 生成可执行二进制文件
  4. 链接动态库

%E6%88%AA%E5%B1%8F2024-04-30_15.13.49.png

CMake 交叉编译

本节通过一个简单的示例,演示如何在 CMake 工程中添加交叉编译的配置,实现编译不同硬件平台的可执行文件。

准备交叉编译工具

确保目标系统的交叉编译工具已经下载配置完成。本示例是在 macOS 系统下编译 windows 系统的可执行文件,所以下载 mingw-w64 编译工具。

brew install mingw-w64

查看安装目录下面的 g++ 编译器,验证安装是否成功

  ~ /opt/homebrew/Cellar/mingw-w64/11.0.1_1/toolchain-x86_64/bin/x86_64-w64-mingw32-g++ --version
x86_64-w64-mingw32-g++ (GCC) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

➜  ~ 

交叉编译配置文件

在示例代码项目的目录下,添加交叉编译的配置文件 xxx.cmake。

# tool_chain_windows.cmake 交叉编译配置文件
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86-64)

set(tools /opt/homebrew/Cellar/mingw-w64/11.0.1_1/toolchain-x86_64)
set(CMAKE_C_COMPILER ${tools}/bin/x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER ${tools}/bin/x86_64-w64-mingw32-g++)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

通过 CMake 的变量 CMAKE_TOOLCHAIN_FILE 来指定工具链文件

cmake -DCMAKE_TOOLCHAIN_FILE=tool_chain_windows.cmake

编译验证

指定工具链文件后进行编译

%E6%88%AA%E5%B1%8F2024-05-06_17.12.31.png

对编译输出的 hello.exe 文件,可在 windows 系统下运行进行验证。

与源文件交互

使用 configure_file() 命令,在构建过程中执行文件转换。

示例代码

定义一个输入文件 config.in ,在输入文件内容中引用的变量(如 @VAR@、VAR{VAR}、CACHE{VAR})将被替换为变量的当前值,如果变量未定义,则替换为空字符串

#define CMAKE_CXX_STANDARD ${CMAKE_CXX_STANDARD}

使用 configure_file() 命令,执行文件转换

cmake_minimum_required(VERSION 3.20)

project(hello CXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 构建过程中执行文件转换,输入输出文件都在当前目录下,输出 .h 文件,后续可以在 c++ 代码中 include
configure_file(config.in config.h)

add_executable(hello hello.cpp src/log.cpp)
# 添加头文件搜索路径——包含 config.h 生成文件的路径
target_include_directories(hello PUBLIC "${PROJECT_BINARY_DIR}")

在 c++ 代码中引入头文件,并使用其中宏定义的常量

#include <iostream>
#include "include/log.h"
#include "config.h"

int main()
{
    std::cout << "hello world" << std::endl;
    std::cout << CMAKE_CXX_STANDARD << std::endl;
}

CMake 项目中,可通过这种形式,把 CMake 使用的变量传递给 C++ 代码。

条件编译

在 CMake 中使用选项**option()**命令,可以控制编译流程。

CMake 输入文件中使用条件编译

使用 **option()**命令控制 CMake 执行不同的命令

# 添加选项 我们定义了一个名为 ENABLE_DEBUG 的选项,它默认为 ON。你可以根据需要修改选项的值,然后在 CMake 构建过程中使用这些选项来控制不同的功能模块
option(ENABLE_DEBUG "Enable Debug Mode" ON)
option(LOG_MSG_START_WITH_PREFIX "Log message start with ======> prefix" ON)

message("LOG_MSG_START_WITH_PREFIX defined: " ${LOG_MSG_START_WITH_PREFIX})
# 模拟使用选项来控制不同的功能模块
if (ENABLE_DEBUG)
        message("ENABLE_DEBUG defined: " ${ENABLE_DEBUG})
else ()
        message("ENABLE_DEBUG un-defined: " ${ENABLE_DEBUG})
endif()

add_library(log_lib src/log.cpp)

target_compile_definitions(log_lib PRIVATE "ENABLE_DEBUG" PRIVATE "LOG_MSG_START_WITH_PREFIX")

输出

LOG_MSG_START_WITH_PREFIX defined: ON
ENABLE_DEBUG defined: ON

C++ 代码中使用条件编译

使用 target_compile_definitions()命令,为目标库添加编译选项,在代码中使用这些选项,类似于 C 语言中的宏条件。

#include "../include/log.h"

void log(const string &message)
{
// 条件预处理指令,用于检查某个标识符是否已定义
#ifdef ENABLE_DEBUG
    // 获取当前日期和时间
    time_t now = time(0);
    struct tm *localTime = localtime(&now);

    // 格式化日期和时间
    char buffer[80];
    strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", localTime);

    // 输出日志信息
    #ifdef LOG_MSG_START_WITH_PREFIX
    cout << "======> " << "[" << buffer << "] " << message << endl;
    #else
    cout << "[" << buffer << "] " << message << endl;
    #endif
#else
   
#endif
}

参考文档&资料

原文链接

cmake.org/

CMake Tutorial CMakeTutorialCN (中文翻译版)

Modern CMake 简体中文版

CMake 开发手册中文版

CMake 简明教程(bilibili 视频,深入浅出,这篇博客可算是此视频学习笔记)

示例代码 cmake-demo.zip