12

cmake 03 - Modern CMake Style

 3 years ago
source link: https://hedzr.github.io/cmake/notes/cmake-03/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

现代编程结构

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

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

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

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

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

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

  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++ 2018- https://www.youtube.com/watch?v=y7ndUhdQuU8

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

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

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

顶级 CMakeLists.txt

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

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

一个样本是:

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。

所以一个样本形如这样:

# 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,从而令你的使用者不必关心间接的库引用关系。

所以我们会看到:

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 子目录之下,从而形成这样的文件夹结构:

6otwSMJn2eIDaRE.png

需要提示的是,请勿着急,这份相关的源码是开源的,只是我还尚未整理完成,因为后续的 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 我们可以这样来构建:

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 中,相应的命令为:

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 并手动删除那些文件。

幸运的是,这并不复杂:

$ xargs rm < build/install_manifest.txt

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

$ 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 以及 IDisposable Thoughts - CMake and out-of-source build

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

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

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

cd my-library
mkdir build && cd build
cmake ..
make
sudo make install

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

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时,必须包含的依赖。

:end:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK