00-简介

https://zhuanlan.zhihu.com/p/487650169
鉴于已经学习并使用cmake长达三年时间,cmake在世界范围也越来越受到欢迎,在这里开个坑吧。也算向自己交个作业。

什么是cmake?

CMake 是一个开源、跨平台的工具,旨在构建、测试和打包您的程序。CMake 用于使用简单的平台和编译器独立配置文件来控制程序编译过程,并生成可在您选择的编译器环境中使用的主机配置文件和项目文件。这套 CMake 工具由 Kitware 创建,以满足 ITK 和 VTK 等开源项目对强大的跨平台构建环境的需求。

官方网站:https://cmake.org/
官方Repo:https://gitlab.kitware.com/cmake/cmake

为什么使用cmake?

众所周知,现今仍然在使用的配置器有许多:configure/make,sln/vcxproj,meson,jom,nmake,qmake等等。有什么理由让你抛弃这些配置器,转而使用cmake?

那必须是很大的优势啦!
cmake具有:

  • 编程式的配置
  • 支持跨平台
  • 支持强依赖受控管理
  • 官方提供的依赖查找方式
  • 支持配置分离
  • 支持多种外部调用方式
  • 官方提供多种系统检测接口
  • 支持工具链(Toolchain)以传递配置
  • 官方提供了多种工具链实现
  • 自身具有版本控制及约束功能
  • 等等

厌倦了查看上万行的configure?厌倦了查看配置中的对应关系?受到跨平台的困扰?没问题,这些cmake都可以帮你解决。
我们完全可以使用cmake来完成CI/CD任务,编程式的编写方式让我们免去了翻配置文件的困扰。所有的构建或测试工作均可在一套cmake代码中完成。甚至你还可以在代码中实现执行任意进程并根据结果进行对应操作!

如今,使用cmake作为配置工具的开源项目已有很多,诸如*TK、opencv、llvm、zstd、curl、libpng、snappy、benchmark、jsoncpp、glfw、kf5系列、flann、pcl、libyuv、protobuf、geotiff、eigen、jasper、cgal、rapidjson、yasm、flatbuffers、catch2、libarchive、abseil、zeromq、openimageio、libssh、tesseract、tiff、proj、libevent、pcre、mysql/mariadb、embree、colmap、liblzma、szip、netcdf-c、geos、openjpeg、libjpeg-turbo、folly、glew、libxml、grpc、sdl、freetype、arrow、openblas、wxwidgets等等等等,可谓各行各业都包含了,不特别知名的我这里都不提,而且越来越多的开源项目转而使用cmake来替换旧的配置工具,例如qt。在微软,也有很多项目正在使用cmake,例如azure系列,onnx系列。当然,我维护的vcpkg也在使用cmake来搭建配置/构建/安装流程,并提供集成功能。

根据原先的配置编写一套cmake代码并不难,甚至要比原先的配置更为简单。可以说,用了都说好!你要不要试试?

cmake有什么缺点?

据我三年经验来看,cmake有以下几个缺点:

  • 文档太差。cmake的文档差是一个公认的问题,那官方文档上连一个具体实例都没有,关键点也不会明确体现出来。很多情况下查了文档,出错,再查文档,再出错,只能全篇看下来,才发现某个角落包含了一句“xx会影响这里的行为”,简直了。
  • 弱变量及未定义的变量导致非预期行为。cmake是一个弱语言,其变量没有具体的类型之分。你可以使用某个变量代表一个字符串,也可以代表一个列表。而在其他部分使用此变量作为非预期的类型会导致无穷无尽的问题。当然,这是弱语言的共通问题。而在一处使用未被定义的变量更容易发生未预期的行为。
  • 调试困难。cmake官方目前不支持断点调试功能(虽然VS Code中的cmake插件可以,很神奇),所以在写代码时只能通过打印日志方式或者添加额外配置参数来打印所执行的所有代码及行号。

哪些项目使用cmake更好?

跨平台的,中大型的,feature多的,模块独立化的,要求依赖关系严格的,均可使用cmake。

学习成本?

鉴于cmake官方文档的糟糕程度,确实增加了cmake的学习成本。
但是如果你是一个程序员,我保证你更倾向于写代码而不是填配置。

在下一章,我会提供一个最简的cmake项目,来向各位大致的讲解一下cmake的代码结构。

01-最小配置示例

https://zhuanlan.zhihu.com/p/487689436

一个最小的cmake配置包含什么?

来让我们看看下面的代码:

  1. cmake_minimum_required(VERSION 3.0)
  2. project(sample CXX)
  3. add_library(sample sample.cpp)
  4. add_executable(sample_exe sample_exe.cpp)

cmake_minimum_required

该函数规定了此工程使用的cmake最低版本。
由于cmake仍在不断发展,每个版本均会修改一些函数参数,也会添加更多函数。此函数的作用是为了防止使用版本过低的cmake来配置导致非预期错误。

project

该函数声明了此项目的名称。
由于一个项目中可能包含多个库或多个可执行程序,在子库/子可执行程序中禁止使用与该函数声明中的相同的名称。
该函数第二个参数为该项目的代码类型。可声明 CCXXC CXX。对应的代表了纯c工程,纯c++工程与混合工程。

注意:声明的代码类型影响了编译器的选取。

add_library / add_executable

该函数声明了添加一个库或添加一个可执行程序。

  • 第一个参数代表了该库/可执行程序的名称。在没有明确声明生成二进制文件名时,也代表了对应生成的二进制文件名。
  • 第二个参数代表了要生成的二进制使用的源文件。这里可以使用列表变量,也可以直接添加源文件名称。当然,也可以在后续使用函数 target_source 添加源文件。

当然,还可以添加其他关键字例如:

  • SHARED 声明该库仅被作为动态库生成
  • STATIC 声明该库仅被作为静态库生成
  • OBJECT 声明该target仅生成中间binary文件,以供其他target使用
  • INTERFACE 声明该库仅是一个接口而并没有属于自己的binary
  • ALIAS 声明该库仅是其他库的别名
  • IMPORTED 声明该库不需要构建,而是已被导入具体配置。此方式一般存在于依赖提供的配置中。

上述关键字只能在 add_library 中被声明。
在未声明前五个关键字时,库的构建类型根据 BUILD_SHARED_LIBS 变化。
注意:声明win32可执行程序时,应在add_executable中程序名称后添加关键字 WIN32 。

关键字 project

如上面所讲,每个cmake项目均应当声明此关键字,这影响了整个项目的属性。cmake也会提供项目对应的各个变量,例如:

  • PROJECT_NAME 项目名称
  • PROJECT_SOURCE_DIR 项目源码根目录
  • PROJECT_VERSION 项目版本
  • PROJECT_BINARY_DIR 项目生成的临时二进制目录,用于存放配置/编译中间文件。

    关键字 target

    target在cmake中是一个很重要的概念,你应当将它理解为一个object。它包含了例如以下内容:

  • 相关的源文件列表

  • 相关的编译选项
  • 相关的依赖库
  • 相关的头文件路径列表
  • 相关的库文件路径列表
  • 相关的其他属性

所以,你在后续可以使用这个target名称做任何事情。对应的cmake函数会使用该名称自动提取函数需使用的属性值。

关键字 PUBLC PRIVATE INTERFACE

这些关键字是用于搭建依赖关系。

  • PUBLIC 声明该关键字后续的值在构建该target时使用,并向下游提供。
  • PRIVATE 声明该关键字后续的值仅在构建该target时使用,不向下游提供。
  • INTERFACE 声明该关键字仅向下游提供,不在构建该target时使用。

以上就是使用cmake编写的工程配置最小示例,是不是很简单?
在后续文章中,我会逐渐的介绍其他cmake函数。

02-使用cmake代码生成二进制

https://zhuanlan.zhihu.com/p/487715137
前面一讲介绍了最小的cmake工程,那么我们如何使用这些代码生成库/可执行程序呢?

cmake下载与安装

你可以在cmake官方网站下载cmake:
https://cmake.org/download/
cmake可执行程序包含了所有的主流平台,并提供压缩包方式和安装包方式。
在此我个人建议诸位同学安装非RC版本,因为他们的RC的不稳定性略高。
在安装过程中,最好将cmake安装目录添加至环境变量PATH中,以便后续使用。

使用GUI生成项目

解压或安装完成后,你可以在cmake安装目录中找到一个可执行程序:cmake-gui.exe,打开它:
image.png
现在创建一个文件夹 sample , 将上一讲中的cmake代码写入新文件 CMakeLists.txt 中:

  1. cmake_minimum_required(VERSION 3.0)
  2. project(sample CXX)
  3. add_library(sample sample.cpp)
  4. add_executable(sample_exe sample_exe.cpp)

创建以下文件至sample目录中:

  1. #include <iostream>
  2. int print_hello_world()
  3. {
  4. std::cout << "hello world!" << std::endl;
  5. return 0;
  6. }
  1. #include <iostream>
  2. int main(void)
  3. {
  4. std::cout << "hello world!" << std::endl;
  5. return 0;
  6. }

在cmake gui中选择“Browse Source…”并选择sample目录。
在sample目录中创建一个新目录 build 以分离生成的配置,编译中间文件和最终二进制文件。
在cmake gui中选择“Browse Build…”并选择新创建的 build 目录。
image.png
点击cmake gui下方的“Configure”,并在弹出的窗口中选择已安装的编译器,点击“Finish”。
image.png
点击“Configure”右边的“Generate”。
此时,你可以在cmake gui中间看到一些内容:
image.png
这些均是构建该项目的配置属性,例如第一个是支持的构建类型,第二个是生成的二进制的默认安装路径。
image.png
点击“Generate”右边的“Open Project”,选择你前面选中的IDE打开:
image.png
在这里我使用了Visual Studio 2019,可以看到针对于Visual Studio的配置已经完全生成,此时只需要构建 ALL_BUILD 即可生成库 sample 和可执行文件 sample_exe。

什么?你说既然生成的还是编译器对应的配置文件,为什么不直接写这些配置文件?嗯,你肯定没有好好看第0篇教程,请重新阅读一遍再接着看下文。

使用命令行生成项目

这种方式更适合于在CI/CD或其他自动化流程中构建您的工程。在这里我讲解的详细一点。
根据文档:https://cmake.org/cmake/help/latest/manual/cmake.1.html
cmake使用安装目录下的 cmake.exe 来执行命令行,并提供了一大堆参数。
使用上面的代码文件,打开命令行,并输入以下命令:

  1. cmake.exe -S K:\\sample -B K:\\sample\\binary -G "Visual Studio 16 2019" -A x64

报错: CMake Error: Could not create named generator Visual Studio 16 2019

  • 在环境变量path里面有cygwin的目录 ```shell

    cmake -S C:\Users\ning.liu\Desktop\C_CPP\CMake\sample -B C:\Users\ning.liu\Desktop\C_CPP\CMake\sample\binary -G “Visual Studio 16 2019” -A x64 CMake Error: Could not create named generator Visual Studio 16 2019

Generators

  • Unix Makefiles = Generates standard UNIX makefiles. Ninja = Generates build.ninja files. Ninja Multi-Config = Generates build-.ninja files. CodeBlocks - Ninja = Generates CodeBlocks project files. CodeBlocks - Unix Makefiles = Generates CodeBlocks project files. CodeLite - Ninja = Generates CodeLite project files. CodeLite - Unix Makefiles = Generates CodeLite project files. Eclipse CDT4 - Ninja = Generates Eclipse CDT 4.0 project files. Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files. Kate - Ninja = Generates Kate project files. Kate - Unix Makefiles = Generates Kate project files. Sublime Text 2 - Ninja = Generates Sublime Text 2 project files. Sublime Text 2 - Unix Makefiles

    1. = Generates Sublime Text 2 project files.
    1. ```shell
    2. > cmake -S C:\Users\ning.liu\Desktop\C_CPP\CMake\sample -B C:\Users\ning.liu\Desktop\C_CPP\CMake\sample\binary -G "Visual Studio 16 2019" -A x64
    3. -- Selecting Windows SDK version 10.0.19041.0 to target Windows 10.0.19044.
    4. -- The CXX compiler identification is MSVC 19.29.30145.0
    5. -- Detecting CXX compiler ABI info
    6. -- Detecting CXX compiler ABI info - done
    7. -- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Tools/MSVC/14.29.30133/bin/Hostx64/x64/cl.exe - skipped
    8. -- Detecting CXX compile features
    9. -- Detecting CXX compile features - done
    10. -- Configuring done
    11. -- Generating done
    12. -- Build files have been written to: C:/Users/ning.liu/Desktop/C_CPP/CMake/sample/binary

    image.png

    1. > cmake --build C:\Users\ning.liu\Desktop\C_CPP\CMake\sample\binary
    2. 用于 .NET Framework Microsoft (R) 生成引擎版本 16.11.2+f32259642
    3. 版权所有(C) Microsoft Corporation。保留所有权利。
    4. Checking Build System
    5. Building Custom Rule C:/Users/ning.liu/Desktop/C_CPP/CMake/sample/CMakeLists.txt
    6. sample.cpp
    7. sample.vcxproj -> C:\Users\ning.liu\Desktop\C_CPP\CMake\sample\binary\Debug\sample.lib
    8. Building Custom Rule C:/Users/ning.liu/Desktop/C_CPP/CMake/sample/CMakeLists.txt
    9. sample_exe.cpp
    10. sample_exe.vcxproj -> C:\Users\ning.liu\Desktop\C_CPP\CMake\sample\binary\Debug\sample_exe.exe
    11. Building Custom Rule C:/Users/ning.liu/Desktop/C_CPP/CMake/sample/CMakeLists.txt

    image.png
    image.png
    对于install命令来讲,由于在cmake代码中并未声明安装任何文件,执行install命令将没有任何作用。

    命令行参数

    对于命令中的参数,在这里我列举几个重要的讲讲:

  • -S: 顶级CMakeLists.txt(包含project声明)所在路径。
  • -B: 存放临时编译的二进制文件(.obj、.ilk等)和编译器对应的配置文件路径。
  • -G: 编译器名称
  • -A: 架构名称
  • -D: 使用该变量以向cmake传入各种参数。包括选项及覆盖cmake提供的各种默认变量值。
  • —toolchain: cmake toolchain文件路径。这一点将在后续讲解。
  • —install-prefix: 安装的二进制存放路径。
  • —trace / —trace-expand: 调试时使用,用于打印已执行的cmake代码及行号。否则仅输出函数message中的内容。
  • —build: 使用cmake直接调用编译器编译项目。
  • —config: 选择需要编译的项目配置类型。
  • —install: 安装已编译好的二进制文件至 CMAKE_INSTALL_PREFIX 中。

在下一讲中,我将扩展的介绍其他常用的cmake函数。

03-依赖管理

https://zhuanlan.zhihu.com/p/487899911
在项目开发过程中,我们无可避免的需要使用各种各样的第三方依赖库,毕竟人人都不是google那样能把轮子造的又好又多不是?何况即使是google,依然需要使用多个第三方库。
那么问题来了,假设我需要使用第三方库去做一些事情,如何在配置中体现这种依赖关系?如果这个第三方库在我们开发过程中更新了自己的API,如何保证我们的程序不受第三方库的API变更影响?
而cmake可以保证这两项。

查找依赖

使用一个依赖库的第一步是先查找到这个依赖库,cmake提供了以下几种方式来查找这些依赖项:

find_package

find_package旨在使用预先设置的配置文件来查找依赖项,其函数原型为:

find_package(PACKAGE_NAME_CASE_SENSITIVE
             [version] [EXACT] [QUIET]
             [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [CONFIG|NO_MODULE]
             [NO_POLICY_SCOPE]
             [NAMES name1 [name2 ...]]
             [CONFIGS config1 [config2 ...]]
             [HINTS path1 [path2 ... ]]
             [PATHS path1 [path2 ... ]]
             [PATH_SUFFIXES suffix1 [suffix2 ...]]
             [NO_DEFAULT_PATH]
             [NO_PACKAGE_ROOT_PATH]
             [NO_CMAKE_PATH]
             [NO_CMAKE_ENVIRONMENT_PATH]
             [NO_SYSTEM_ENVIRONMENT_PATH]
             [NO_CMAKE_PACKAGE_REGISTRY]
             [NO_CMAKE_BUILDS_PATH] # Deprecated; does nothing.
             [NO_CMAKE_SYSTEM_PATH]
             [NO_CMAKE_SYSTEM_PACKAGE_REGISTRY]
             [CMAKE_FIND_ROOT_PATH_BOTH |
              ONLY_CMAKE_FIND_ROOT_PATH |
              NO_CMAKE_FIND_ROOT_PATH)

参数很多是不是?不要慌, 我来讲解一下主要几个:

  • PACKAGE_NAME_CASE_SENSITIVE: 这个名称表示了需要查找的库的名称,大小写敏感,与之对应的是调用了包含此名称的配置文件。如果使用该函数始终找不到库,请仔细查看文件名称中包含的库名称大小写。
  • version: 依赖的版本号。如果依赖的配置同时提供了版本文件,则会使用该值对比配置中的版本而确定是否可以使用。
  • EXACT: 该关键字声明了版本号必须严格对应配置中的版本号后,此依赖才可以被使用。大一点小一点均不可。
  • QUIET: 该关键字关闭了查找信息(不包含查找失败/错误信息)的输出。
  • CONFIG: 该关键字声明了需要使用 依赖项通过自己的cmake代码 使用cmake 自动生成的 配置文件,入口配置文件名称一般为 _<LOW_CASE_PACKAGE_NAME>-config.cmake__<ALL_CASE_PACKAGE_NAME>Config.cmake_

请注意一个是使用全小写的依赖名称而另一个使用包含大小写的依赖项名称。这一点很多人忽略了,导致了自己项目中始终找不到配置文件,即使查找路径正确。
额外使用 version 关键字时,cmake会调用文件 -config-version.cmakeConfigVersion.cmake 来查看版本是否匹配。
由于是通过依赖项的cmake配置自动生成的,一般情况下不会过期或出现错误。所以我推荐使用此模式。
对于查找路径来说,是我们特别容易忽略又非常重要的一点。

官方文档是: https://cmake.org/cmake/help/latest/command/find_package.html#id7

简单说下需要注意的几点:

  • _DIR : 你可以在 find_package 之前直接设置这个宏至依赖包路径下来告诉cmake需要查找的路径。对于一些特殊的场景有奇效。
  • CMAKE_PREFIX_PATH : cmake统一使用该list中的路径来查找所有依赖项。所以如果你把依赖项都放在一起,请将他们所在的根目录 APPEND 或 REPEND 到此list中。
  • CMAKE_FRAMEWORK_PATH: 此路径为MacOS专用,存放系统 framework 的依赖项位置。
  • CMAKE_APPBUNDLE_PATH: 此路径为MacOS专用,存放 app bundle的位置。

对于上述前两个变量/列表而言,cmake会在每一条的以下扩展路径中查找配置文件:

  • / 根目录
  • /cmake
  • /
  • //cmake
  • //cmake
  • //
  • ///cmake
  • ///
  • ////cmake

很晕是不是?其实通用做法很简单:将你需要提供的配置文件扔到 _root/share/<PACKAGE_NAME>__root/lib/cmake/<PACKAGE_NAME>_ 中就行了。
使用该模式时,一般情况下会提供依赖项对应的 target 名称(包含或不包含namespace)以供使用。极少数情况下也提供依赖项对应的各种宏,这主要是为了兼容预先cmake提供的MODULE模式文件使用方式(设置CMAKE_FIND_PACKAGE_PREFER_CONFIG 而无需修改任何 find_package 代码即可优先使用 CONFIG 模式)。
当未使用 REQUIRED 关键字时,我们可以使用 **<PACKAGE_NAME>_FOUND** 来判断是否找打了合适的依赖项。
该关键字不可与下一关键字同时声明。

  • MODULE: 该关键字声明了需要使用 cmake官方依赖项本身本项目 提供的 _Find<PACKAGE_NAME>.cmake_ 文件。

使用了上一条相同的查找规则。需要特别注明的是,它提供了一个额外的查找路径 CMAKE_MODULE_PATH ,你可以将自己写好的配置文件路径 PREPEND 到这个 list中来优先使用你的配置文件。
额外使用 version 关键字时,cmake会使用依赖项中某些文件(例如头文件或README)中存在的版本号对应配置文件中的宏 _VERSION 来查看版本是否匹配。
使用该模式时,一般情况下会提供依赖项对应的宏以供使用:

  • _INCLUDE_DIRS / _INCLUDE_DIR 头文件路径
  • _LIBRARIES / _LIBRARY 库名称(包含路径和配置表达式)
  • _VERSION / _VERSION_STRING 完整版本号
  • _FOUND 是否查找到该依赖

而在少数情况下也提供 target 名称以供下游使用。
注意:在没有声明上一个及这个关键字时,cmake会根据 CMAKE_FIND_PACKAGE_PREFER_CONFIG 的设置来判断优先使用 CONFIG 模式或 MODULE 模式。

  • NAMES: 将此关键字后面的名称替换PACKAGE_NAME_CASE_SENSITIVE来查找配置文件,并提供此名称对应的宏。 由于带“S”,你可以多写几个来匹配。
  • PATHS: 声明查找配置文件的根目录,相当于 CMAKE_PREFIX_PATH, 不过是额外的。
  • PATH_SUFFIXES: 声明查找根目录下的额外扩展相对路径。
  • NODEFAULT_PATH: 不使用默认路径查找,也就是说不使用上面列举的以 **CMAKE 为前缀的路径,而必须设置_DIRPATHS** 来查找。这一项在禁用cmake自带的MODULE文件时有奇效。
  • NO_SYSTEM_PATH: 不在系统路径中查找。

find_library

这是原始的cmake查找依赖方式:直接查找依赖项库文件,一般与下面的一项同时使用。
其函数原型为:

find_library (
          <LIBRARY_NAME>
          name | NAMES name1 [name2 ...] [NAMES_PER_DIR]
          [HINTS [path | ENV var]... ]
          [PATHS [path | ENV var]... ]
          [PATH_SUFFIXES suffix1 [suffix2 ...]]
          [DOC "cache documentation string"]
          [NO_CACHE]
          [REQUIRED]
          [NO_DEFAULT_PATH]
          [NO_PACKAGE_ROOT_PATH]
          [NO_CMAKE_PATH]
          [NO_CMAKE_ENVIRONMENT_PATH]
          [NO_SYSTEM_ENVIRONMENT_PATH]
          [NO_CMAKE_SYSTEM_PATH]
          [CMAKE_FIND_ROOT_PATH_BOTH |
           ONLY_CMAKE_FIND_ROOT_PATH |
           NO_CMAKE_FIND_ROOT_PATH]
         )

这与上面的 find_package 略有不同,我仅在此说明不同之处:

  • LIBRARY_NAME : 由于直接查找库文件而不是查找配置文件,此名称仅作为结果中宏的前缀使用。
  • NAMES : 此项声明了库文件的名称。值得注意的是,在UNIX-style系统中,自动添加“lib”作为库名称的前缀。
  • NAMES_PER_DIR: 一个名称遍历查找一次,再用另一个名称遍历查找一次。而不是根据路径使用多个名称遍历。

查找完成后:

  • 如果查找到,则会设置 LIBRARY_NAME 为查找到的库文件的名称(包含全路径)。
  • 如果没有查找到,则会将 LIBRARY_NAME 设置为 -NOTFOUND

所以这里和 find_package 又有不同,我们应当使用以下代码判断是否查找到:

if (PACKAGE_NAME MATCHES "-NOTFOUND")
    message(FATAL_ERROR "${PACKAGE_NAME} not found!")
endif()

find_path

这个函数一般是查找头文件或其他的 非库文件 且 非可执行程序。其函数原型为:

find_path (
          <FILE_NAME>
          name | NAMES name1 [name2 ...]
          [HINTS [path | ENV var]... ]
          [PATHS [path | ENV var]... ]
          [PATH_SUFFIXES suffix1 [suffix2 ...]]
          [DOC "cache documentation string"]
          [NO_CACHE]
          [REQUIRED]
          [NO_DEFAULT_PATH]
          [NO_PACKAGE_ROOT_PATH]
          [NO_CMAKE_PATH]
          [NO_CMAKE_ENVIRONMENT_PATH]
          [NO_SYSTEM_ENVIRONMENT_PATH]
          [NO_CMAKE_SYSTEM_PATH]
          [CMAKE_FIND_ROOT_PATH_BOTH |
           ONLY_CMAKE_FIND_ROOT_PATH |
           NO_CMAKE_FIND_ROOT_PATH]
         )

参数没什么好说的,参考上一条。
一般情况下,由于需要cmake表达式来让cmake判断使用哪个配置的库,我们通常这么写:

find_path(<PACKAGE_NAME>_INCLUDE_DIR NAMES header.h PATH_SUFFIXES include/...)

find_library(<PACKAGE_NAME>_LIBRARY_RELEASE NAMES name1 name2)
find_library(<PACKAGE_NAME>_LIBRARY_DEBUG NAMES name1d name2d)
select_library_configurations(<PACKAGE_NAME>)
// ...
target_*(target_name ${<PACKAGE_NAME>})

find_program

这个函数专门用于查找可执行程序。其函数原型为:

find_program (
          <VAR>
          name | NAMES name1 [name2 ...] [NAMES_PER_DIR]
          [HINTS [path | ENV var]... ]
          [PATHS [path | ENV var]... ]
          [PATH_SUFFIXES suffix1 [suffix2 ...]]
          [DOC "cache documentation string"]
          [NO_CACHE]
          [REQUIRED]
          [NO_DEFAULT_PATH]
          [NO_PACKAGE_ROOT_PATH]
          [NO_CMAKE_PATH]
          [NO_CMAKE_ENVIRONMENT_PATH]
          [NO_SYSTEM_ENVIRONMENT_PATH]
          [NO_CMAKE_SYSTEM_PATH]
          [CMAKE_FIND_ROOT_PATH_BOTH |
           ONLY_CMAKE_FIND_ROOT_PATH |
           NO_CMAKE_FIND_ROOT_PATH]
         )

也没什么好说的,直接使用 PROGRAM_NAME 就好了。

使用依赖

经过了上面的狂轰乱炸,我们终于可以使用依赖项了。我们可以将查找到的依赖项用于多个函数中,例如添加头文件路径,添加链接库,添加编译选项等。
对于不同的查找方式,配置文件或cmake提供了不同的使用方式:

例如 _INCLUDE_DIRS _LIBRARIES 这种方式。
对于头文件来讲,直接加到include_directories中就好了。而对于库来讲,则复杂点:
由于不能混合使用debug库及release库,cmake必须明确知道在不同配置下使用哪个库。所以宏中一般使用到了cmake表达式来处理这种情况:https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html

$<$<CONFIG:DEBUG>:library.lib> $<${NOT:$<CONFIG:DEBUG>>:libraryd.lib>

所以我们在写配置时,尽量将debug和release库均查找后使用 select_library_configurations 来生成表达式以便不同配置下使用。

target

target 就简单的多了,因为它是一个object,cmake函数可以轻松提取 target 包含的需要使用的属性来使用。
当然,target 包含非namespace与namespace两种形式,不过使用上没区别。

内部依赖

对于大型项目来说,我们可能需要声明不同的target,并将这些target建立依赖关系。所以我们通常使用以下方式:

add_dependencies

函数原型为:

add_dependencies(<target> [<target-dependency>]...)

这个很简单,向前者添加依赖项(后者),可以添加多个。在编译或某些配置时,优先处理后者。

好了,这一篇主要讲解了查找和使用依赖,下一篇我将讲解其他常用的cmake函数。

声明:在教程中可能遗漏或写错,请诸位多提意见和建议。

04-编译相关函数

https://zhuanlan.zhihu.com/p/488197187

本篇教程假设读者已拥有使用命令行编译程序的经验。

在使用源码文件生成二进制过程中,我们知道编译过程与链接过程需要向编译器及链接器传入不同的参数,而这些参数分为以下几类:

  • 编译选项(包括宏定义)
  • 头文件路径
  • 链接库文件名称
  • 链接库查找路径

而cmake作为配置器,当然也支持向 target 附加这些操作。

target_compile_options

此函数向目标添加编译选项,其原型为:

target_compile_options(<target> [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

例如:

target_compile_options(sample PUBLIC /arch=avx2 /Wall)

需要注意的是,如果要设置c / c++ 标准,不仅可以使用该函数添加 -std=STANDARD,还可以且更推荐设置以下两个cmake预设宏的值:

  • CMAKE_C_STANDARD
  • CMAKE_CXX_STANDARD

直接填标准号的数字即可,cmake会自动设置标准相关的选项。

target_compile_definitions

此函数专门向目标添加预设宏声明及定义。当然,你也可使用上面的函数完成这个操作,毕竟最后都是要变成命令行中的参数不是?不过专业的人干专业的事情,建议使用此函数添加宏定义。其原型为:

target_compile_definitions(<target>
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

例如:

target_compile_definitions(sample PRIVATE BUILD_DLL PUBLIC "-DPI=3.14159")

target_compile_features

此函数专门向目标设置c / c++版本。当然,你仍然可以选择第一个函数中的两种做法。其原型为:

target_compile_features(<target> <PRIVATE|PUBLIC|INTERFACE> <feature> [...])

例如:

target_compile_features(sample PUBLIC cxx_std_17)

target_precompile_headers

该函数向目标添加了预编译头文件。就是使用Visual Studio 创建新工程后自动创建的pch.h这种文件。其原型为:

target_precompile_headers(<target>
  <INTERFACE|PUBLIC|PRIVATE> [header1...]
  [<INTERFACE|PUBLIC|PRIVATE> [header2...] ...])

例如:

target_precompile_headers(sample PRIVATE pch.hpp)

target_include_directories

该函数声明了编译目标时查找使用头文件的路径。其原型为:

target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

例如:

target_include_directories(sample PUBLIC public/include/sample PRIVATE sample)

target_link_libraries

该函数声明了链接时需要参与的依赖库名称或target。其名称可包含完整路径。其原型为:

target_link_libraries(<target>
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

例如:

target_link_libraries(sample PUBLIC CURL::curl glib m)

注意:由于一个target中包含多个属性,一般情况下包含了头文件路径。所以使用target作为参数传入此函数时,无需调用 target_include_directories 再次声明添加头文件路径。

target_link_directories

该函数声明了链接时查找依赖库的路径。其原型为:

target_link_directories(<target> [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

例如:

target_link_directories(sample PUBLIC third_party/libs/x86/rel)

target_link_options

该函数声明了添加额外的链接选项。其原型为:

target_link_options(<target> [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

例如:

target_link_options(sample PUBLIC /shared)

target_sources

该函数声明了向target添加源文件。其原型为:

target_sources(<target>
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

例如:

target_sources(sample PRIVATE decode.cxx)

注:不要认为这个函数完全无用,可以被 add_library / add_executable 替代。在某些情况下,此函数还是非常好用的。例如:
我需要提供一个仅包含源代码的库给下游使用,而不是编译好二进制提供给下游。此时,我们需要使用 add_library(sample INTERFACE) 并使用此函数添加源码文件来链接至 sample ,并通过cmake导出此target。如此一来,下游只需要使用 target 族函数便可以不通过其他额外代码使用我提供的源码文件。

target族函数注意事项

当需要使用cmake export关键字导出声明的target并附带其中的 PUBLIC 属性时,我们必须 将PUBLIC / PRIVATE / INTERFACE 关键字向这类 target 族函数补齐。且如果一个 target 族函数声明了这三个关键字其中之一,该 target 所属的其他 target 族函数均应当声明关键字。且对于包含路径的值,我们需要声明此值的使用范围:

  • 编译/链接时使用。必须使用绝对路径。
  • 导出以向下游提供。必须使用相对路径。

所以,我们通常情况下使用以下方式:

target_include_directories(sample PRIVATE $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/header/include> PUBLIC $<INSTALL_INTERFACE:include>)

旧的对应函数族

当然,你完全可以使用以下对应的旧cmake函数:

  1. add_compile_options
  2. add_compile_definitions
  3. include_directories
  4. link_libraries
  5. link_directories
  6. add_link_options

他们由于未被声明绑定在target上,已经逐渐被弃用了。虽然现在仍然可以使用它们,但是为了严格的依赖关系,我仍不建议使用它们。
作用域:这些函数的作用域是,从声明函数开始,至配置流程结尾结束。

完整示例

下面提供一个简单而完整的示例代码:

cmake_minimum_required(VERSION 3.0)
project(sample CXX)
set(CMAKE_CXX_STANDARD 11)

add_library(sample sample.cpp)
target_include_directories(sample PRIVATE $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/header/include> PUBLIC $<INSTALL_INTERFACE:include>)
target_compile_options(sample PUBLIC /Wall)
target_compile_definitions(sample PRIVATE BUILD_DLL INTERFACE USE_DLL)
target_link_libraries(sample PRIVATE thread m)

现在我们可以使用cmake来编写一个简单项目的配置了,但仍需要一些其他的功能,比如选项。在下一篇中,我将讲解这些内容。

05-选项及变量

https://zhuanlan.zhihu.com/p/488201866
在我们的开发过程中,难免的需要添加一些选项以供下游选择。此篇内容正是介绍选项的添加,以及顺带讲解一下cmake中变量的使用方式。

option

此函数向项目添加一个选项,可以包含选项介绍及默认值。其原型为:

option(<OPTION_NAME> "<help_text>" [value])

示例:

option(ENABLE_SAMPLE_BUILD "Build the sample programs" OFF)
  • 第一个参数为选项名称。需要注意的是,此选项不仅仅可以是boolean,也可以是string或list。
  • 第二个参数为选项介绍,为string。
  • 第三个参数为选项默认值,依据选项类型设置。

CMAKE_DEPENDENT_OPTION

进阶的。我们可以根据一些选项而额外设置其他选项值。其原型为:

cmake_dependent_option(<OPTION_NAME> "<help_text>" <default_value> <depends> <force_value>)

示例:

CMAKE_DEPENDENT_OPTION(ENABLE_SAMPLE_TESTING "Enable sample test programs" ON
                       "ENABLE_SAMPLE_BUILD;NOT DISABLE_TESTING" OFF)
  • 第一个参数为选项名称。
  • 第二个参数为选项介绍。
  • 第三个参数为默认值。
  • 当第四个参数为TRUE时,开启此选项自动设置第二个参数的值为默认值。否则,将强制设置该选项默认值为第五个参数值,使用者不能修改。

在此示例中,如果 ENABLE_SAMPLE_BUILD 为 ON,且 DISABLE_TESTING 为 OFF, 则自动设置 ENABLE_SAMPLE_TESTING 为 ON,且该值可以通过cmake传入来覆盖,否则 ENABLE_SAMPLE_TESTING 会被强制设置为 OFF 且无法通过cmake传入覆盖。
值得注意的是,第四个参数中可以用分号代表与的关系,也可以仅添加一个判断选项。

set

一个语言最基础的是变量的设置及使用。cmake作为一个弱类型语言,一个变量可代表多种类型,当我们设置一个变量并赋予初始值时,一般使用该函数。其原型有三种:

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

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

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

第一种:使用这种声明时,此变量作用域为从此声明起,至包含此行代码的函数结束,若没有被函数包含,则至此文件结束,向下游自动传递此变量。

  • 第一个参数是变量名称。
  • 第二个参数是设置的初始值。

如果声明了 PARENT_SCOPE 关键字,则此变量向调用此函数或此文件的地方提供,且仅提供给上一层。

第二种:这种情况下你可以认为它相当于 option

  • 第一个参数是变量名称。
  • 第二个参数是设置的初始值。CACHE 关键字代表了此变量是缓存变量。如果此变量在设置前不存在,则会将第二个值作为初始值。如果存在且没有传入 FORCE 关键字,则使用已存在的值;如果传入 FORCE 关键字,则强制设置为第二个值。
  • 第三个参数代表了该变量类型,有 BOOL, FILEPATH, PATH, STRING, INTERNAL(代表着FORCE)。

第三种:这种情况下你可以设置一个环境变量。使用 ENV{VAR_NAME} 来设置一个名为VAR_NAME的环境变量值。
cmake中使用每个变量,或者说获取变量的值有两种方式:

  • 使用 ${} 并在大括号中间写入变量名称,例如:

    ${SAMPLE_SOURCES}
    

    大部分情况下我们通常使用此种方式将变量转化为值来使用。需要注意的是,经过此种方式转化的值,会被还原为该变量类型。如果变量是string,则最后相当于设置了一个 “abc” ; 如果变量是布尔值,则相当于 ON ;如果是路径,则相当于 c:/windows ; 如果是list,则相当于传递了多个参数。
    使用环境变量也是一样的:

    ${ENV{VAR_NAME}}
    
  • 直接使用变量名: 此种情况仅针对于cmake的各个函数,比如 foreach。一般情况下不建议各位使用。注意:如果使用以下代码,则相当于调用 unset 函数来解除此变量:

    set(VARIABLE_ONE)
    

unset

对应的,该函数是解除一个变量声明。其原型为:

unset(<variable> [CACHE | PARENT_SCOPE])

unset(ENV{<variable>})

string

该函数专门用于字符串处理,包含正则表达式匹配功能。其大体原型为:

string(operator ...)

具体请参考官方文档 https://cmake.org/cmake/help/latest/command/string.html?highlight=string

下面介绍几个经常使用的:

  • APPEND: 追加字符串

    string(APPEND STRING_1 "world")
    
  • PREPEND: 向起始位置插入字符串

    string(PREPEND STRING_1 "hello")
    
  • REPLACE: 替换字符串内容

    string(REPLACE "hello" "world" STRING_2 ${STRING_1})
    
  • LENGTH: 获取字符串长度

    string(LENGTH STRING_1 STRING_1_LEN)
    
  • SUBSTRING: 按长度截取字符串。

    string(SUBSTRING STRING_1 0 4 STRING_2)
    
  • TOUPPER / TOLOWER: 字符串转大小写:

    string(TOUPPER STRING_1 STRING_2)
    string(TOLOWER STRING_1 STRING_1)
    
  • FIND: 查找内容并获取内容的初始位置。

    string(FIND STRING_1 "hello" STRING_HELLO_INDEX)
    

    经常情况下我们使用下面的正则匹配机制。

REGEX: 此关键字声明使用正则匹配,包含几个子关键字:

  • MATCH 匹配一次
  • MATCHALL 全部匹配
  • REPLACE 匹配并替换字符串

提到正则匹配,那肯定要看表达式写法呀!官方文档为 https://cmake.org/cmake/help/latest/command/string.html?highlight=string#regex-specification
提几个经常用的:

  • ^ 表示开头
  • $表示结尾
  • .一个任意字符
  • *多次匹配
  • +至少一次匹配
  • ?匹配0次或者1次
  • ()提取其中内容
  • []匹配其中任意字符
  • |匹配两侧任意一个
  • \转义符

例子:

string(REGEX REPLACE "^(.*)world$" "HELLO" STRING_1 "${STRING_1}")


如果使用“()”,在第四个参数(替换成的字符串)中可使用 “\”的形式来直接表示其匹配到的内容,例如 \1 。从1开始计算index。
值得注意的是正则匹配也可以用于 if 函数 (使用 MATCHES)。

list

处理列表操作。其大体原型为:

list(operator <LIST_NAME> ...)

具体请参考官方文档:https://cmake.org/cmake/help/latest/command/list.html?highlight=list

列举几个常用的:

  • APPEND: 追加一项

    list(APPEND LIST_1 ${STRING_1})
    
  • PREPEND: 列表头插入一项

    list(PREPEND LIST_1 ${STRING_1})
    
  • INSTERT: 插入一项

    list(INSERT LIST_1 1 ${STRING_1})
    
  • REMOVE_ITEM: 删除指定项

    list(REMOVE_ITEM LIST_1 ${STRING_1})
    
  • REMOVE_AT: 删除指定位置项

    list(REMOVE_AT LIST_1 1 )
    
  • REMOVE_DUPLICATES: 删除重复项

    list(REMOVE_DUPLICATES LIST_1)
    
  • LENGTH: 获取列表项数

    list(LENGTH LIST_1 LIST_1_LEN)
    
  • FILTER: 使用正则表达式删除匹配到或未匹配到项, 正则表达式请参考上面。

    list(FILTER LIST_1 "" INCLUDES REGEX "^hello world$")
    

TRANSFORM: 此关键字用于将列表作为操作单元来对内部的每一条内容执行操作,所以无需遍历列表即可完成操作需求。此项可使用正则表达式。
子关键字有:

  • APPEND / PREPEND: 向列表中每一项 追加或头插 字符串

    list(TRANSFORM LIST_1 APPEND "hello " LIST_2)
    
  • TOLOWER / TOUPPER: 将列表中每一项转为全小写或全大写。

    list(TRANSFORM LIST_1 TOUPPER)
    
  • STRIP: 删除列表中每一项开头或结尾的空格。

    list(TRANSFORM LIST_1 STRIP)
    
  • REPLACE: 替换列表中每一项的部分内容。

    list(TRANSFORM LIST_1 REPLACE "hello" "HELLO")
    

这些子关键字还可以附加以下规则以确定作用范围:

  • AT 从哪条开始(到哪条结束)
  • FOR 执行范围:开始-结束。
  • REGEX 正则表达式确定范围

例如:

string(TRANSFORM LIST_1 REPLACE "hello" "HELLO" REGEX "^.*world$")

if

判断语句在程序中不可缺少,cmake中的判断语句格式为:

if (CONDITION_1)
    # do something
elseif (CONDITION_2)
    # do something
else()
    # do something
endif()

每个条件前可加 NOT 表明取反, 条件间可加 ANDOR 说明 关联关系。
条件中可使用变量、字符串、数字、列表、版本号及target。

以下详细说明:

  • 变量: 单独对变量判断一般为判空: ```cmake if (VAR_1 STREQUAL “”)

    endif()

if (DEFINED VAR_1)

endif()

if (VAR_1)

endif()


- 字符串: 使用 STREUQAL 或 MATCHES 判断:
```cmake
if (STRING_1 STREQUAL "hello world")
# ...
endif()


if (NOT "${STRING_1}" STREQUAL "HELLO WORLD" AND NOT "${STRING_1}" STREQUAL "hello world")
# ...
endif()

这两种均可,个人建议第二种,声明了两边均为字符串。

激活此关键字可以使用正则表达式:

if (STRING_1 MATCHES "^.*(world)$")
# ...
endif()

值得说明的是,在if中间,可使用 **CMAKE_MATCH_<MATCH_NUM>** 来使用匹配到的内容,例如在上面的例子中, **CMAKE_MATCH_1****world**, MATCH_NUM1开始计算。

  • 数字: 数字这个东西在cmake中很不敏感,毕竟一般情况下也不会用数字进行运算,不过cmake仍提供了相关关键字:
    • LESS
    • GREATER
    • EQUAL
    • LESS_EQUAL
    • GREATER_EQUAL

例如:

if (LIST_1_COUNT LESS 5)
# ...
endif()
  • 列表: 使用 IN_LIST 关键字判断列表中是否存在某项:

    if ("feature_name1" IN_LIST FEATURES)
    # ...
    endif()
    
  • 版本号: 版本号不能使用数字判断关键字,而应当使用自己的一套关键字:

    • VERSION_LESS
    • VERSION_GREATER
    • VERSION_EQUAL
    • VERSION_LESS_EQUAL
    • VERSION_GREATER_EQUAL
      if (CURL_VERSION VERSION_LESS 3.3.1)
      # ...
      endif()
      
  • target: 使用target作为操作单元一般情况下仅仅判断它是否存在:

    if (NOT TARGET CURL::curl)
    # ...
    endif()
    

foreach

cmake使用 foreach 作为遍历函数名称。其原型为:

foreach(<loop_var> <operator> <items>)
  <commands>
endforeach()

其中可以包含以下函数:

  • break:声明跳出遍历。
  • continue:直接遍历下一条。


示例:

foreach (ONE_ITEM IN LISTS LIST_1 LIST_2 LIST_3)
    if (...)
        break()
    endif()
    # ...
endforeach()

foreach (ONE_ITEM IN ITEMS STRING_1)
    if (...)
        continue()
    endif()
     # ...
endforeach()

foreach (ONE_ITEM RANGE 0 3)
     # ...
endforeach()

while

当然,cmake也提供while函数,不过用的甚少(场景略少)。其函数原型为:

while(<condition>)
  <commands>
endwhile()

和 foreach 差不多用法,不过你没法在里面直接使用每一个项。所以用的很少。
https://cmake.org/cmake/help/latest/command/while.html

message

最后提一下这个常用排行榜第一的函数。它的作用是在选定条件下打印一些信息,当然,也有可能包含特殊操作。
我们的调试工作一般情况下全靠它了。其原型为:

message([<mode>] "message text" ...)

其中mode有多种,我挑一些常用的:

  • FATAL_ERROR 打印错误信息并终止整个配置流程
  • WARNING 打印警告信息
  • DEPRECATION 打印弃用信息
  • STATUS 打印状态信息
  • DEBUG 打印调试信息
  • 空 或 NOTICE 打印普通信息。

例如:

message("hello world!")
message(WARNING "Warning!")
message(STATUS "Continue...")
message(DEBUG "string 1 is: ${STRING_1}")
message(FATAL_ERROR "Error!")

需要说明的是,其中可以包含获取变量值操作。

初阶的cmake知识到此完毕,在下一篇中,我会讲解一些有关系统和编译器的cmake函数。

2022-03-28编辑:添加while函数。

06-执行命令

https://zhuanlan.zhihu.com/p/488312020
如同其他配置器,cmake也提供调用自定义命令功能,以便在配置过程中进行一些自定义任务。
一般情况下我们使用以下三种:

execute_process

在配置过程中执行一条命令。其函数原型为:

execute_process(COMMAND <cmd1> [<arguments>]
                [COMMAND <cmd2> [<arguments>]]...
                [WORKING_DIRECTORY <directory>]
                [TIMEOUT <seconds>]
                [RESULT_VARIABLE <variable>]
                [RESULTS_VARIABLE <variable>]
                [OUTPUT_VARIABLE <variable>]
                [ERROR_VARIABLE <variable>]
                [INPUT_FILE <file>]
                [OUTPUT_FILE <file>]
                [ERROR_FILE <file>]
                [OUTPUT_QUIET]
                [ERROR_QUIET]
                [COMMAND_ECHO <where>]
                [OUTPUT_STRIP_TRAILING_WHITESPACE]
                [ERROR_STRIP_TRAILING_WHITESPACE]
                [ENCODING <name>]
                [ECHO_OUTPUT_VARIABLE]
                [ECHO_ERROR_VARIABLE]
                [COMMAND_ERROR_IS_FATAL <ANY|LAST>])

参数很多,不要怕,我来挑一些经常用的讲解:

  • COMMAND: 执行的命令。建议使用双引号括起来,以免出现路径包含空格问题。其中可以调用 find_program 找到的可执行程序。
  • WORKING_DIRECTORY: 执行命令的基准路径。由于有时候命令需要远程调用,所以这个是必须的,建议传入。
  • RESULT_VARIABLE / RESULTS_VARIABLE: 存储单个/多个进程的返回值,以供后续使用。
  • OUTPUT_VARIABLE: 存储输出信息,指的是print出来的内容。
  • ERROR_VARIABLE: 存储错误信息。
  • TIMEOUT: 命令执行超时时间设置,超时将被强行终止。单位:秒。

示例:

execute_process(COMMAND "ls -ll"
                WORKING_DIRECTORY "/root"
                OUTPUT_VARIABLE LL_CONTENT_STRING
)

add_custom_command

对于复杂场景来讲,我们一般使用这个而不是上一条。
此函数主要用于以下场景:

  • 生成文件。
  • 在配置后编译前执行一条命令(PRE_BUILD)。
  • 在编译后链接前执行一条命令(PRE_LINK)。
  • 在链接后执行一条命令(POST_BUILD)。

函数原型为:

# 生成文件
add_custom_command(OUTPUT output1 [output2 ...]
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [MAIN_DEPENDENCY depend]
                   [DEPENDS [depends...]]
                   [BYPRODUCTS [files...]]
                   [IMPLICIT_DEPENDS <lang1> depend1
                                    [<lang2> depend2] ...]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment]
                   [DEPFILE depfile]
                   [JOB_POOL job_pool]
                   [VERBATIM] [APPEND] [USES_TERMINAL]
                   [COMMAND_EXPAND_LISTS])
# 编译链接前后执行命令
add_custom_command(TARGET <target>
                   PRE_BUILD | PRE_LINK | POST_BUILD
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [BYPRODUCTS [files...]]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment]
                   [VERBATIM] [USES_TERMINAL]
                   [COMMAND_EXPAND_LISTS])

官方文档为: https://cmake.org/cmake/help/latest/command/add_custom_command.html

这个函数就略微复杂了一点。简单解释下:

  1. 第一种情况下,预期生成的文件名为 output1,也就是说COMMAND 最终会生成名为output1的文件。如果声明了DEPENDS,则会在该依赖被更改时始终运行此命令。如果没有声明 DEPENDS,则在output1不存在时始终执行此命令。所以我们需要传入一个 OUTPUT 来在不传入 DEPENDS 时判断是否执行命令。
  2. 第二种情况下,该命令 始终 该target 的 PRE_BUILD / PRE_LINK / POST_BUILD 时被执行。因为cmake有缓存机制,如果重新配置时存在该target的缓存而该target未被更改(也就是target的缓存不需要更新),则不会执行此命令。

示例(使用opencv中的代码):

add_custom_command(
      OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${OCL_NAME}.cpp"  # don't add .hpp file here to optimize build process
      COMMAND ${CMAKE_COMMAND} "-DMODULE_NAME=${name}" "-DCL_DIR=${CMAKE_CURRENT_LIST_DIR}/src/opencl" "-DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/${OCL_NAME}.cpp" -P "${OpenCV_SOURCE_DIR}/cmake/cl2cpp.cmake"
      DEPENDS ${cl_kernels} "${OpenCV_SOURCE_DIR}/cmake/cl2cpp.cmake"
      COMMENT "Processing OpenCL kernels (${name})"
    )

add_custom_command(TARGET ${the_module}
        POST_BUILD
        COMMAND copy /y "\"$(VCInstallDir)redist\\$(PlatformTarget)\\Microsoft.VC$(PlatformToolsetVersion).CRT\\msvcp$(PlatformToolsetVersion).dll\"" "\"${CMAKE_BINARY_DIR}\\bin\\$(Configuration)\\msvcp$(PlatformToolsetVersion)_app.dll\""
        COMMAND copy /y "\"$(VCInstallDir)redist\\$(PlatformTarget)\\Microsoft.VC$(PlatformToolsetVersion).CRT\\msvcr$(PlatformToolsetVersion).dll\"" "\"${CMAKE_BINARY_DIR}\\bin\\$(Configuration)\\msvcr$(PlatformToolsetVersion)_app.dll\""
        COMMAND copy /y "\"$(VCInstallDir)redist\\$(PlatformTarget)\\Microsoft.VC$(PlatformToolsetVersion).CRT\\vccorlib$(PlatformToolsetVersion).dll\"" "\"${CMAKE_BINARY_DIR}\\bin\\$(Configuration)\\vccorlib$(PlatformToolsetVersion)_app.dll\""
      )

需要注意的是,依赖关系一定要建立起来,以防止cmake在并行配置时出现顺序问题。

add_custom_target

这个函数的意义在于创建一个空的target,并将命令绑定到这个target上,以便于其他target与此target建立依赖关系;或将这个新的target与 add_custom_command 绑定来建立依赖关系。
其原型为:

add_custom_target(Name [ALL] [command1 [args1...]]
                  [COMMAND command2 [args2...] ...]
                  [DEPENDS depend depend depend ... ]
                  [BYPRODUCTS [files...]]
                  [WORKING_DIRECTORY dir]
                  [COMMENT comment]
                  [JOB_POOL job_pool]
                  [VERBATIM] [USES_TERMINAL]
                  [COMMAND_EXPAND_LISTS]
                  [SOURCES src1 [src2...]])

说明:如果添加关键字 ALL ,则此命令在配置时始终会被执行(一般配合 add_custom_command 来使用)。否则你应当声明 DEPENDS,并在这个依赖被更改之后始终被执行(可以是其他 target,也可以是文件)。

示例代码:

add_custom_target(always_run_this ALL)
add_custom_command(OUTPUT ...)

add_custom_target(configure ALL ...)
add_custom_target(build DEPENDS configure ...)
add_custom_target(install DEPENDS build ...)

下一篇教程中,我会介绍文件相关函数并且着重介绍target属性的操作。

07-文件,数学计算及属性

https://zhuanlan.zhihu.com/p/488312219
在本章内容中我将讲解文件操作、数学计算及target属性设置及获取。

file

文件操作是配置器必不可少的部分。例如:安装文件,删除缓存等。
在cmake中,我们统一使用file函数来进行这些操作。原型为:

file(operator FILE_PATH ...)

操作符有:

  • READ 读取文件内容:

    file(READ "${FILE_FULL_PATH}" FILE_CONTENT)
    
  • STRINGS 进阶读取文件内容,可采用限定长度,查找符合内容等:

    file(STRINGS "${FILE_FULL_PATH}" FILE_CONTENT LENGTH_MAXIMUM 100) # 读取最长100个字符的行
    file(STRINGS "${FILE_FULL_PATH}" FILE_CONTENT LENGTH_MINIMUM 10) # 读取最短10个字符的行
    file(STRINGS "${FILE_FULL_PATH}" FILE_CONTENT LIMIT_COUNT 5) # 读取5个不同字符串
    file(STRINGS "${FILE_FULL_PATH}" FILE_CONTENT LIMIT_INPUT 3) # 最小读取3字节
    file(STRINGS "${FILE_FULL_PATH}" FILE_CONTENT LIMIT_OUTPUT 200) # 最大读取200字节
    file(STRINGS "${FILE_FULL_PATH}" FILE_CONTENT REGEX "^(h.+d)$") # 正则匹配内容
    file(STRINGS "${FILE_FULL_PATH}" FILE_CONTENT REGEX ENCODING UTF-8) # 选择编码格式
    
  • WRITE / APPEND 写入/追加内容:

    file(WRITE "${FILE_FULL_PATH}" "hello world")
    file(APPEND "${FILE_FULL_PATH}" STRING_CONTENT)
    
  • COPY / INSTALL 复制/安装一个文件:

    file(COPY "${FILE_FULL_PATH}" DESTINATION ${CMAKE_INSTALL_PREFIX}/include)
    file(INSTALL "${FILE_FULL_PATH}" DESTINATION ${CMAKE_INSTALL_PREFIX}/include)
    

    两个关键字的不同之处在于前者不打印操作信息,后者打印。
    在安装时,建议使用 install 函数而不是用这个。

  • REMOVE / REMOVE_RECURSE 删除/递归删除文件:

    file(REMOVE "${FILE_FULL_PATH}" FILE_PATH_LIST)
    file(REMOVE_RECURSE "${ANOTHER_PATH}")
    
  • RENAME 重命名一个文件:

    file(RENAME "${OLD_FILE_FULL_PATH}" "${NEW_FILE_FULL_PATH}")
    
  • GLOB / GLOB_RECURSE 查询/递归查询一个路径中的文件,可使用匹配机制:

    file(GLOB LICENSE_PATH "${FILE_FULL_PATH}/LICENSE")
    file(GLOB_RECURSE SAMPLE_SOURCES "${CMAKE_SOURCE_DIR}/sources/*.cxx")
    
  • MAKE_DIRECTORY 创建一个文件夹:

    file(MAKE_DIRECTORY "${PATH_WILL_BE_CREATED}")
    
  • TOUCH / TOUCH_NOCREATE 无条件/文件存在时更新时间戳:

    file(TOUCH "${FILE_FULL_PATH}")
    file(TOUCH_NOCREATE "${FILE_FULL_PATH_MAYBE_NOT_EXSIT}")
    
  • CHMOD 更改一个文件的访问权限:

    file(CHMOD "${FILE_FULL_PATH}" PERMISSIONS OWNER_READ)
    file(CHMOD "${FILE_FULL_PATH}" PERMISSIONS OWNER_WRITE)
    file(CHMOD "${FILE_FULL_PATH}" FILE_PERMISSIONS OWNER_EXECUTE)
    file(CHMOD "${FILE_FULL_PATH}" PERMISSIONS GROUP_READ)
    file(CHMOD "${FILE_FULL_PATH}" PERMISSIONS WORLD_READ  DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
    
  • SIZE 计算文件大小,可以用来判空:

    file(SIZE "${FILE_FULL_PATH}" THIS_FILE_SIZE)
    
  • HASH 获取hash值以便判断是否更改或者完整性:

    file(HASH "${FILE_FULL_PATH}" THIS_FILE_HASH)
    
  • DOWNLOAD / UPOLAD 下载/上传文件

    file(DOWNLOAD ${DOWNLOAD_URL} "${FILE_FULL_PATH}")
    file(UPLOAD "${FILE_FULL_PATH}" ${UPLOAD_URL})
    
  • CREATE_LINK 创建链接:

    file(CREATE_LINK "${FILE_FULL_PATH}" "${LINK_FULL_PATH}")
    
  • RELATIVE_PATH 计算相对路径:

    file(RELATIVE_PATH THIS_RELATIVE_PATH "${THIS_PATH1}" "${THIS_PATH2}")
    
  • TO_CMAKE_PATH / TO_NATIVE_PATH 转为cmake格式 / 系统格式的路径

    file(TO_CMAKE_PATH "${THIS_PATH1}" CONVERTED_CMAKE_PATH1)
    file(TO_NATIVE_PATH "${THIS_PATH1}" CONVERTED_SYS_PATH1)
    

    这个很有用,因为cmake格式的路径不一定与系统格式路径一致,且各系统的路径格式也不一定一致。

number

有时候我们也需要数学操作,比如加一或者减一。
cmake在这部分做的很差,不过好歹提供了接口:

math(EXPR SUB_TWO_NUN "1 + 2")

property

前面我说过,target在cmake中是一个非常重要的概念,并相当于一个object。所以每个target均会包含不同的属性。一般情况下,我们用到的有:

  • INTERFACE_INCLUDE_DIRECTORIES 对应 target_include_directories
  • INTERFACE_COMPILE_DEFINITIONS 对应 target_compile_definitions
  • INTERFACE_COMPILE_OPTIONS 对应 target_compile_options
  • INTERFACE_LINK_LIBRARIES 对应 target_link_libraries
  • INTERFACE_LINK_DIRECTORIES 对应 target_link_directories
  • INTERFACE_LINK_OPTIONS 对应 target_link_options
  • INTERFACE_SOURCES 对应 target_sources

完整的属性文档:https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#target-properties
我们可以在使用已有配置找到一个依赖后,使用配置提供的target来获取这些值。
在这里我介绍一下如何设置或获取这些属性来进行操作。

set_target_properties

设置一个或多个target属性。原型为:

set_target_properties(target1 target2 ...
                      PROPERTIES prop1 value1
                      prop2 value2 ...)

例如:

set_target_properties(sample1 sample::sample2
                      PROPERTIES 
                      INTERFACE_SOURCES "${SOURCE_FILE_PATH}")

get_target_property

获取一个target的一个属性。原型为:

get_target_property(<VAR> target property)

例如:

get_target_property(SAMPLE_SOURCES sample::sample2 INTERFACE_SOURCES)

下一章,我会介绍其他的我们经常使用的cmake函数。
2022-03-30 编辑: 添加 RELATIVE_PATH,TO_CMAKE_PATH 及 TO_NATIVE_PATH

08-其他实用函数

https://zhuanlan.zhihu.com/p/488312475
本章内容中将包含一些我们经常使用的cmake函数介绍。

function

cmake支持模块化,我们无需重复的编写同样操作的代码,而可以使用function来完成这些工作。
其原型为:

function(function_name <arg1_name arg2_name ...>)
   ...
endfunction()

这里需要注意,我们可以使用三种方式来在函数内部调用参数

  • 使用原始方式: ARGV ,例如: ```cmake function(dosomething) if (ARGV0)
      string(APPEND ARGS_STRING "${ARGV0}")
    

    endfunction()

dosomething(“1” “2” “3”)


- 根据已声明的参数名称来调用:
```cmake
function(dosomething FIRST_ARG SECOND_ARG)
    if (FIRST_ARG)
        string(APPEND ARGS_STRING "${FIRST_ARG}")
# ...
endfunction()

dosomething(FISRST_ARG "1" SECOND_ARG "2")
  • 使用cmake函数 cmake_parse_argument 来处理这些参数(强烈推荐): ```cmake function(dosomething) cmake_parse_argument(dsth “SWITCH_ARG” “ONE_VALUE_ARG” “MULTI_VALUE_ARG1;MULTI_VALUE_ARG2”) # ds:dosomething简写 if (dsth_UNPARSED_ARGUMENTS)
      message(WARNING "warning! unparsed arguments found: ${dsth_UNPARSED_ARGUMENTS}")
    
    endif() if (dsth_SWITCH_ARG)
      file(READ "${dsth_ONE_VALUE_ARG}" ...)
      list(APPEND CURR_LIST_1 dsth_MULTI_VALUE_ARG1)
    

    endfunction()

dosomething( SWITCH_ARG ONE_VALUE_ARG “FULL_FILE_PATH” MULTI_VALUE_ARG1 “FILE_LIST1”

# ...

)

该函数的原型为:
```cmake
cmake_parse_arguments(<prefix> <options> <one_value_keywords>
                      <multi_value_keywords> <args>...)

cmake_parse_arguments(PARSE_ARGV <N> <prefix> <options>
                      <one_value_keywords> <multi_value_keywords>)

使用此种方式时,参数变量总以 prefix_ 为开头,当然你可以设置为空,但是为了防止变量冲突,建议设置。后续三个参数分别为:布尔类型(ON / OFF),只能设置一个值,可以设置多个值。如果某种参数不需要,直接设置为空(**""**)

如果我们需要在中途直接退出函数,可以使用:

return() # 声明从函数返回

cmake的函数是不完整的,因为它完全不支持返回值,并且入参完全是不带引用的!
所以我们只能变相的来实现返回值这个操作:

function(dosomething1)
# ...
set(OUT_VAR_1 ${OUT_VAR_1} PARENT_SCOPE)
endfunction()

dosomething1(...)
string(REGEX OUT_VAR_1 ...)


function(dosomething2 OUT_VARIABLE)
# ...
set(${OUT_VARIABLE} "${OUT_PATH}" PARENT_SCOPE)
endfunction()

dosomething2(OUT_VARIABLE OUT_VALUE1)
string(REGEX OUT_VALUE1 ...)

macro

不同于function,使用macro时内部的变量作用域并不局限于macro内部,而是根据调用位置更改。
个人建议仅在完成一些简单操作时再使用此函数。其原型为:

macro(macro_name <arg1_name arg2_name ...>)
endmacro()

需要注意的是,宏名称不分大小写,也就是说你可以以任何大小写来调用这个宏:

macro(do_something)
   if (SAMPLE_LIST)
       list(APPEND SAMPLE_LIST ...)
# ...
endmacro()

# ...
list(APPEND SAMPLE_LIST ...)
# ...
do_something()
Do_Something()
DO_SOMETHING()

参数方面支持函数部分的前两种,返回值就不要想啦,肯定不支持。

configure_file

依据当前被定义的变量来替换文件内容并生成新的文件。
该函数经常用在生成配置头文件或配置cmake文件时。其原型为:

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

示例:
我现在有一个文件,名为: sample_header.h.in (通常我们的模板后缀都为.in):

#cmakedefine FOO_ENABLE
#cmakedefine FOO_DISABLE
#cmakedefine FOO_STRING "@FOO_STRING@"

通过以下cmake代码:

set(FOO_ENABLE ON)
set(FOO_STRING "123")
configure_file("${CMAKE_SOURCE_DIR}/sample_header.h.in" "${CMAKE_BINARY_DIR}/sample_header.h" @ONLY)

变为:

#define FOO_ENABLE
/* #undef FOO_DISABLE */
#define FOO_STRING 123

注意,如果你没有define一个值,cmake将自动屏蔽这一行。

当然你可以不用cmakedefine,而是直接用两个@来包含cmake中存在的变量,并通过该函数将它转换:

if (@ENABLE_CURL@)
  find_dependency(CURL CONFIG REQUIRED)
#...
endif()

cmake代码:

option(ENABLE_CURL "..." OFF)
# ...
configure_file("${CMAKE_SOURCE_DIR}/sample-config.cmake.in" "${CMAKE_BINARY_DIR}/lib/cmake/sample/sample-config.cmake" @ONLY)

生成的 sample-config.cmake:

if (OFF)
    find_dependency(CURL CONFIG REQUIRED)
    # ...
endif()

需要注意的是,只有在声明了“@ONLY”时才会转义,使用“COPYONLY”时相当于调用 file(COPY)

install

cmake与其他配置器一样,支持安装(导出)各种文件。其统一原型为:

install(operator ... <DESTINATION> ...)

这个函数支持传入文件(包含路径),路径或 target 。

  • 文件

    install(FILES "${FILE_FULL_PATH}" FILE_LIST1 DESTIONATION share/sample)
    
  • 路径

    install(DIRECTORY "${FULL_PATH1}" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/sample")
    
  • TARGET

    install(TARGET sample
      RUNTIME bin # 可执行程序
      LIBRARY lib # dynamic library 或 static library
      ARCHIVE lib # 与dynamic library匹配的只拥有符号的static library
      PUBLIC_HEADER include # 公共头文件
      PRIVATE_HEADER include # 私有头文件
    )
    

    target之后的这几项可以选填,当然就按照cmake默认的规矩啦。默认值就是示例中的值。
    详情参考官方文档:https://cmake.org/cmake/help/latest/command/install.html
    注意:如果 DESTINATION 后的参数不包含绝对路径,cmake将以 CMAKE_INSTALL_PREFIX 为基准路径来拼接后安装。

add_subdirectory

cmake支持分离配置,以免中大型工程中所有的cmake代码都写一起。
一般情况下,一个中大型的项目中cmake代码结构为:

root
|
CMakeLists.txt # 入口文件
|---cmake
|    |
|    |---function1.cmake
|    |---function2.cmake
... ...
|---source1
|    |---CMakeLists.txt
|
|---source2
|    |---CMakeLists.txt
...

我们可以通过这个函数来将其他目录中的 CMakeLists.txt 加入构建流程中:

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

一般情况下我们只传第一个参数:与调用此函数的文件的相对路径。例如:

add_subdirectory(source1)
add_subdirectory(source2)

include

我们一般将完整的功能性函数抽离到后缀为 .cmake 的文件中。并使用该函数调用这些文件以使用其中包含的函数或宏。其原型为:

include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>]
                      [NO_POLICY_SCOPE])

例如:

include(funtion1)
include(function2)

注意:cmake文件作为函数传入时,不包含文件后缀 (.cmake)。

get_filename_component

如果我们需要获取包含完整路径的其中一部分,例如文件名称(不包含)后缀,或者其所在的文件夹的绝对路径。使用正则匹配显然很麻烦。幸运的是,cmake提供了此类功能的函数。其原型为:

get_filename_component(<VAR> <FileName> <COMP> [CACHE])

COMP可选择以下关键字:

  • DIRECTORY 所在文件夹的绝对路径
  • NAME 文件名(包含后缀)
  • EXT 文件后缀
  • NAME_WE 文件名(不包含后缀)
  • ABSOLUTE 绝对路径,一般用于使用相对路径生成绝对路径
  • REALPATH 链接指向的绝对路径

例如:

get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH)

enable_testing

作为一个健壮的项目,当然需要各种各样的测试。cmake在此提供了单元测试功能,不过比较弱。
此函数没有任何参数,根据cmake默认选项 BUILD_TESTING 启用使用CTest的测试单元:

enable_testing()

并使用以下函数添加测试用例:

add_test(NAME <name> COMMAND <command> [<arg>...]
         [CONFIGURATIONS <config>...]
         [WORKING_DIRECTORY <dir>]
         [COMMAND_EXPAND_LISTS])

参考文档: https://cmake.org/cmake/help/latest/command/add_test.html

ExternalProject_Add

在一些大型工程中,经常使用各种各样的第三方库。在构建此大型工程时只能使用这些库提供的导出文件,或者使用命令下载这些库的源码并使用命令配置。我们一般抽象出一个函数来实现这些功能。但是cmake如今支持一键来完成它们。

此函数非常强大,也就非常复杂,其原型大致为:

ExternalProject_Add(<name>
    # 文件夹类
    <PREFIX ...>
    <TMP_DIR ...>
    <STAMP_DIR ...>
    <LOG_DIR ...>
    <DOWNLOAD_DIR ...>
    <SOURCE_DIR ...>
    <BINARY_DIR ...>
    <INSTALL_DIR ...>
    # 下载及验证类
    <DOWNLOAD_COMMAND ...>
    <URL* ...>
    <DOWNLOAD_* ...>
    <INACTIVITY_TIMEOUT ...>
    <HTTP_* ...>
    <TLS_* ...>
    <NETRC* ...>
    <GIT_* ...>
    <SVN_* ...>
    # 更新类
    <UPDATE_* ...>
    # 配置类
    <CONFIGURE_COMMAND ...>
    <CMAKE_* ...>
    # 编译链接类
    <BUILD_* ...>
    # 安装类
    <INSTALL_* ...>
    # 操作日志类
    <LOG_* ...>
    # 设置依赖关系
    <DEPENDS ...>
    ...

我们可以使用此函数来一次性完成下载、校验、配置、生成及安装工作。
此函数已经有越来越多的大型开源项目使用了。

https://cmake.org/cmake/help/latest/module/ExternalProject.html

好啦,本篇内容完毕。在下篇教程中,我会介绍一些跟系统/编译器配置相关的cmake函数,和cmake提供的一些默认变量。
2022-03-30 编辑: 添加函数 get_filename_component

09-系统相关函数及cmake默认变量

https://zhuanlan.zhihu.com/p/488312337
如同autotools,cmake也提供了针对于系统或编译器的检测函数。这些函数用于根据系统或编译器属性来选择启用或禁用某些功能。
这些函数应当首先include其所在的cmake文件才能被使用。照例,在此我列举几个常用的。

check_compiler_flag / check_c_compiler_flag / check_cxx_compiler_flag

检查 C或C++ / C / C++ 编译器支持的flag。原型为:

check_compiler_flag(<lang> <flag> <var>) # needs "include(CheckCompilerFlag)"
check_c_compiler_flag(<flag> <var>) # needs "include(CheckCCompilerFlag)"
check_cxx_compiler_flag(<flag> <var>) # needs "include(CheckCXXCompilerFlag)"

通用函数中 可选择 c 或 cxx 。

include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-std=c++11" COMPILER_SUPPORTS_CXX11)

check_source_compiles / check_c_source_compiles / check_cxx_source_compiles

尝试仅编译一段代码来生成可执行程序,从而检测编译器是否支持代码中的某些语言特性。原型为:

check_source_compiles(<lang> <code> <resultVar>
                      [FAIL_REGEX <regex1> [<regex2>...]]
                      [SRC_EXT <extension>]) # needs "include(CheckSourceCompiles)"
check_c_source_compiles(<code> <resultVar>
                        [FAIL_REGEX <regex1> [<regex2>...]]) # needs "include(CheckCSourceCompiles )"
check_cxx_source_compiles(<code> <resultVar>
                          [FAIL_REGEX <regex1> [<regex2>...]]) # needs "include(CheckCXXSourceCompiles)"

参数请参考官方文档:
https://cmake.org/cmake/help/latest/module/CheckSourceCompiles.html
https://cmake.org/cmake/help/latest/module/CheckCSourceCompiles.html
https://cmake.org/cmake/help/latest/module/CheckCXXSourceCompiles.html

include(CheckCXXSourceCompiles)
check_cxx_source_compiles(
"#include <nmmintrin.h>
int main() {
    _mm_crc32_u64(0, 0);
    return 0;
}"
HAS_mm_crc32_u64)

if(HAS_mm_crc32_u64)
    # ...
endif()

此函数已经可以完全替代cmake的另一个函数 try_compile 。推荐使用此函数实现某些功能。
需要注意的是,此函数仅进行编译链接,并不会执行此可执行程序。如果需要执行来返回结果,请使用下面的函数。

check_source_runs / check_c_source_runs / check_cxx_source_runs

尝试编译一段代码并执行生成的可执行程序,从而返回使用的编译器的某些属性。原型为:

check_source_runs(<lang> <code> <resultVar>
                  [SRC_EXT <extension>]) # needs "include(CheckSourceRuns)"
check_c_source_runs(<code> <resultVar>) # needs "include(CheckCSourceRuns)"
check_cxx_source_runs(<code> <resultVar>) # needs "include(CheckCXXSourceRuns)"

参数请参考官方文档:
https://cmake.org/cmake/help/latest/module/CheckSourceRuns.html
https://cmake.org/cmake/help/latest/module/CheckCSourceRuns.html
https://cmake.org/cmake/help/latest/module/CheckCXXSourceRuns.html
这个函数很有用,例如可以查询当前选择的架构(指针是32位还是64位)或者查询系统api是否存在。
使用zeromq的示例:

include(CheckSourceRuns)
check_c_source_runs(
    "
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc, char *argv [])
{
    int s = socket(PF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    return(s == -1);
}
"
    ZMQ_HAVE_SOCK_CLOEXEC)

check_symbol_exists / check_cxx_symbol_exists

尝试编译一段 C / C++ 代码来确认库中的符号是否存在。此函数一般用于检测系统默认库。其原型为:

check_symbol_exists(<symbol> <files> <variable>) # needs "include(CheckSymbolExists)"
check_cxx_symbol_exists(<symbol> <files> <variable>) # needs "include(CheckCXXSymbolExists)"

示例:

check_symbol_exists(<symbol> <files> <variable>) # needs "include(CheckSymbolExists)"
check_cxx_symbol_exists(<symbol> <files> <variable>) # needs "include(CheckCXXSymbolExists)"

check_function_exists

尝试编译一段 纯C 代码来确认C函数是否存在。其原型为:

check_function_exists(<function> <variable>) # needs "include(CheckFunctionExists)"

示例:

include(CheckFunctionExists)
check_function_exists(malloc HAVE_MALLOC)

检测系统配置时可与上面一个函数配合使用。

check_include_files / check_include_file / check_include_file_cxx

尝试编译一段 C或C++ / C / C++代码来确认头文件是否存在。其原型为:

check_include_files("<includes>" <variable> [LANGUAGE <language>]) # needs "include(CheckIncludeFiles)"
check_include_file(<include> <variable> [<flags>]) # needs "include(CheckIncludeFile)"
check_include_file_cxx(<include> <variable> [<flags>]) # needs "include(CheckIncludeFileCXX)"

示例:

include(CheckIncludeFile)
check_include_file(errno.h HAVE_ERRNO_H)
check_include_file(fcntl.h HAVE_FCNTL_H)

check_library_exists

尝试编译一段代码以确认库中是否包含某个符号。其原型为:

check_library_exists(LIBRARY FUNCTION LOCATION VARIABLE) # needs "include(CheckLibraryExists)"

示例为:

include(CheckLibraryExists)
check_library_exists(m floor "" HAVE_LIBM)

check_linker_flag

尝试编译一段代码以确认当前编译器是否支持链接属性。其原型为:

check_linker_flag(<lang> <flag> <var>) # needs "include(CheckLinkerFlag)"

示例为:

include(CheckLinkerFlag)
check_linker_flag("-stdlib=libc++" CXX_LINKER_SUPPORTS_STDLIB)

check_function_exists

尝试编译一段代码以确认是否存在某个变量。其原型为:

check_function_exists(VAR VARIABLE) # needs "include(CheckVariableExists)"

示例为:

include(CheckVariableExists)
check_function_exists(ftime HAVE_FTIME)

check_type_size

检测一个类型的长度。也可以用此函数替代 check_source_runs 等来查询当前架构。其原型为:

check_type_size(<type> <variable> [BUILTIN_TYPES_ONLY]
                                  [LANGUAGE <language>]) # needs "include(CheckTypeSize)"

示例为:

include(CheckTypeSize)
check_type_size("socklen_t" HAS_SOCKLEN_T BUILTIN_TYPES_ONLY)

以上就是我们常用的针对系统及编译器的检测函数。
全部内容请参考 https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html

cmake默认变量

cmake也提供了一些基础的变量以供配置过程使用。同样的,在此列举一些常用的:

  • BUILD_SHARED_LIBS: 为 ON 时默认生成动态库,否则默认生成静态库
  • CMAKE_BUILD_TYPE: 库生成类型, debug、release、RelWithDebInfo、MinSizeRel等
  • CMAKE_BINARY_DIR: 生成的中间文件路径
  • CMAKE_COMMAND: cmake可执行程序的路径,一般用于执行命令
  • CMAKE_CROSSCOMPILING: 是否正在交叉编译。cmake支持交叉编译。
  • CMAKE_CURRENT_FUNCTION: 当前调用的函数名称
  • CMAKE_CURRENT_LIST_DIR: 当前执行的代码所在文件夹路径
  • CMAKE_CURRENT_LIST_FILE: 当前执行的代码所在文件名
  • CMAKE_EXECUTABLE_SUFFIX: 当前系统的可执行文件默认后缀
  • CMAKE_GENERATOR: 当前选择的编译器。
  • CMAKE_GENERATOR_PLATFORM: 当前选择的编译器平台。
  • CMAKE_GENERATOR_TOOLSET: 当前选择的编译器toolset。
  • CMAKE_PROJECT_NAME: 当前工程名称。
  • CMAKE_PROJECT_VERSION / CMAKE_PROJECT_VERSION_MAJOR / CMAKE_PROJECT_VERSION_MINOR / CMAKE_PROJECT_VERSION_PATCH: 当前工程完全版本号 / 大版本号 / 小版本号 / 补丁号
  • CMAKE_SIZEOF_VOID_P: 当前架构指针默认长度,可用来判断x86或x64。
  • CMAKE_SOURCE_DIR: 当前源码的根目录。
  • CMAKE_TOOLCHAIN_FILE: cmake toolchain文件(包含路径),入参。
  • CMAKE_VERSION: 当前cmake版本。
  • CMAKE_FIND_LIBRARY_PREFIXES: 使用 find_library 时默认包含的库前缀。

例如:

set(CMAKE_FIND_LIBRARY_PREFIXES lib)
find_library(MATH_LIB NAMES m) # 查找库名为libm.a

注意:此值在非Windows中默认设置为“lib”。

  • CMAKE_FIND_LIBRARY_SUFFIXES: 使用 find_library 时默认包含的库后缀。

例如:

set(CMAKE_FIND_LIBRARY_SUFFIXES lib)
find_library(SAMPLE_LIB NAMES sample) # 查找库名为sample.lib

此值根据不同系统而变:

  1. lib —— Windows
  2. a —— Unix-style
  • CMAKE_INSTALL_PREFIX: 安装文件的根目录。控制文件安装位置。
  • CMAKE_MODULE_PATH: 查找所有cmake模块文件的默认路径。(不包括config文件路径,包括cmake提供的文件)
  • CMAKEPREFIX_PATH: 使用 **find* 族函数查找的默认路径。

注意:此变量为list,影响了 find_package 的结果。

  • CMAKE_PROGRAM_PATH: 使用 find_program 查找可执行程序的默认路径。

至此,有关系统/编辑器和cmake默认参数的内容介绍完毕。
在下一章中,我将讲解如何将cmake配置导出来让下游查找并使用。

10-导出配置

为了让下游能够方便的使用我们发布的库,我们通常会提供两种配置之一: Find模块或CONFIG模块。
对于find模块(Find.cmake)来说,它并不能根据库的更新来被动升级,所以经常会出现一些bug。而使用cmake导出的CONFIG模块更加合适。
在这篇教程中,我将展示将库导出为CONFIG模块的各个函数及用法。
(依据:Importing and Exporting Guide

Prequirements

当需要导出配置时,我们必须对一些cmake编译相关函数进行处理:

  • 将所有不包含以 target 开头的函数尽量转换为以 target 开头的函数以声明作用域。
  • target_compile_definitions / target_include_directories 一类构建与使用方式不同的函数中的参数进行修改:
    • PUBLIC 中的所有变量拆分为 $<BUILD_INTERFACE:VAR_USED_IN_BUILD>$<INSTALL_INTERFACE: VAR_USED_IN_USE>
    • 保留其他关键字中内容
  • 如果依赖于其他库并使用CONFIG模式(使用依赖的target),则应当预先准备<_PACKAGE_NAME>http://Config.cmake.in_ 并写入 find_package 对应 find_dependency 代码。
  • 如果需要导出版本并对版本匹配有特殊要求,则应当预先准备 _<PACKAGE_NAME>Confighttp://Version.cmake.in_ 并写入对应代码。

install(EXPORT)

虽然cmake提供了export函数,但是现在已经被 install(EXPORT) 所替代。在这里我只讲解后者。

  • 首先,在 install(TARGETS) 的代码中声明导出target的名称:

    install(TARGETS sample
          EXPORT sampleTargets # Additional content 
          RUNTIME ...
          LIBRARY ...
          ARCHIVE ...
          # ...
    )
    
  • 其次,单独使用 install(EXPORT) :

    install(EXPORT sampleTargets # Declared in install(TARGETS)
      FILE sampleTargets.cmake # 导出的文件基准名。
      NAMESPACE sample:: # 使用该库时使用包含namespace用法。可选参数
      DESTINATION share/sample # 依据CMAKE_INSTALL_PREFIX为基准路径
    )
    

    此函数生成sampleTargets.cmake 并根据配置生成 sampleTargets-debug.cmakesampleTargets-release.cmake 。 一个配置中只生成两个文件。
    sampleTargets.cmake 文件将在内部自动调用 sampleTargets-debug.cmake 或/和 sampleTargets-release.cmake。
    注意:如果不包含依赖或使用MODULE模式查找依赖(使用依赖的各种绝对路径而不是target),则可直接导出 sampleConfig 并不需要单独编写 sampleConfig.cmake 的内容。

  • 使用 file(WRITE) 或创建 sampleConfig.cmake.in 文件来编写包含查找依赖的代码和各种检查函数 ```cmake @PACKAGE_INIT@

include(CMakeFindDependencyMacro) find_dependency(…)

include(“${CMAKE_CURRENT_LIST_DIR}/sampleTargets.cmake”)

其中内容会在下面讲解。

<a name="ydaI3"></a>
## PACKAGE_INIT
此宏将在 configure_file 时自动替换为对路径及内容进行检查的预设cmake宏,用于检查各种预设路径。可以替换 set 为 set_and_check 以使用以下内容调用这个检查,<br />例如:
```cmake
 set_and_check(@PROJECT_NAME@_INSTALL_DIR @PACKAGE_CMAKE_INSTALL_PREFIX@)
 set_and_check(@PROJECT_NAME@_INCLUDE_DIR @PACKAGE_CMAKE_INSTALL_PREFIX@/include)

find_dependency

此函数对应 find_package ,作为在CONFIG配置中查找依赖的唯一方法。需提前包含 CMakeFindDependencyMacro.cmake文件。
示例:

include(CMakeFindDependencyMacro)
find_dependency(absl CONFIG REQUIRED) # Match find_package(absl CONFIG REQUIRED)

所有包含在 find_package 中的内容应当无修改的写入此函数中。

write_basic_package_version_file

非特殊情况下,我们希望轻松的导出版本文件。cmake提供了此函数来实现这个功能。其原型为:

write_basic_package_version_file(<filename>
  [VERSION <major.minor.patch>]
  COMPATIBILITY <AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion>
  [ARCH_INDEPENDENT] )

例如:

write_basic_package_version_file(
  "${CMAKE_BINARY_DIR}/sampleConfigVersion.cmake"
  COMPATIBILITY ExactVersion
)

sample code

在此我提供一个简单的示例代码:

# ...
# find the dependencies
find_package(absl CONFIG REQUIRED)
# ...
# glob the sources and headers
list(APPEND SAMPLE_SOURCES
    # ...
)
list(APPEND SAMPLE_PUBLIC_HDRS 
    # ...
)
# Declare the target
add_library(sample ${SAMPLE_SOURCES})
if (BUILD_SHARED_LIBS)
    target_compile_definitions(sample PUBLIC $<BUILD_INTERFACE:BUILD_DLL> $<INSTALL_INTERFACE:USE_DLL>)
else()
    target_compile_definitions(sample PUBLIC USE_LIB)
endif()
target_include_directories(sample PUBLIC $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include/sample> $<INSTALL_INTERFACE:include/sample>)
target_link_libraries(sample PUBLIC abseil::abseil PRIVATE threads)

# Install headers
install(FILES ${SAMPLE_PUBLIC_HDRS} DESTINATION include/sample) # 依据CMAKE_INSTALL_PREFIX为基准路径
# Install binaries
install(TARGETS sample
    EXPORT sampleTargets # Export name
    # ...
)
# Install cmake targets files
install(EXPOT sampleTargets # Export name
    FILE sampleTargets.cmake # 导出的文件基准名。
    NAMESPACE sample:: # 使用该库时使用包含namespace用法。可选参数
    DESTINATION share/sample # 依据CMAKE_INSTALL_PREFIX为基准路径
)
# Install cmake config file
file(WRITE "${CMAKE_BINARY_DIR}/sampleConfig.cmake.in" [[
@PACKAGE_INIT@

include(CMakeFindDependencyMacro)
find_dependency(absl CONFIG REQUIRED)

include("${CMAKE_CURRENT_LIST_DIR}/sampleTargets.cmake")
]])

configure_file(sampleConfig.cmake.in ${CMAKE_BINARY_DIR}/sampleConfig.cmake @ONLY)
install(FILES "${CMAKE_BINARY_DIR}/sampleConfig.cmake" DESTINATION share/sample) # 依据CMAKE_INSTALL_PREFIX为基准路径

至此,你可以熟练的使用cmake来完成项目的各项配置工作。
在下篇内容中,我将开始讲解cmake高端用法:toolchain。

11-toolchain及重载

简介

cmake支持使用toolchain(工具链),大家应该有些toolchain这个概念。我们可以编写toolchain并提供给用户以自动的设置或应用一些功能。

编译链

其实cmake自带一些toolchain,我们在使用默认设置时(传入Generator)时就已经在用了。其位置在 _cmake/share/cmake-<VERSION>/Modules/Platform_ 中。我们可以发现这个目录中包含了各个平台及各种编译器匹配的一些cmake MODULE文件(_<PLATFORM>-<COMPILER>.cmake_)。
打开其中一个文件,我们可以看到其中设置了一些cmake的默认变量。其中重要的有:

  • CMAKE__COMPILER
  • CMAKE__FLAGS*
  • CMAKE_*_FLAG
  • CMAKE__STANDARD
  • CMAKE_AR
  • *_VERSION
  • CMAKE_*_PATH
  • CMAKE_*_PREFIX
  • CMAKE_*_SUFFIX

我们可以发现,这些设置是针对以下几点的:

  1. 编译器
  2. 编译器默认选项
  3. 编译器版本
  4. 语言标准
  5. 各种路径
  6. 生成文件前缀
  7. 生成文件后缀

这也就是为什么我们不需要设置什么就可以直接找到并使用编译器的原因。
我们完全可以编写一套针对于自定义编译器的规则,并提供给下游使用。这就是toolchain的第一种实现。
值得一提的是,cmake支持交叉编译。所以在这些变量中,我们可以发现一些包含“HOST”的变量及一些包含“TARGET”的变量。
所有相关的选项可在官方文档中查阅: https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html

功能链

当然,有时候我们需要做的并没有这么多:可能是提供一种feature,可能是优化某些函数。此时,我们也可以将其cmake代码保存为toolchain并向下游提供。这时toolchain的第二种实现。
我们在这里以vcpkg toolchain当做示例讨论:
scripts/buildsystems/vcpkg.cmake · JackBoosY/vcpkg - Gitee.com
我们可以看到,toolchain中设置了一些选项,以供用户使用。并使用了一些特殊的用法例如:

# ...
function(add_library)
    z_vcpkg_function_arguments(ARGS)
    _add_library(${ARGS})
    set(target_name "${ARGV0}")

    list(FIND ARGS "IMPORTED" IMPORTED_IDX)
    list(FIND ARGS "INTERFACE" INTERFACE_IDX)
    list(FIND ARGS "ALIAS" ALIAS_IDX)
    if(IMPORTED_IDX EQUAL -1 AND INTERFACE_IDX EQUAL -1 AND ALIAS_IDX EQUAL -1)
        get_target_property(IS_LIBRARY_SHARED "${target_name}" TYPE)
        if(VCPKG_APPLOCAL_DEPS AND Z_VCPKG_TARGET_TRIPLET_PLAT MATCHES "windows|uwp" AND (IS_LIBRARY_SHARED STREQUAL "SHARED_LIBRARY" OR IS_LIBRARY_SHARED STREQUAL "MODULE_LIBRARY"))
            z_vcpkg_set_powershell_path()
            add_custom_command(TARGET "${target_name}" POST_BUILD
                COMMAND "${Z_VCPKG_POWERSHELL_PATH}" -noprofile -executionpolicy Bypass -file "${Z_VCPKG_TOOLCHAIN_DIR}/msbuild/applocal.ps1"
                    -targetBinary "$<TARGET_FILE:${target_name}>"
                    -installedDir "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}$<$<CONFIG:Debug>:/debug>/bin"
                    -OutVariable out
            )
        endif()
        set_target_properties("${target_name}" PROPERTIES VS_USER_PROPS do_not_import_user.props)
        set_target_properties("${target_name}" PROPERTIES VS_GLOBAL_VcpkgEnabled false)
    endif()
endfunction()
# ...

add_library 不是cmake提供的函数用来声明一个库吗?为什么这里还能定义一个相同名称的函数?
还有,为什么里面会调用以“_”开头的相同名称函数?这么写是什么意思?
这就是cmake的重载功能:我们可以重载任意已有的函数,来实现特殊功能:
我们可以再次声明已知函数,并使用包含前缀“_”的相同函数名来调用原始函数。
在上面的例子中,vcpkg重载了 add_library 函数,并在调用原始 add_library 之后执行了一个自定义命令。其命令内容为通过 powershell 脚本来分析此库需要使用的依赖,并执行自动复制到安装路径下。

我相信,当你从“使用者”变为“提供者”时,toolchain是你最佳的实现方式。

以上内容来源为官方文档: https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html
在最后一篇内容中,我将介绍cmake policy。各位下篇见。

12-policy

为什么需要policy?cmake在不断更新中且更新幅度很大,新的feature在旧版本上肯定是不能用的,一些行为在旧版本与新版本上面也可能有差异。
为了解决这些差异,保证我们的cmake代码始终可用,cmake提出了policy这个概念。
最简单的例子就是 list 的 PREPEND 关键字。该关键字在cmake 3.15 引进,如果使用该版本之前的版本,则会直接执行失败并终止。
所有的policy可在官方文档查询: https://cmake.org/cmake/help/latest/manual/cmake-policies.7.html

这里,我介绍使用方式:

cmake_minimum_required

使用该函数来限制最低的cmake版本。

if (POLICY CMP)

使用此条件来判断当前的cmake是否支持某条policy。

cmake_policy

使用此函数来设置/获取policy。此函数有多重用法:

  • 声明policy:使用 NEW 来声明后续代码依赖于此policy。使用OLD来向使用此policy的配置过程发出警告信息。

    cmake_policy(SET CMP<NNNN> NEW)
    cmake_policy(SET CMP<NNNN> OLD)
    
  • 获取policy: 使用GET来获取当前支持的policy。

    cmake_policy(GET CMP<NNNN> <variable>)
    
  • 临时设置policy: 使用PUSH/POP来临时将其中的一段代码设置policy。一般用于依赖于此policy的关键代码。

    cmake_policy(PUSH)
    cmake_policy(POP)
    
  • 设置全局policy版本: 设置该项目的policy范围。但一般使用cmake_minimum_required。

    cmake_policy(VERSION <min>[...<max>])
    

samples

对于policy,我们可以做以下实验:

  • 向一个CMakeLists.txt中添加以下内容:

    cmake_policy(SET CMP9999 NEW) # CMP9999 is not defined yet
    

    configure,cmake会报错并终止configure流程:

    > [CMake] CMake Error at CMakeLists.txt:14 (cmake_policy):
    1> [CMake]   Policy "CMP9999" is not known to this version of CMake.
    

    如果说我们需要在全局声明必须支持此policy,则可以用这种方法。

  • 向一个CMakeLists.txt中添加以下内容:

    if (POLICY CMP0000)
    cmake_policy(SET CMP0000 OLD) # CMP0000 is always outdated
    endif()
    

    configure,cmake会警告而不会终止configure流程:

    1> [CMake] CMake Deprecation Warning at CMakeProject1/CMakeLists.txt:14 (cmake_policy):
    1> [CMake]   The OLD behavior for policy CMP0000 will be removed from a future version
    1> [CMake]   of CMake.
    1> [CMake] 
    1> [CMake]   The cmake-policies(7) manual explains that the OLD behaviors of all
    1> [CMake]   policies are deprecated and that a policy should be set to OLD only under
    1> [CMake]   specific short-term circumstances.  Projects should be ported to the NEW
    1> [CMake]   behavior and not rely on setting a policy to OLD.
    

    我们可以强制声明某个policy已过期,并让cmake自动发出警告,但不会终止configure流程。

  • 向一个CMakeLists.txt中添加以下内容:

    cmake_policy(PUSH)
    cmake_policy(SET CMP0057 NEW)
    # ...
    if (SOME_ITEM IN_LIST SOME_LIST) # IN_LIST needs CMP0057
    # ...
    cmake_policy(POP)
    

    当使用不支持此policy的cmake版本configure时,cmake会报错并终止configure流程:

    1> [CMake] CMake Error at CMakeProject1/CMakeLists.txt:15 (cmake_policy):
    1> [CMake]   Policy "CMP0057" is not known to this version of CMake.
    

    我们可以使用此方式来声明该段代码必须使用支持此policy的cmake版本。注意作用域仅限于此段代码。

  • 向一个CMakeLists.txt中添加以下内容:

    cmake_policy(PUSH)
    cmake_policy(VERSION 3.15)
    list(PREPEND SOME_LIST ${SOME_ITEM}) # PREPEND needs cmake 3.15
    cmake_policy(POP)
    

    当使用低于此version的cmake版本configure时,cmake会报错并终止configure流程:

    1> [CMake] CMake Error at CMakeProject1/CMakeLists.txt:15 (cmake_policy):
    1> [CMake]   An attempt was made to set the policy version of CMake to "3.15" which is
    1> [CMake]   greater than this version of CMake.  This is not allowed because the
    1> [CMake]   greater version may have new policies not known to this CMake.  You may
    1> [CMake]   need a newer CMake version to build this project.
    

    当某个cmake feature没有设置特定的policy时,我们可以使用此方式来声明该段代码必须使用不低于这个版本的cmake。注意作用域仅限于此段代码。

读到这里,相信你已经会使用cmake高级用法。希望这13篇教程对你有所帮助。
在所有的教程中,由于作者水平,难免会有一些错误或误差,请各位多多包涵并提出意见建议。

谢谢各位。

2022-04-07编辑:添加samples。