按:

这个系列与 cmake 有关的记录也好,笔记也好,成书也好,目前暂且归属在 cmake-hello 标记之下。

源码请访问:

https://github.com/hedzr/study-cmake
请注意源码仍在跟随我的笔记内容的迭代而更新中。

本章中逐步解释 study-cmake 所建立的 cmake 范本所处理的任务。

C++11 等

CMAKE_CXX_STANDARD 是一个内建的变量,其用途是为了去掉 -stdc++11 -gnuc++11 -std=gnu++11 等等等这样的乱象。

我们现在也有了比较安全简练的宣告方法:

1
2
3
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# set(CMAKE_CXX_EXTENSIONS OFF)

CMAKE_CXX_STANDARD_REQUIRED 可以为每个 Target 初始化 CXX_STANDARD_REQUIRED 属性。而 CXX_STANDARD_REQUIRED 存在的目的是如果用户没有对自己的 CXX_STANDARD 作出宣告的话,那它就会死——但 set(CMAKE_CXX_STANDARD 17) 可以令这些措施一律无意义。这是为了能够对编译器的C++标准兼容性进行更好的约束,但对绝大多数人来说毫无意义。

针对具体 Target

旧方法

对于每个 Target 来说,你原本应该这样宣告 CXX_STANDARD:

1
set_property(TARGET tgt PROPERTY CXX_STANDARD 11)

但 CMAKE_CXX_STANDARD 已经被预设的情况下,CXX_STANDARD 会获得相应值。

新方法

1
2
3
4
5
set_target_properties(myTarget PROPERTIES
    CXX_STANDARD 11
    CXX_STANDARD_REQUIRED YES
    CXX_EXTENSIONS NO
)

Policies

Policies 不知道该译作策略、政策、准则还是前提条件,或者是约定?所以干脆不译了,反正当年搞 C++ Template 时我也从未翻译过 policy。

CMake Policies 是一系列的兼容性约定,它决定了 CMake 在处理 CMakeLists.txt 时怎么进行解释,要不要判定无效、过时语法或者尚不能识别的语法并对其报错。

对于现实生活来说,你往往并不需要关心该采用哪些 policies,又该将哪些 policies 置为向前兼容。因为很多错误提示都将会提示你应该启用 CMP#### 的 Policy 兼容性开关。

一些常常被用到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# CMake Warning (dev) at CMakeLists.txt:5 (ADD_EXECUTABLE):
# Policy CMP0049 is not set: Do not expand variables in target source
# entries.  Run "cmake --help-policy CMP0049" for policy details.  Use the
# cmake_policy command to set the policy and suppress this warning.
if (POLICY CMP0049)
  cmake_policy(SET CMP0049 NEW)
endif ()

if (COMMAND cmake_policy)
  # we prefer the more strict behavior, to find out more:
  # cmake --help-policy CMP0003
  cmake_policy(SET CMP0003 NEW)
endif ()


cmake_policy(SET CMP0042 NEW) # ENABLE CMP0042: MACOSX_RPATH is enabled by default.
cmake_policy(SET CMP0048 NEW)
cmake_policy(SET CMP0054 NEW) # ENABLE CMP0054: Only interpret if() arguments as variables or keywords when unquoted.
cmake_policy(SET CMP0063 NEW) # ENABLE CMP0063: Honor visibility properties for all target types.
cmake_policy(SET CMP0069 NEW)
cmake_policy(SET CMP0077 NEW) # ENABLE CMP0077: option() honors normal variables

if (POLICY CMP0068)
  cmake_policy(SET CMP0068 NEW)
endif ()

if (${CMAKE_VERSION} VERSION_LESS 3.12)
  cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif ()

你可以跳过了解 Policy,它不会阻碍你掌握 cmake 的运用。

在将来当你遇到相关问题时,跟随提示阅读在线文档即可。例如对于 CMP0054 来说,命令行 cmake --help-policy CMP0054 能够获得一个简要的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
CMP0054
-------

Only interpret ``if()`` arguments as variables or keywords when unquoted.

CMake 3.1 and above no longer implicitly dereference variables or
interpret keywords in an ``if()`` command argument when
it is a :ref:`Quoted Argument` or a :ref:`Bracket Argument`.

The ``OLD`` behavior for this policy is to dereference variables and
interpret keywords even if they are quoted or bracketed.
The ``NEW`` behavior is to not dereference variables or interpret keywords
that have been quoted or bracketed.

Given the following partial example:

::

 set(A E)
 set(E "")

 if("${A}" STREQUAL "")
   message("Result is TRUE before CMake 3.1 or when CMP0054 is OLD")
 else()
   message("Result is FALSE in CMake 3.1 and above if CMP0054 is NEW")
 endif()

After explicit expansion of variables this gives:

::

 if("E" STREQUAL "")

With the policy set to ``OLD`` implicit expansion reduces this semantically to:

::

 if("" STREQUAL "")

With the policy set to ``NEW`` the quoted arguments will not be
further dereferenced:

::

 if("E" STREQUAL "")

This policy was introduced in CMake version 3.1.
CMake version 3.18.3 warns when the policy is not set and uses
``OLD`` behavior.  Use the ``cmake_policy()`` command to set
it to ``OLD`` or ``NEW`` explicitly.

.. note::
  The ``OLD`` behavior of a policy is
  ``deprecated by definition``
  and may be removed in a future version of CMake.

这玩意儿老长的,但本质特性就一句话,以前 if() 中的变量名不管有没有引号包围都会被尝试做变量求值和展开(也不管有没有 ${var} 展开表达式),但如果你启用了 cmake_policy(SET CMP0054 NEW) 呢,引号包围的变量名就不会被尝试求值了,它就是个简单的字符串而已,引号中想要求值就必须采用 ${var} 展开表达式才行了。

作为一个拓展,你也可以参考:

等等。

Modules

你可以自定义 FindXXX.cmake,当你在使用 find_library(XXX) 时,find_library 将会寻找 FindXXX.cmake 并用于定位特定的 Module。

例如你可以编写一个 FindFLEX.cmake,那么今后用到 find_library(FLEX) 的时候,你的脚本就可以被用于寻找 flex,这是一个著名的词法编译器的编译器。

对于 find_package 指令来说也是如此。

不过,在本节之中,只是为了给传统(旧式的)Modules留一个位置,因为早期 cmake 编写惯例中需要首先编写一系列的 FindXXX 比如说 find_package(BOOST REQUIRED) 什么的来定位和安排我们所需要用到的第三方依赖库。

这往往还会涉及到第三方依赖库被安放在什么地方,要不要事先编译等等问题。

而在 Modern CMake 中,我们只会在每个 Target 中有选择地进行 FindXXX 调用,有的时候甚至可以略过它。

更多有用的模块

cmake 提供了大量内置的 Modules,CheckXXX、FindXXX 是其中最著名的两大类。CheckXXX 通常提供某种环境条件的检测并以内建变量的方式返回检测结果,FindXXX 一般被 find_package 或 find_library 指令所调用,目的是找到构建主机上某个第三方库的安装位置、头文件、库文件等等配置值,解决相应的依赖关系。

有一些 Modules 可能是相当有用的,所以下面选择几个简单介绍,你可以在 Modules at GitHub 浏览全部 cmake 内置 Modules,此外,官网文档的 cmake-modules(7) — CMake 3.19.0 Documentation 区有相应的文档,都能提供很好的帮助。

CheckCXXCompilerFlag

CheckCXXCompilerFlagdoc, src)可被用于检测编译器标志是否是被当前编译器所支持的。

1
2
3
4
5
6
7
8
9
10
11
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-std=c++21" COMPILER_SUPPORTS_CXX21)
check_cxx_compiler_flag("-std=c++17" COMPILER_SUPPORTS_CXX17)
check_cxx_compiler_flag("-std=c++14" COMPILER_SUPPORTS_CXX14)
check_cxx_compiler_flag("-std=c++11" COMPILER_SUPPORTS_CXX11)
check_cxx_compiler_flag("-std=c++0x" COMPILER_SUPPORTS_CXX0X)
if(COMPILER_SUPPORTS_CXX11)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
    add_definitions(-DCOMPILEDWITHC11)
    message(STATUS "Using flag -std=c++11.") 
endif()

注意类似的内建 Modules 还可以有 [CheckForPthreads],[CheckFunctionExists],[CheckLanguage],[CheckLibraryExists],[CheckLinkerFlag] 等等。在 cmake-modules(7) 页面中可以找到它们。

CMakePrintHelpers

CMakePrintHelpersdoc, src)包含一些调试输出函数,例如 cmake_print_variablescmake_print_properties

示例:

1
2
3
4
cmake_print_variables(var1 var2 ..  varN)
cmake_print_variables(CMAKE_C_COMPILER CMAKE_MAJOR_VERSION DOES_NOT_EXIST)
cmake_print_properties(TARGETS foo bar PROPERTIES
                       LOCATION INTERFACE_INCLUDE_DIRECTORIES)

cmake_print_properties 具有如下的语法和格式:

1
2
3
4
5
6
7
  cmake_print_properties([TARGETS target1 ..  targetN]
                        [SOURCES source1 .. sourceN]
                        [DIRECTORIES dir1 .. dirN]
                        [TESTS test1 .. testN]
                        [CACHE_ENTRIES entry1 .. entryN]
                        PROPERTIES prop1 .. propN )

FeatureSummary

在 CMakeLists.txt 一开始加入命令:

1
include(FeatureSummary)

接着照常使用任何 find_package 语句。现在有所不同的是,find 过程中的相关信息都能被收集到某处。最终,你可以将这些信息输出到某处:

1
2
3
4
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
    feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES PACKAGES_FOUND)
    feature_summary(FILENAME ${CMAKE_CURRENT_BINARY_DIR}/features.log WHAT ALL)
endif()

在你使用大量 find_package 的过程中,还可以使用 add_feature_info 来嵌入你自己的文字。例如这样:

1
2
3
4
5
6
7
8
include(FeatureSummary)

...
find_package(OpenMP)
add_feature_info(WITH_OPENMP OpenMP_CXX_FOUND "OpenMP (Thread safe FCNs only)")
...

...

你还可以为某个程序包扩展一些元数据:

1
2
3
4
set_package_properties(OpenMP PROPERTIES
    URL "http://www.openmp.org"
    DESCRIPTION "Parallel compiler directives"
    PURPOSE "This is what it does in my package")

GNUInstallDirs

CMake 提供一个内建的标准化目录集,这是一组符合 GNU Coding Standard 的文件夹的预定义变量集合。它们将被用于 install() 指令中。

我们可以首先包含它:

1
2
# Must use GNUInstallDirs to install libraries into correct locations on all platforms.
include(GNUInstallDirs)

然后我们将会获得一堆变量,它们是 CMAKE_INSTALL_<dir> 以及 CMAKE_INSTALL_FULL_<dir>

1
2
CMAKE_INSTALL_<dir>      - destination for files of a given type
CMAKE_INSTALL_FULL_<dir> - corresponding absolute path

<dir> 将会是这些名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BINDIR           - user executables (bin)
SBINDIR          - system admin executables (sbin)
LIBEXECDIR       - program executables (libexec)
SYSCONFDIR       - read-only single-machine data (etc)
SHAREDSTATEDIR   - modifiable architecture-independent data (com)
LOCALSTATEDIR    - modifiable single-machine data (var)
LIBDIR           - object code libraries (lib or lib64 or lib/<multiarch-tuple> on Debian)
INCLUDEDIR       - C header files (include)
OLDINCLUDEDIR    - C header files for non-gcc (/usr/include)
DATAROOTDIR      - read-only architecture-independent data root (share)
DATADIR          - read-only architecture-independent data (DATAROOTDIR)
INFODIR          - info documentation (DATAROOTDIR/info)
LOCALEDIR        - locale-dependent data (DATAROOTDIR/locale)
MANDIR           - man documentation (DATAROOTDIR/man)
DOCDIR           - documentation root (DATAROOTDIR/doc/PROJECT_NAME)

