按:

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

源码请访问:

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

以下内容完全面向初学者,完全没有参考相关原文,完全依照个人理解成文,所以欠缺体系性。若要系统学习 cmake 的 modern style 不妨直达我所列出的 Modern CMake 的相关网站,也可以追更我后续的文字,暂时打算的是逐步抽空完成一个系列,届时才能呈现出较为正式的版本。

本文可能需要进一步校订细微之处,未来全系列文字全数就绪之后将会以新版本呈现(但应该不会有技术内容上的变化,而是在于措辞与结构上)。

现代编程结构

CMAKE 自身,就是一个编程语言,虽然很笨的样子。

老实说我倒是很鄙视 CMAKE 这所谓的语言,真的是弄得太啰嗦太别扭了。

不过我也承认 CMake 有一个很好的生态,所以现在我们看到什么 ninjia scons 基本上都偃旗息鼓了,但 CMake 的活跃度依旧很高。


CMake从3.0开始进入Modern时代,关于这个时代的演进历史可以参看 What’s new in CMake · Modern CMake

但或许也应注意到,直到大约 v3.5..v3.8 之后,其所谓的 Modern 风格才逐渐完善,中间过程中又有若干的新增内容和废弃内容,可想而知这中间有多少过渡性的东西早已该被废弃,又有多少以讹传讹的内容在中文网路上流传。


那么 CMake 现在,也就是所谓的 Modern CMake1 的脚本是用这么一种编程结构:

  1. 有一个根目录以及 CMakeLists.txt
  2. 有若干子目录以及相应的 CMakeLists.txt,并且被通过 add_subdirectory() 的方式添加到根目录的 CMakeLists.txt 的末尾,从而构成一个完整的构建链。

我们通过这样的编程结构来管理一个工作区(Workspace)中的若干子项目(Projects)以及子项目中的若干构建目标(Targets)

Modern CMake 最著名的是的一个开源书籍:https://cliutils.gitlab.io/modern-cmake/。它由 Henry Schreiner 编写,但也有一些贡献者为其完善。

此外,Youtube 上有几份资源可以康康:

  1. More Modern CMake - Deniz Bahadir - Meeting C++ 20182 - https://www.youtube.com/watch?v=y7ndUhdQuU8

  2. C ++现在2017年:丹尼尔·菲费尔“有效的CMake” 3 - https://www.youtube.com/watch?v=bsXLMQ6WgIk

另外,Effective Modern CMake 中译4 是一篇好文章。而有人也做了

«Modern CMake» 翻译 1. CMake 介绍 - 数据管理乐园 - 博客园5 但格式太差,不易阅读。不过 Modern CMake 还有另一份译文:github, [gitbook](https://xiazuomo.gitbook.io/modern-cmake-chinese/6,只不过翻译进度有点感人。

顶级 CMakeLists.txt

在 Source Tree 根目录的 CMakeLists.txt 中,通常完成基本构建环境的准备,例如检测有效的构建工具,检测依赖的三方库,定义公共边境,等等。

我们把根目录的 CMakeLists.txt 看作是一个 Workspace,其中的每个子目录可以使用 project 宏来定义多个子项目,而每个子项目的作用域范围中分别也可以定义一到多个 Targets

一个样本是:

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
cmake_minimum_required(VERSION 3.9..3.13)

set(CMAKE_SCRIPTS "cmake")
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS}/modules;${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS};${CMAKE_MODULE_PATH}")
# message("CMAKE_MODULE_PATH = ${CMAKE_MODULE_PATH}") ###


include(add-policies)     # ${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS}/
include(detect-systems)
include(target-dirs)
include(utils)


