按:
这个系列与 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 的脚本是用这么一种编程结构:
- 有一个根目录以及
CMakeLists.txt
。 - 有若干子目录以及相应的
CMakeLists.txt
,并且被通过 add_subdirectory() 的方式添加到根目录的CMakeLists.txt
的末尾,从而构成一个完整的构建链。
我们通过这样的编程结构来管理一个工作区(Workspace)中的若干子项目(Projects)以及子项目中的若干构建目标(Targets)。
Modern CMake 最著名的是的一个开源书籍:https://cliutils.gitlab.io/modern-cmake/。它由 Henry Schreiner 编写,但也有一些贡献者为其完善。
此外,Youtube 上有几份资源可以康康:
More Modern CMake - Deniz Bahadir - Meeting C++ 20182 - https://www.youtube.com/watch?v=y7ndUhdQuU8
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 子目录之下,从而形成这样的文件夹结构:
需要提示的是,请勿着急,这份相关的源码是开源的,只是我还尚未整理完成,因为后续的 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
了,取而代之的是直接在项目目录中完成构建:
-
cmake -S . -B build/
从源代码根目录(-S .
)处理CMakeLists.txt
文件并将构建所需的中间文件写入构建目录(-B build/
)中。构建目录将会被自动创建(也可能并不)。需要 cmake v3.13 以上版本以支持
-S
实例中的
-S .
实际上是可以忽略的。 -
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时,必须包含的依赖。
🔚
留下评论