这些目录与 PREFIX 有关:在 configure 体系中,指的是 ./configure --prefix,通常我们是使用 ./configure --prefix=/usr/local 来让 INSTALL_DIR 为 /usr/local/{bin,include,lib} 等等位置而不是 /usr/{bin,include,lib} 等位置以避免 sudo/root 特权请求;而在 cmake 体系中,它指的是 CMAKE_INSTALL_PREFIX 这个变量,你可以设置该变量达到等同的效果。

例如典型地 CMAKE_INSTALL_FULL_BINDIR=/usr/local/bin

不做 CMAKE_INSTALL_PREFIX 设定的话,它的默认值是 /usr/local,这是现代 cmake 以及现代 linux 发行版(以及macOS)的经常性的默认设定。不过这个话题相当复杂,因为有的时候可能会有 empty 值,/ 以及 /opt 等可能性。关于文件系统中的这些结构也可以参考 Filesystem Hierarchy Standard

CMAKE_INSTTALL_PREFIX 更多是为库的使用者准备的,使用者可以通过 cmake -D CMAKE_INSTALL_PREFIX=xxx 的方式来选择自己想要的构建目的。

对于想要支持 Multi-arch 的使用者来说,他也许会使用 /usr/local/lib/x86 作为一个构建目的,这避免了污染构建主机上的活动 arch。至于使用 /usr/local 而不是 /usr 来避免 sudo 请求也是一种可能。其它的可能性还有不同的 lib 变体(例如 /usr/local/{lib,lib64,lib32,libx32}),交叉编译环境,等等。

GoogleTest

TODO

UsewxWidgets

TODO

WriteCompilerDetectionHeader

在下一节中会有详细介绍

检测系统,检测编译器环境

检测构建时系统环境

对于系统环境,cmake 会在首次构建准备时识别并置放内建变量 CMAKE_SYSTEM_NAMECMAKE_SYSTEM_VERSIONCMAKE_SYSTEM_PROCESSOR(可能的取值可以参阅 Possible values for uname -m)。如果对内幕感兴趣还可以参考 CMakeDetermineSystem.cmake 中的代码。

由于我们喜欢 MACOS,以及其它一些原因,所以制作了一个 detect-systems.cmake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# for MacOS X or iOS, watchOS, tvOS (since 3.10.3)
if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin" OR APPLE)
  # MESSAGE("** Darwin detected.")
  SET(MACOSX TRUE)
  SET(MACOS TRUE)
  SET(macOS TRUE)
  SET(DARWIN TRUE)
  SET(MAC TRUE)
  SET(Mac TRUE)
endif ()

if (UNIX AND NOT MACOS)
  # for Linux, BSD, Solaris, Minix
  if (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
    set(LINUX TRUE)
    set(Linux TRUE)
  elseif (${CMAKE_SYSTEM_NAME} MATCHES "^GNU/kFreeBSD|NetBSD|OpenBSD")
    set(BSD TRUE)
  elseif (${CMAKE_SYSTEM_NAME} MATCHES "Minix")
    set(Minix TRUE)
  endif ()
endif ()

if (WIN32)
  #do something
endif (WIN32)

if (MSVC OR MSYS OR MINGW)
  # for detecting Windows compilers
endif ()


if (LINUX)
  message(STATUS ">>> Linux" " FOUND")
elseif (MAC)
  message(STATUS ">>> macOS" " FOUND")
elseif (UNIX)
  message(STATUS ">>> Unix (BSD+,Unix+)" " FOUND")
elseif (MSVC)
  message(STATUS ">>> MSVC" " FOUND")
elseif (WIN32)
  message(STATUS ">>> Win32" " FOUND")
else ()
  message(STATUS ">>> Unknown" " FOUND")
endif ()

检测编译器环境

cmake 也自动完成编译器的检测。检测的结果被分散在世界各地,所以我们通常不针对编译器做特定的校正,而且依赖于兼容 CXX_STANDARD 并且期待编译器能够满足我们的 CXX_STANDARD 宣告。

如果存在特殊的语法兼容问题,则我们往往是通过在 C++ 代码文件中使用 #if 宏来搞定的。

古典的 C++ 代码中的编译器及其特性检测已经超出了本文的范畴,所以我们不做展开。但实际上你也可以不再学习传统的 #if 宏套路,而是利用 cmake 的检测结果。

所以现在我们更多是采用 cxx 标准以及 cxx 特性的支持度来进行代码编写。在C++的现代开发中,如果通过测试编译器支持度的方式来编写好的模板代码基本上是 C++ 元编程的必须学习的步骤,不过这个方面其实也没有什么系统的教材、文章可以求取,只能通过研究他人源码、跟进 CppCon 之类的风向去设法学习。

不过 CMake 提供了另一种方案,比较轻量和简便,只是略有一点被绑定的危险。请继续阅读下面的小节:

WriteCompilerDetectionHeader

CMake 现在支持一种叫做 WriteCompilerDetectionHeader ( src ) 的新特性,它可以将编译器定义为一个C++宏变量并写入一个 C++ 头文件。除此而外,它还通过检测各 CXX 编译器预定义宏的方式来提供对 CXX 特性的判定。

依据这些判定,我们能够通过较为简单的方式来确定当前编译器对 CXX 标准特性的支持度,例如我们可以通过指定 FEATURES cxx_variadic_templates 来检测编译器对可变数量参数的模板的支持度。

更多的 cxx 特性名字可直达 Symbol MacrosCompatibility Implementation Macros 等等一系列在线文档。

检测 variadic template 特性

我们制作了名为 cxx-detect-compilers.cmake 的片段来引用该特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# https://cmake.org/cmake/help/latest/module/WriteCompilerDetectionHeader.html
# include(WriteCompilerDetectionHeader)
# https://cmake.org/cmake/help/v3.14/manual/cmake-compile-features.7.html#manual:cmake-compile-features(7)
set(WriterCompilerDetectionHeaderFound NOTFOUND)
# This module is only available with CMake >=3.1, so check whether it could be found
# BUT in CMake 3.1 this module doesn't recognize AppleClang as compiler, so just use it as of CMake 3.2
if (${CMAKE_VERSION} VERSION_GREATER "3.2")
  include(WriteCompilerDetectionHeader OPTIONAL RESULT_VARIABLE WriterCompilerDetectionHeaderFound)
endif ()
if (WriterCompilerDetectionHeaderFound)
  write_compiler_detection_header(
          FILE the-compiler.h
          PREFIX The
          COMPILERS GNU Clang MSVC Intel  # AppleClang SunPro
          FEATURES cxx_variadic_templates
  )
endif ()

当 cmake 的构建准备完成之后,你可以引用这个片段的输出文件 the-compiler.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "the-compiler.h"

#if The_COMPILER_IS_AppleClang
#define COMPILER 1
#endif

#if The_COMPILER_IS_GNU
#define COMPILER 2
#endif

// 由于我们请求了 variadic template 特性的检测,所以下面的预编译
// 条件可以保证编译正确性。
#if The_COMPILER_CXX_VARIADIC_TEMPLATES
template<int I, int... Is>
struct Interface;

template<int I>
struct Interface<I>
{
  static int accumulate()
  {
    return I;
  }
};

template<int I, int... Is>
struct Interface
{
  static int accumulate()
  {
    return I + Interface<Is...>::accumulate();
  }
};
#else
template<int I1, int I2 = 0, int I3 = 0, int I4 = 0>
struct Interface
{
  static int accumulate() { return I1 + I2 + I3 + I4; }
};
#endif

也别忽略了真正的宝藏是在 the-compiler.h 中,读它,怎么正确地处理编译器差别你会学到很多东西。

输出文件夹

输出位置与安装

这些路径都是绝对路径:

  • CMAKE_SOURCE_DIR

    内容为 source tree 根目录。

  • CMAKE_BINARY_DIR

    内容为 binary tree 根目录,在 in-source build 时与 CMAKE_SOURCE_DIR 相同。

  • PROJECT_SOURCE_DIR

    对于每个 project 而言,相应的 project 指令所在的 CMakeLists.txt 文件所处的文件夹。

  • PROJECT_BINARY_DIR

    和 CMAKE_BINARY_DIR 相似,往往等于${CMAKE_BIANRY_DIR}/<project-name>。在 in-source build 时和 PROJECT_SOURCE_DIR 相同。

  • CMAKE_CURRENT_SOURCE_DIR

    当前正在处理的 CMakeLists.txt 所在的文件夹。

  • CMAKE_CURRENT_BINARY_DIR

    当前正在处理的 CMakeLists.txt 所对应的构建文件夹位置。

此外,对于每个 PROJECT 来说,<project_name>_SOURCE_DIR<project_name>_BIANRY_DIR 也是有效的。某些场合下你或许用得上。

此外,安装位置也有一组变量,但我们只需要知道

print

利用我们提供的 debug_print_value() 宏你可以很方便地调试和检视这些变量的实际值,以便深刻理解他们的相应关系。

1
2
include(utils)
debug_value_print(PROJECT_SOURCE_DIR)

其它输出位置

EXECUTABLE_OUTPUT_PATH 是执行文件的输出位置,executable target 将被写入该位置。

LIBRARY_OUTPUT_PATH 是库文件的输出位置,library target 将被写入该位置。

默认时,该两变量均位于 CMAKE_BINARY_DIR 之中。

但我们可以将其移出 build 文件夹以便于调用,例如:

1
2
3
4
5
set(EXECUTABLE_OUTPUT_PATH "${CMAKE_SOURCE_DIR}/bin")
set(LIBRARY_OUTPUT_PATH "${CMAKE_SOURCE_DIR}/bin/lib")

# Note that CMAKE_GENERATED_DIR is NOT a cmake builtin variable.
set(CMAKE_GENERATED_DIR "${CMAKE_BINARY_DIR}/generated")

CMAKE_GENERATED_DIR 并不是内建变量,但我们定义它以便于被 version-gen 所使用。有关 version-gen 的内容,请参考“生成版本号文件”章节以及 version.in 和版本迭代的管理

调试和输出

--trace 可以提供比较详细的构建细节,它可能会有助于你寻找自己的 cmake 脚本中的问题。

--verbose 很多时候是有效的,例如在 cmake --build .... 时,它的作用也是增加输出内容的丰富程度。

CMAKE_VERBOSE_MAKEFILE

修改 CMakeLists.txt 加入下面的行:

1
set(CMAKE_VERBOSE_MAKEFILE ON)

则 cmake 会在构建时显示出构建命令行,例如 cc,link到底在执行什么样的选项,链入什么样的中间文件等等。

命令行方式

你可以不必修改 CMakeLists.txt 加入上面的行,而是直接采用命令行方式:

1
cmake -DCMAKE_VERBOSE_MAKEFILE=ON ...options...

或者还可以使用很严格的声明:

1
cmake -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON

强迫症患者会喜欢这样的。

utils

我们提供了一个 utils.cmake 文件,包含了 debug_print_value 以及 debug_print_top_value 等预定义的宏或者函数,它们的作用是将对应变量的值打印出来以便利于侦错。

它们是对CMakePrintHelpers 的补充。

简单的示例:

1
2
3
4
set(A "hello world")
debug_print_value(A)
# 将会输出:
# DEBUG - A = hello world

调试生成表达式

请参考前面的章节 生成式表达式/调试

生成版本号文件

请参考前面的章节 version.in 和版本迭代的管理

指定编译选项

以前我们需要设置 CXX_FLAGS 之类的全局变量。例如:

1
SET(CMAKE_CXX_FLAGS "-std-c++11 ${CMAKE_CXX_FLAGS}")

但后来我们可以通过 add_compile_optionsadd_definitions 而不必去操作全局变量了:

1
2
3
add_definitions(-DFOO -DBAR ...)
add_definitions(-std=c++11)     # CMake 2.8.11 or older
add_compile_options(-std=c++11) # CMake 2.8.12 or newer

再到后来我们可以不必为了 c++11 去直接操作编译器选项了:

1
2
3
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# set(CMAKE_CXX_EXTENSIONS OFF)

但其它的情况(非 -std=c++11)依旧是可以利用 add_compiler_options 的。

到了 Modern CMake 时代呢,全局的 add_compiler_options 被推荐以面向 target 的新指令 target_compile_featurestarget_compile_definitions 以及 target_compile_options 所替代了。

1
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_11)