project(study-cmake
        VERSION 0.3.1.2
        DESCRIPTION "the examples of study-cmake"
        LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# set(CMAKE_CXX_EXTENSIONS OFF)

include(setup-build-env)

set(ARCHIVE_NAME ${CMAKE_PROJECT_NAME}-${PROJECT_VERSION})
set(xVERSION_IN ${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS}/version.h.in)
include(gen-versions)

debug_print_top_vars()


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

debug_print_value(CMAKE_RUNTIME_OUTPUT_DIRECTORY)

当前我们并不对此样本多加解释,今后会在介绍了足够的知识点之后另行介绍它的衍生版本——所谓的 CMake 的最佳实践。

子目录中的 CMakeLists.txt

子目录被用于安排我们的每个子项目。这些子项目中包含着一个或者多个 Targets,当然你也可以让某个子目录中的 CMakeLists.txt 不必编写一个 Target 于其中,这是被允许的。

传统方法

首先的一种方案,可以参考前两篇笔记中的实例。在早前的章节里,我们给出了非常传统的 CMake 脚本编写方法。

一般地,我们的每一个子项目,以 project 开头,以 include_directories, set(cxx_flags) 等配置项继之,后接 add_executable/add_library 声明一个确切的 target,但你也可以声明多个 Targets。

所以一个样本形如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# The muchs library
# set (header_files ${CMAKE_CURRENT_SOURCE_DIR}/include/muchs/muchs.hh)
FILE(GLOB_RECURSE header_files ${CMAKE_CURRENT_SOURCE_DIR}/include/*.hh)
LIST(APPEND source_files library.cc)

# library
add_library(muchs STATIC ${source_files} ${header_files})
target_include_directories(muchs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# The main program

# The sources shared between the main program and the tests
set(PROJECT_SOURCES main.cc)

add_executable(library-1-test-program ${PROJECT_SOURCES})
target_link_libraries(library-1-test-program PRIVATE muchs)

Modern CMake 方法

然而,传统方法自从 cmake 3.0 起就被建议放弃,取而代之的是所谓的 Modern CMake 方法。这种方法中的变化在于不使用诸如 include_directory,set(CXX_FLAGS …) 之类的旧式工具,改而采用 target_compile_featrues,target_sources,target_include_directories,等等新式工具。注意 target_link_libraries 同时适用于两种方法。

Modern CMake 当然不是仅仅包含上述变化,实际上其重点在于思考的视角已经发生了变化:以前的工具虽然也有 target,但却通过全局性质的 include_directories 等等设定来作用于每一个 target,因而你需要小心控制顺序,并通过有限的几个小工具(例如 target_link_directories,target_properties 等等)进行微调;但新版本之后你不应该随时随地地修改全局性的设定,而是面对每个 target 进行具体设置。

简而言之,Modern CMake 更强调每一个 Target 自身的面向对象的性质。

此外,Modern CMake 也会将 Target 细分为几种不同的可见性,例如 PUBLIC,PRIVATE 和 INTERFACE。对于库作者而言,你的库所需要的依赖库通常应该被标记为 PRIVATE,从而令你的使用者不必关心间接的库引用关系。

所以我们会看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cmake_minimum_required(VERSION 3.5)
project(MyLibrary VERSION 1.0.0 LANGUAGES CXX)

find_package(OpenCV REQUIRED)

# A Target:
add_library(MyLibrary)
target_compile_features(MyLibrary PRIVATE cxx_std_11)
target_sources(MyLibrary PRIVATE src/my_library.cpp)
target_include_directories(MyLibrary
        PUBLIC
            $<INSTALL_INTERFACE:include>
            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        PRIVATE
            ${OpenCV_INCLUDE_DIRS}
        )
target_link_libraries(MyLibrary PRIVATE ${OpenCV_LIBRARIES})

进一步的格式

在 Modern 风格中,为了兼顾多种目的,我们推荐的最佳格式是将库放在 libs 子目录之下,从而形成这样的文件夹结构:

image-20201121165541697

需要提示的是,请勿着急,这份相关的源码是开源的,只是我还尚未整理完成,因为后续的 posts 也在计划之中,因此本系列文字的后期才会一并放出。

这种结构,将会使得 sm-lib 被你的 app-auto 直接引用的同时,也能兼顾到分发 sm-lib 之后他人引用之,具体理由在于其 libs::sm-lib 引用语法上,无论是本地还是他人通过 cmake --build build/ --target install 自行安装 sm-lib 都能同样地通过该引用语法透明地寻找到该库。

为了说明这一点,我们的 app-auto 会在我们的开发过程中跟随 sm-lib 同步构建并直接引用到 sm-lib。而 app 不会参与我们开发过程中的构建,而是在 sm-lib 被执行了 –target install 之后单独地进行一次构建,从而引用到你的 cmake 安装目录中已被注册的 sm-lib。

这一部分的细节较多,因而请直接参考源码。

源码释出时,我可能会重写本篇,将这些细节也一一阐述一遍,不过也可能不,因为那样的细节有点枯燥,太考验笔力了未免。

我目前考虑的是是否应该将这组惯用法组织为一个公共函数,或者一个 cli 工具,以帮助你建立这样的代码结构。因为手工组织它实在是太无趣了。

构建方法

现在,针对一个 Target 我们可以这样来构建:

1
2
3
cd my-library
cmake -S . -B build/
cmake --build build/

如你所见,新的风格不再是 mkdir build && cd build && cmake .. && make 了,取而代之的是直接在项目目录中完成构建:

  1. cmake -S . -B build/ 从源代码根目录(-S .)处理 CMakeLists.txt 文件并将构建所需的中间文件写入构建目录(-B build/)中。构建目录将会被自动创建(也可能并不)。

    需要 cmake v3.13 以上版本以支持 -S

    实例中的 -S . 实际上是可以忽略的。

  2. cmake --build build/ 完成相应的构建,其内幕等价于 cd build/ && make

    See also: https://cmake.org/cmake/help/latest/guide/user-interaction/index.html#selecting-a-target

尽管本质没有任何区别,但作为构建者来说,不再需要 cd build && cd .. 这样的无意义的目录切换了,构建者可以从项目根目录开始发起一切动作。

IMPORTANT

-B 并不等于 –build。-B 是在给定的 binary dir 中写入中间文件,–build 是从给定的 binary dir 中执行构建动作(通过 make makefile)。

不要混淆两条命令的用法,严格按照示例的方式进行饮用。

make install

在顺利完成构建之后,我们往往需要将构建结果(通常是库)安装到工作系统中,然后依赖项目才能顺利引用对应的依赖库并完成链接。

在旧式 cmake 中,这是通过 make install 或者 sudo make install 来实现的。

在现代 cmake 中,相应的命令为:

1
2
3
cmake --build build/ --target install
# Or:
cmake --install build/ # CMake 3.15+ only

See also: https://cmake.org/cmake/help/latest/guide/user-interaction/index.html#software-installation

关于构建目标(Target)可以参见:

https://cmake.org/cmake/help/latest/guide/user-interaction/index.html#selecting-a-target

如果你采用了 generator expression,那么 cmake build 可能会生成形如 install/fast 一样的 target,所以这时需要 cmake --build build/ --target install/fast 来调用那样的目标。

make uninstall

要注意的是,cmake 并不提供一个所谓的 uninstall 命令来卸载通过 make install 步骤安装到系统的文件。

你不得不自行查阅 build/install_manifest.txt 并手动删除那些文件。

幸运的是,这并不复杂:

1
$ xargs rm < build/install_manifest.txt

如果 make install 时使用了 sudo 权限,那么卸载时需要稍微注意:

1
2
$ cat build/install_manifest.txt | sudo xargs rm
$ cat build/install_manifest.txt | xargs -L1 dirname | sudo xargs rmdir -p

避免 make install

通过在 app 项目中定义 依赖库的源码中相关路径的方式,你可以不必在构建 library 时执行 make install 的步骤,这在很多情况下是值得推荐的,因其避免了临时性的二进制污染到工作主机系统中。

我们(作为库作者)通常都要注意在开发过程中避免 make install 带来某些难以预料的副作用。

但这并不是绝对的。

假设你是专职的库作者,那么你的库的使用者们将会需要 make install 或者 cmake –target install 等方法来从源码构建你的库到他们的工作空间里。

我们在开源示例中提供了 z11_m1/app 和 z11_m1/app_auto 来分别展示库作者怎样自行构建并依赖于其 library(通过 app_auto),以及库使用者如何通过 cmake 构建并安装的方式去使用 library(通过 app)。

以后我们会看到,使用 cmake 的可下载的特性,我们可以直接包含 github 或其它开源站点的公开的 library 并完成自己的构建,无需手工执行 library 的 cmake --build build/ --target install

辨析

In-source Build vs Out-of-source Build

这两个术语可以参考一下 directory structure - In-Source Build vs. Out-Of-Source Build - Software Engineering Stack Exchange 7 以及 IDisposable Thoughts - CMake and out-of-source build 8

一般来说,在 Source Tree 目录直接使用 cmake && make 就是所谓的 In-source Build 了,此时中间 makefile 输出以及构建时的 .obj 输出都会混杂在源代码及其目录树之中,将会产生污染,且有误冲与覆盖源代码的潜在危险。

Out-of-source Build 通常代表着在整个项目工作区的根目录中新建一个 build 目录,并在该子目录中进行构建。这代表一种模式,但构建目录名却并非只能采用 build 这个单词,你、或者 CI、或者 IDE 可以使用自己的偏好名字。

这往往是通过如下序列搞定的:

1
2
3
4
5
cd my-library
mkdir build && cd build
cmake ..
make
sudo make install

而在新版本 cmake 中,你可以通过 Modern CMake Style 风格的命令来完成:

1
2
3
4
cd my-library
cmake -S . --build build/
cmake --build build/
cmake --build build/ --target install

In-source build 通常是不被允许的。它往往引起潜在问题且污染 Source Tree 之中的内容。而新版 cmake 要求你必须在一个与 source tree 分离的单独的构建目录中进行构建,参考 out-of-source build 以及 build 目录。

IMPORTANT

cmake 会输出 CMakeCache.txt 作为中间文件之一(此外一个 makefile 也会是必然的输出之一)。需要小心的是如果 Source Tree 的根目录中包含一个 CMakeCache.txt 文件的话(无论是不是你曾经误用过 In-source Build 指令),则 cmake 将会总是进入 In-source Build 模式,哪怕你正在采用 mkdir build && cd build && cmake .. 这样的序列尝试 OUt-of-source Build。

Build Requirements vs Usage Requirements

可以参考 cmake 官方文档之 cmake-buildsystem(7) — CMake 3.19.0 Documentation 。 此外 It’s Time To Do CMake Right - Pablo Arias 中有相应的阐述。

综合它们的说明来看,所谓 Usage Requirements,基本上特指 INTERFACE 类型的 Target,这是 cmake 3 以后的一种新的 Target 类型,你可以简单地将其理解为 headers-only 的库,即只有头文件的库,使用者实际上只需要编译时包含之,而无需链接时寻找其 .a 并链入其二进制机器码。

而 Build Requirements(官方文档仅使用了 Build Specification 一词),基本上特指 PRIVATE 类型的 Target,这其实也是 cmake 3 以后的一种新的 Target 类型(因 cmake 2.8 等早期版本中 Target 无所谓类型,倒是 DYNAMIC 和 STATIC 可以勉强被视作是 library Target 的类型),而 PRIVATE Target 表示一个 动态库或静态库目标 A,它如果有进一步所依赖的其它库,则这些库对于 A 的使用者来说其实是不可见的,使用者也无需关心 A 所依赖的其它库应该被如何找到(相应的依赖关系在使用者通过 cmake --target install 安装 A 时已经被透明地解决了)。

引用:

  • Build-Requirements: 包含了所有构建Target必须的材料。如源代码,include路径,预编译命令,链接依赖,编译/链接选项,编译/链接特性等。
  • Usage-Requirements:包含了所有使用Target必须的材料。如源代码,include路径,预编译命令,链接依赖,编译/链接选项,编译/链接特性等。这些往往是当另一个Target需要使用当前target时,必须包含的依赖。

🔚