利用 generator expression 的支持,我们还可以很好地运用多种构建配置。例如当你想要支持 Debug 和 Release 两种构建配置时,可以启用下面的语句:

1
2
3
4
5
set(MY_DEBUG_OPTIONS "-DDEBUG=1")
set(MY_RELEASE_OPTIONS "-DNDEBUG=1")

target_compile_options(foo PUBLIC "$<$<CONFIG:DEBUG>:${MY_DEBUG_OPTIONS}>")
target_compile_options(foo PUBLIC "$<$<CONFIG:RELEASE>:${MY_RELEASE_OPTIONS}>")

宏,函数

实用工具

TODO

自定义宏和函数

study-cmake 样本项目中,我们提供了一系列的 cmake 工具函数(自定义宏和函数),它们被以多种方式组织在 cmake 子文件夹中。

在 Source Tree 根目录的 CMakeLists.txt 中我们以一定的顺序加载它们,从而提供一个较为完备的基本环境——所以你可以重点考虑如何在子目录的 CMakeLists.txt 中针对你的 Target 编写构建处理脚本,而不必关心构建的整体框架——构建脚本的整体框架现在已经具备了大量的基础特性。

Source Tree 根目录的 CMakeLists.txt 样本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
cmake_minimum_required(VERSION 3.9..3.19)
set(CMAKE_SCRIPTS "cmake")
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS}/modules;${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS};${CMAKE_SOURCE_DIR};${CMAKE_MODULE_PATH}")
include(prerequisites)
include(version-def)
include(add-policies)     # ${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS}/
include(detect-systems)
include(target-dirs)
include(utils)


project(study-cmake
        VERSION ${VERSION}
        DESCRIPTION "study for cmake"
        LANGUAGES C CXX)

include(cxx-standard-def)
include(cxx-detect-compilers)
include(setup-build-env)
include(versions-gen)

debug_print_top_vars()


# add_subdirectory(third-party/catch)
# add_subdirectory(src)
# add_subdirectory(test)

# add_library(study_cmake library.cxx library.h)


debug_print_value(CMAKE_RUNTIME_OUTPUT_DIRECTORY)

add_subdirectory(z01-hello-1)
add_subdirectory(z02-library-1)
add_subdirectory(z03-library-2)
add_subdirectory(z04-header-library)

add_subdirectory(z11-m1)

下载文件

FetchContent

TODO

FetchContent 可用于下载远程文件,git repo 等远程对象。

下载远程项目

cmake 现在支持 ExternalProject ( src ) 特性,允许你直接从远程服务器下载项目并嵌入当前项目中。这种方案很好地解决了第三方依赖库的依赖问题。

ExternalProject

典型的用法是从 github(或者其它 git 服务商):

1
2
3
4
5
ExternalProject_Add(foobar
  GIT_REPOSITORY git@github.com:FooCo/FooBar.git
  GIT_TAG        origin/release/1.2.3
  STEP_TARGETS   build
)

也可以从 source tarball:

1
2
3
4
5
6
7
8
find_program(MAKE_EXE NAMES gmake nmake make)
ExternalProject_Add(secretsauce
  URL               http://intranet.somecompany.com/artifacts/sauce-2.7.tgz
                    https://www.somecompany.com/downloads/sauce-2.7.zip
  URL_HASH          MD5=d41d8cd98f00b204e9800998ecf8427e
  CONFIGURE_COMMAND ""
  BUILD_COMMAND     ${MAKE_EXE} sauce
)

ExternalProject 的语法看起来相当复杂,不过那是因为各种来源有着各自的下载和编译参数需求,实际上运用起来还是没有什么难度的。

对于那些支持 modern cmake 风格的项目,我们可以在 BUILD_COMMAND/COMMAND 时使用 cmake 构建命令行语法,甚至可以:

1
2
3
4
5
6
ExternalProject_Add(example
  ... # Download options, etc.
  BUILD_COMMAND ${CMAKE_COMMAND} -E echo "Starting $<CONFIG> build"
  COMMAND       ${CMAKE_COMMAND} --build <BINARY_DIR> --config $<CONFIG>
  COMMAND       ${CMAKE_COMMAND} -E echo "$<CONFIG> build complete"
)

使用 OPTION

option 指令提供一个开关量,如果你在命令行中显式定义了该开关量的话,则以命令行中定义的值,否则的话 option 指令所指定的默认值被应用。例如:

1
2
3
4
5
6
7
option(ENABLE_BOOST "Enable Boost" ON)

if(ENABLE_BOOST)
  set(BOOST_STRING "foo")
endif()

configure_file(config.h.in config.h)

你通过 cmake -DENABLE_BOOST=OFF 的方式可以指示 ENABLE_BOOST 为 false 值,那样的话,Line 3 的检测就不会被通过了。

在很多大型代码库中,借助这种方式能够为代码库的构建提供灵活的配置。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
option(BUILD_SHARED_LIBS "Build DOLFIN with shared libraries." ON)
option(CMAKE_USE_RELATIVE_PATHS "Use relative paths in makefiles and projects." OFF)
option(DOLFIN_AUTO_DETECT_MPI "Detect MPI automatically (turn this off to use the MPI compiler wrappers directly via setting CXX, CXX, FC)." ON)
option(DOLFIN_ENABLE_CODE_COVERAGE "Enable code coverage." OFF)
option(DOLFIN_WITH_LIBRARY_VERSION "Build with library version information." ON)
option(DOLFIN_ENABLE_TESTING "Enable testing." OFF)
option(DOLFIN_ENABLE_GTEST "Enable C++ unit tests with Google Test if DOLFIN_ENABLE_TESTING is true (requires Internet connection to download Google Test when first configured)." ON)
option(DOLFIN_ENABLE_BENCHMARKS "Enable benchmark programs." OFF)
option(DOLFIN_ENABLE_DOCS "Enable generation of documentation." ON)
option(DOLFIN_SKIP_BUILD_TESTS "Skip build tests for testing usability of dependency packages." OFF)
option(DOLFIN_DEPRECATION_ERROR "Turn deprecation warnings into errors." OFF)
option(DOLFIN_IGNORE_PETSC4PY_VERSION "Ignore version of PETSc4py." OFF)

...

其它编译器

在使用 cmake 时,CC 这样的环境变量很难奏效了。远古时我们都是通过 CC=/usr/devel/gcc-9/bin/g++ ./configure 的方式来使能一个非系统默认的编译器的。但对于 cmake 来说,下面的方法才能达到相似的目的:

1
cmake -DCMAKE_C_COMPILER=/usr/local/opt/bin/gcc -DCMAKE_CXX_COMPILER=/usr/local/opt/bin/g++ ...

🔚