提到的:
“THE MOST COMMON C++ TOOLSET”
- VERSION CONTROL SYSTEM
- BUILDING
- PACKAGE MANAGEMENT
这些是 C++ 项目开发中最核心的工具链组成部分。下面我将逐一解释每部分的作用、常见工具,以及它们如何协同构建现代 C++ 项目。
1. VERSION CONTROL SYSTEM(版本控制系统)
用来追踪文件变化、管理协作开发、备份代码历史。
常用工具:
工具 | 简介 |
---|---|
Git | 最主流的分布式版本控制系统,用于本地和远程协作(如 GitHub、GitLab) |
SVN | 老牌集中式版本控制系统,已不太常见 |
最佳实践:
- 用 Git 管理源代码、构建脚本、配置文件
- 使用
.gitignore
排除临时文件、构建目录 - 每个新功能用一个分支(如
feature/foo
) - 使用 Pull Request(PR)做代码审查
2. BUILDING(构建系统)
将
.cpp
、.h
等源文件编译链接成可执行程序或库。
常用工具:
工具 | 简介 |
---|---|
CMake | 最主流跨平台构建系统,支持多编译器、自动生成构建文件(Makefile / Ninja / Visual Studio) |
Make / GNU Make | 老牌 UNIX 构建工具,依赖 Makefile |
Ninja | 更快的现代构建工具,常和 CMake 搭配 |
MSBuild | Windows Visual Studio 的默认构建系统 |
示例:CMake 构建流程
mkdir build
cd build
cmake ..
cmake --build .
3. PACKAGE MANAGEMENT(包管理器)
用来下载、安装、管理 C++ 库和依赖项。解决“手动下载库、设置 include/lib”的麻烦。
常用工具:
工具 | 简介 |
---|---|
vcpkg | 微软出品,支持跨平台,最主流之一,支持 CMake 集成 |
Conan | 更强大,支持版本控制、构建配置、包上传等功能,适合大型项目 |
Hunter / CPM.cmake | CMake 模块级集成的包管理器(更轻量) |
示例:用 vcpkg 安装库
./vcpkg install fmt
cmake .. -DCMAKE_TOOLCHAIN_FILE=path/to/vcpkg.cmake
结合起来:现代 C++ 项目的推荐结构
my_project/
├── CMakeLists.txt # 构建定义
├── src/ # 源文件
├── include/ # 头文件
├── tests/ # 单元测试
├── build/ # 构建输出目录
├── .git/ # Git 仓库
├── .gitignore
└── vcpkg.json / conanfile.txt # 包依赖声明
总结:C++ 常用开发工具集
模块 | 工具 | 说明 |
---|---|---|
版本控制 | Git | 管理代码版本、团队协作 |
构建 | CMake + Ninja/Make/MSBuild | 自动构建项目 |
包管理 | vcpkg / Conan | 管理第三方库依赖 |
编译器 | GCC / Clang / MSVC | 实际进行编译 |
这些工具协同使用,构成了现代 C++ 工程开发的基础工具链。 |
现代 C++ 最常用的开发工具集(THE MOST COMMON C++ TOOLSET)
目前业界主流推荐组合是:
类别 | 工具 | 说明 |
---|---|---|
Version Control | Git | 无可争议的行业标准 |
Building | CMake | 最广泛支持、跨平台、集成多构建系统 |
Package Manager | (暂时无标准) | 生态分裂,但 Conan 是强有力的候选者 |
主要观点解释(你列出的那些 bullet points)
“Please do not partition our C++ development environment even more”
- 意思是 C++ 社区的工具链已经过于碎片化(很多人用不同工具,比如 Makefile、Bazel、vcpkg、手动管理等),不希望再加剧这种分裂。
- 建议尽量统一使用主流工具(Git + CMake + 一个主力包管理器)来提高协作效率和生态整合。
“Other tools may sometimes be better at one aspect but probably are worse at others”
- 意思是:虽然一些构建系统、包管理器在某些方面可能比 CMake/Conan 强(比如速度、配置简洁等),但它们在其它方面(生态支持、可维护性、跨平台能力)可能较差。
- 例如:
Bazel
构建很快,语法简洁,但不易上手、缺少通用库支持。
“Until a strong game-changer appears on the market please use above tools”
- 当前没有出现足够强大的新工具能全面替代 Git、CMake 等。
- 所以除非出现“颠覆性”工具,建议继续使用 目前最成熟、最兼容、最通用的工具集。
Conan:被视为最有希望成为 C++ 的主流包管理器
“Conan is a strong contender to become the C++ Package Manager”
这是对 C++ 生态中长期缺乏官方标准包管理器的回应。
Conan 的优势:
特性 | Conan 说明 |
---|---|
支持版本控制 | 明确依赖版本与构建配置 |
跨平台 / 跨编译器 | 支持 MSVC, GCC, Clang 等多种配置 |
与 CMake 深度集成 | 可以自动生成 CMake 工具链 |
支持私有仓库 | 适合公司/企业内部使用 |
但目前还有一些阻力,比如: |
- 语法复杂度偏高
- 社区生态还不如 npm / pip 成熟
- 学习曲线相对陡峭
总结:你的笔记理解如下
C++ 最主流的开发工具集(当前最佳实践):
- 版本控制:Git
- 构建系统:CMake
- 包管理器:暂无统一标准,但 Conan 是最有力候选
社区呼吁统一工具链,避免碎片化,除非有革命性工具出现,建议使用上述工具。
列出的是 C++ 项目中管理依赖(第三方库)的典型方式,出现在许多大型项目。
这些方法各有优缺点,理解它们可以帮助你选择适合你项目的依赖管理策略。
TYPICAL WAYS TO HANDLE DEPENDENCIES IN C++ PROJECTS
1. external/
或 3rdparty/
目录 + add_subdirectory()
把第三方库的源代码直接复制到你的项目中,然后用
add_subdirectory()
加进 CMake 构建图。
优点:
- 简单、可控,离线也能构建
- 不依赖外部工具
缺点: - 手动更新很麻烦
- 难以维护多个版本
- 侵入式(把别人的代码塞你项目里)
适用: - 小项目
- 学习/教学用途
- 稳定依赖,不常更新
2. Git Submodules + add_subdirectory()
用 Git submodule 管理外部库仓库,引入源码,再通过 CMake 构建。
优点:
- 外部库保持原始结构
- 可以独立更新、checkout 版本
缺点: - Git submodule 本身复杂(init、update、版本同步)
- 依赖项目的 CMake 配置必须支持被嵌入(有些不支持)
适用: - 中等规模项目
- 对版本有控制要求的团队开发
3. ExternalProject_Add()
+ add_subdirectory()
用 CMake 的
ExternalProject_Add()
指定外部源码的下载方式,然后在构建时拉取并集成。
优点:
- 支持自动拉取、构建第三方项目
- 不需要在源码里维护外部库
缺点: - 编译/构建步骤复杂
- 与主项目构建不共享 target,难以链接
- 外部项目的安装方式要求统一
适用: - 大型项目
- 需要自动化集成但不想内嵌源码
4. 预先下载并安装依赖 + find_package()
要求开发者/CI 先安装所有依赖(系统或自定义路径),然后通过
find_package()
找到它们。
优点:
- 符合 CMake 模式,清晰且传统
- 依赖项目和主项目解耦
缺点: - 需要你手动安装依赖(容易出错)
- 跨平台兼容性差(每个平台装法不同)
适用: - 系统库(如 Boost)
- 公司/团队统一开发环境
5. 使用其他语言生态工具(如 Maven、NuGet 等)
使用 Java、.NET 等语言的包管理器(如 NuGet for MSVC,vcpkg 也类似),间接管理 C++ 依赖。
优点:
- 如果已有现成工具链,方便复用
- 在某些平台上更友好(如 NuGet + MSVC)
缺点: - 不通用,跨平台麻烦
- 与 C++ 本身生态割裂
适用: - 混合项目(如 C++/CLI、C++ + .NET)
- Windows-only 项目
6. 使用专门的 C++ 包管理器(如 Conan)
使用 Conan(或 vcpkg 等)来拉取、构建、管理依赖。
优点:
- 管理灵活、支持多版本、平台和编译器
- 可与 CMake 深度集成(Conan + CMake Toolchain)
- 适合企业级使用,支持私有仓库
缺点: - 有学习成本
- 有时包缺失,需要自己构建
适用: - 现代项目
- 多平台构建
- CI/CD 集成
总结对比表
# | 方法 | 控制性 | 易用性 | 可维护性 | 推荐场景 |
---|---|---|---|---|---|
1 | 手动源码 + add_subdirectory() | 高 | 简单 | 差 | 教学、小项目 |
2 | Git submodule + add_subdirectory() | 高 | 中等 | 一般 | 中小型项目 |
3 | ExternalProject_Add() | 中 | 复杂 | 一般 | 自动拉库,隔离构建 |
4 | 系统安装 + find_package() | 低 | 中等 | 差 | 老项目、公司环境固定 |
5 | Maven / NuGet | 低 | 特定平台好用 | 差 | 混合语言项目 |
6 | Conan / vcpkg | 高 | 现代 | 强 | 现代企业级开发 |
问题是关于 使用 add_subdirectory()
管理 C++ 依赖(deps),并配合一个 Mermaid 图 来表达依赖关系:
classDiagramclass LibA class Boost_1_66 class GTest_1_8_0LibA o-- Boost_1_66 : usesLibA ..> GTest_1_8_0 : tests with
你这个 Mermaid 类图很清晰地表达了:
LibA
聚合(o--
)了Boost 1.66
(即LibA
使用 Boost,但不拥有生命周期)LibA
通过实现/依赖(..>
)了GTest 1.8.0
(即LibA
依赖 GTest 来做测试)
关于 add_subdirectory()
用于依赖管理的理解:
1. add_subdirectory()
是 CMake 中的一个命令,用来:
- 把一个子目录(通常包含一个
CMakeLists.txt
)加入当前构建树 - 这样子目录的目标(target)、变量等都会被纳入当前项目的构建管理
2. 对于第三方依赖的处理:
- 如果你把第三方库的源码(比如 GTest)放在你项目某个子目录下(
external/gtest
),并且它有自己的CMakeLists.txt
,你就可以通过add_subdirectory(external/gtest)
来把它编译进来。 - 对于像 GTest 这样原生支持 CMake 的库,
add_subdirectory()
是一种很方便的集成方式。 - 但是对于 Boost 这种库(尤其老版本 1.66),它并不是完整 CMake 项目,通常不适合用
add_subdirectory()
,更常用的是系统安装或通过find_package(Boost)
来使用。
3. 结合你的类图和依赖关系:
LibA
对Boost
是聚合依赖,建议用find_package()
或包管理工具(Conan、vcpkg)来集成 Boost。LibA
对GTest
是测试依赖,如果你放了 GTest 的源码,可以用add_subdirectory()
。
总结
依赖库 | 用 add_subdirectory() 管理吗? | 推荐做法 |
---|---|---|
Boost 1.66 | 否 | 使用 find_package() 或包管理 |
GTest 1.8.0 | 是 | 直接 add_subdirectory() |
以下是基于您提供的图片生成的 Mermaid 类图代码,展示 LibA
和 LibB
及其依赖关系:
这张类图很好地表达了两个库 LibA
和 LibB
与它们依赖的第三方库版本之间的关系。针对你问的“add_subdirectory() FOR DEPS?”,我帮你理解和总结:
你的 Mermaid 类图解释
classDiagramclass LibA class LibB LibA *--> Boost 1.66LibA ..> GTest 1.8.0LibB *--> Boost 1.57LibB --> OpenSSL 1.0.2LibB ..> GTest 1.7.0
classDiagramclass LibA class LibB LibA *--> Boost 1.66LibA ..> GTest 1.8.0LibB *--> Boost 1.57LibB --> OpenSSL 1.0.2LibB ..> GTest 1.7.0
*-->
表示 组合(强拥有),即LibA
和LibB
都强依赖特定版本的 Boost。-->
表示 关联/依赖,LibB
依赖 OpenSSL。..>
表示 实现/测试依赖,LibA
和LibB
都用不同版本的 GTest 测试。
add_subdirectory() 用于依赖管理的理解
什么时候用 add_subdirectory()
?
- 你拥有第三方库的源码,并且该源码是 CMake 项目(带有
CMakeLists.txt
), - 你希望直接把第三方库编译到你的项目里,不用单独安装或包管理,
- 这时可以用
add_subdirectory()
把依赖库源码目录加入构建。
针对你的依赖
依赖 | 是否适合用 add_subdirectory() | 理由 |
---|---|---|
Boost 1.66 | 通常不适合 | Boost 通常是预编译库或系统库,用 find_package() 更合适 |
Boost 1.57 | 同上 | 同上 |
OpenSSL 1.0.2 | 通常不适合 | 也是系统库,或者用包管理安装更方便 |
GTest 1.8.0 | 适合 | GTest 官方支持 CMake,源码常用 add_subdirectory() |
GTest 1.7.0 | 适合 | 同上 |
推荐的依赖管理方式结合你的图
- Boost 和 OpenSSL 用系统安装的库或者包管理器(Conan、vcpkg等),然后用
find_package()
找到它们。 - GTest 放源码子目录,用
add_subdirectory()
编译。
举例说明
# CMakeLists.txt
# 找系统或包管理的 Boost
find_package(Boost 1.66 REQUIRED) # for LibA
find_package(Boost 1.57 REQUIRED) # for LibB
find_package(OpenSSL 1.0.2 REQUIRED)
# 添加 GTest 源码子目录(假设源码在 external/gtest)
add_subdirectory(external/gtest)
# 定义 LibA
add_library(LibA ...)
target_link_libraries(LibA PRIVATE Boost::boost gtest)
# 定义 LibB
add_library(LibB ...)
target_link_libraries(LibB PRIVATE Boost::boost OpenSSL::SSL OpenSSL::Crypto gtest)
总结
- add_subdirectory() 适合用来管理支持 CMake 的第三方库源码(如 GTest)。
- 对于系统或预编译的库(Boost、OpenSSL),用
find_package()
+ 包管理更常见。 - 你的图很好地表达了依赖关系和库版本,结合合适的 以下是基于您提供的图片生成的 Mermaid 类图代码,展示
App
及其依赖关系:
classDiagramclass App class LibA class GTest_Latestclass OpenSSL_1_1_0class LibBApp --> LibAApp --> GTest_LatestApp --> OpenSSL_1_1_0App --> LibBLibA o--> Boost 1.66LibA ..> GTest 1.8.0LibB o--> Boost 1.57LibB --> OpenSSL 1.0.2LibB ..> GTest 1.7.0
完整的示例 CMakeLists.txt
,重点展示 LibA、LibB 和它们的依赖如何链接,以及 App 如何链接 LibA、LibB 和第三方库。
假设目录结构如下:
/project/LibACMakeLists.txt.../LibBCMakeLists.txt.../external/gtestCMakeLists.txtCMakeLists.txt <-- 顶层文件
顶层 CMakeLists.txt 示例
cmake_minimum_required(VERSION 3.15)
project(MyApp)
# 查找系统/包管理的 Boost 和 OpenSSL
find_package(Boost 1.66 REQUIRED) # 用于 LibA
find_package(Boost 1.57 REQUIRED) # 用于 LibB
find_package(OpenSSL 1.1 REQUIRED) # 用于 LibB 和 App
# 添加子目录,构建 LibA, LibB 和 GTest
add_subdirectory(LibA)
add_subdirectory(LibB)
add_subdirectory(external/gtest)
# 构建应用程序
add_executable(App src/main.cpp)
# 链接 App 依赖
target_link_libraries(App PRIVATE LibA LibB OpenSSL::SSL OpenSSL::Crypto gtest)
LibA/CMakeLists.txt 示例
add_library(LibA src/liba.cpp)
# 链接 Boost 1.66(假设 find_package 找到的 target 是 Boost::boost)
target_link_libraries(LibA PUBLIC Boost::boost)
# 添加测试依赖 GTest(假设 GTest target 是 gtest)
target_link_libraries(LibA PRIVATE gtest)
LibB/CMakeLists.txt 示例
add_library(LibB src/libb.cpp)
# 链接 Boost 1.57 和 OpenSSL
target_link_libraries(LibB PUBLIC Boost::boost OpenSSL::SSL OpenSSL::Crypto)
# 添加测试依赖 GTest
target_link_libraries(LibB PRIVATE gtest)
说明
target_link_libraries(LibA PUBLIC Boost::boost)
表示 LibA 对 Boost 的依赖对它的用户(比如 App)也是可见的。target_link_libraries(LibA PRIVATE gtest)
表示 GTest 只在 LibA 内部用于测试,不传播给 App。- 顶层 App 链接了 LibA, LibB,以及 OpenSSL 和 gtest(测试用库)。
这样写,整个依赖链和第三方库都清晰管理,避免重复链接问题。
CMake 不是构建系统,而是构建系统生成器
- CMake 的任务是根据你的配置文件(
CMakeLists.txt
)生成适合你平台的构建系统文件。 - 例如:
- 在 Linux 下,它生成 Makefile 或 Ninja 文件
- 在 Windows 下,它生成 Visual Studio 工程文件
跨平台 C++ 构建生成工具
- 你写的 CMake 配置可以在多平台、多种编译器间复用
- 只需要改动少量参数,方便跨平台开发和构建
CMake 的典型工作流程示例
Linux 下用 GCC
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~/.local # 配置项目
cmake --build . # 编译
ctest -VV # 运行测试
cmake --build . --target install # 安装
Windows 下用 Visual Studio
cmake .. -G "Visual Studio 15 2017 Win64" -DCMAKE_INSTALL_PREFIX=~/.local # 生成 VS 工程
cmake --build . --config Release # 编译 Release 版本
ctest -VV -C Release # 运行测试
cmake --build . --target install --config Release # 安装 Release 版本
总结
- 先用
cmake
命令生成构建系统 - 用
cmake --build
或相应平台的构建工具去构建项目 - 用
ctest
运行测试 - 再用
cmake --build --target install
安装项目
传统用法中管理编译选项(build flags)遇到的问题,并引出现代 CMake 的核心思想。帮你总结理解一下:
传统管理编译选项的问题
- Build flags 不可扩展:
你对某个库或组件设置的公共编译选项(比如-Wall
、-O2
、-std=c++17
)必须一层层向上层传递给依赖它的所有组件和最终应用。
这在大型项目里会很难维护。 - 冲突难解决:
不同目标(target)可能需要不同甚至冲突的编译选项,统一管理非常麻烦。
Modern CMake 的解决方案
- 以 Target(目标)为中心管理属性:
- 目标(target)是指
add_library()
或add_executable()
创建的具体模块。 - 现代 CMake 建议把所有编译选项、包含目录、链接库等信息绑定到对应 target 的属性上。
- 目标(target)是指
- 属性(Properties)管理依赖和编译设置:
- 每个 target 可以独立定义自己的编译选项、包含目录、依赖库等,且可以控制传递规则(
PRIVATE
、PUBLIC
、INTERFACE
)。 - 这样更灵活,也方便依赖管理和维护。
- 每个 target 可以独立定义自己的编译选项、包含目录、依赖库等,且可以控制传递规则(
总结
- 不要用全局变量(如
CMAKE_CXX_FLAGS
)来设置编译选项。 - 要给每个 target 设置自己的编译选项和属性。
- 这样能让项目更模块化、可维护,解决冲突问题。
介绍 CMake 中的 目标类型(target types),也就是 add_executable
和 add_library
支持创建的不同目标类型。帮你梳理理解:
CMake 常见的目标类型(Target Types)
1. 可执行文件 (EXECUTABLE)
add_executable(MyApp main.cpp)
- 生成可执行程序。
- 最终产物是一个程序文件,比如 Linux 下的
MyApp
。
2. 共享库 (SHARED LIBRARIES)
add_library(MySharedLib SHARED lib.cpp)
- 动态链接库,运行时加载。
- Linux 下生成
.so
,Windows 下生成.dll
。
3. 静态库 (STATIC LIBRARIES)
add_library(MyStaticLib STATIC lib.cpp)
- 静态链接库,编译时直接打包进可执行文件。
- Linux 下生成
.a
,Windows 下生成.lib
。
4. 对象库 (OBJECT LIBRARIES)
add_library(MyObjLib OBJECT obj1.cpp obj2.cpp)
- 不生成库文件,生成目标文件集合 (
.o
或.obj
)。 - 这些对象文件可以被其他目标(静态库、共享库、可执行文件)重用。
- 方便代码复用,但不会直接链接。
5. 接口库 (INTERFACE LIBRARIES)
add_library(MyInterfaceLib INTERFACE)
- 不产生任何编译产物(没有源文件)。
- 主要用来传递编译选项、包含目录、编译定义等给依赖它的目标。
- 类似“虚拟目标”,用来管理接口依赖。
6. 别名库 (ALIAS LIBRARIES)
add_library(MyAliasLib ALIAS RealLib)
- 给已有目标起一个别名。
- 方便引用,特别是在大型项目或跨目录使用时。
- 不生成新的目标,只是引用已有的。
7. 导入库 (IMPORTED LIBRARIES)
add_library(ExternalLib IMPORTED GLOBAL)
- 用来声明外部预编译好的库(比如第三方库)。
- 不由本项目构建,而是外部已有。
- 你可以给它设置位置、包含目录、链接选项等,供项目引用。
总结
目标类型 | 是否生成产物 | 用途 |
---|---|---|
EXECUTABLE | 是 | 生成可执行程序 |
SHARED | 是 | 生成动态库 (.so/.dll) |
STATIC | 是 | 生成静态库 (.a/.lib) |
OBJECT | 否 | 生成目标文件集合,用于复用 |
INTERFACE | 否 | 用于传递接口信息,无代码 |
ALIAS | 否 | 给目标起别名 |
IMPORTED | 否 | 声明外部已有的库 |
CMake 中 add_library()
没有指定库类型时的默认行为。我们来详细拆解理解:
add_library()
没有指定类型时怎么办?
如果你写:
add_library(MyLib lib.cpp)
你没有写 STATIC
、SHARED
、OBJECT
、INTERFACE
这些类型,这时 CMake 怎么办?
关键点:取决于变量 BUILD_SHARED_LIBS
- CMake 有一个变量叫
BUILD_SHARED_LIBS
,默认是OFF
。 - 如果你没有显式指定类型,CMake 会这样判断:
|BUILD_SHARED_LIBS
值 | 默认类型 |
| --------------------- | ------------- |
|ON
|SHARED
(共享库) |
|OFF
(默认) |STATIC
(静态库) |
示例
默认情况下(静态库):
# 没有设置 BUILD_SHARED_LIBS
add_library(MyLib lib.cpp) # 实际等价于 add_library(MyLib STATIC lib.cpp)
如果你这样设置:
set(BUILD_SHARED_LIBS ON) # 全局开启共享库构建
add_library(MyLib lib.cpp) # 实际等价于 add_library(MyLib SHARED lib.cpp)
误区
这个行为 只影响没有明确指定类型的库,一旦你写了:
add_library(MyLib STATIC lib.cpp)
那 BUILD_SHARED_LIBS
就不会影响这个库了。
总结
add_library(MyLib lib.cpp)
没有指定类型时,CMake 会参考BUILD_SHARED_LIBS
的值:ON
→ 构建为动态库OFF
→ 构建为静态库(默认)
- 推荐做法:
- 对于每个库都明确指定类型,避免构建行为不一致或依赖外部变量。
CMake 现代用法讲解,核心是理解 target_link_libraries()
的作用域关键字(PRIVATE / PUBLIC / INTERFACE) 以及它们在依赖传播中的含义。
我帮你详细解释一下,确保你理解 现代 CMake 的依赖传播模型(MODULAR DESIGN)。
现代 CMake 的依赖传播方式(自 CMake 2.8.12 起)
target_link_libraries(<target> PRIVATE|PUBLIC|INTERFACE ...)
这是现代 CMake 的核心命令,用于将依赖项添加到一个目标上。它不仅是“链接库”,还能控制依赖是否传递(transitive)。
三种依赖作用域的区别
作用域 | 对我有用?() | 对依赖我的人有用?(dependers) | 用于什么场景? |
---|---|---|---|
PRIVATE | 是的 | 否 | 只在当前目标中使用,比如实现细节库、内部工具 |
PUBLIC | 是的 | 是的 | 当前目标用,依赖它的人也要用,比如头文件库 |
INTERFACE | 否 | 是的 | 自己不使用,仅是为别人传递依赖,比如纯头文件库 |
示例 1:普通库依赖实现细节
target_link_libraries(MyLib PRIVATE zlib)
- MyLib 使用 zlib
- 依赖 MyLib 的其他目标不会知道或需要 zlib
示例 2:MyLib 使用 Boost 的头文件库,且用户也必须包含
target_link_libraries(MyLib PUBLIC Boost::headers)
- MyLib 需要 Boost
- 依赖 MyLib 的其他目标也会自动获得 Boost 的包含路径和编译选项
示例 3:MyHeaderOnlyLib 是一个纯头文件库,自己不编译但别人需要
add_library(MyHeaderOnlyLib INTERFACE)
target_include_directories(MyHeaderOnlyLib INTERFACE include/)
- INTERFACE 库不能编译,但可以给别人传递包含路径
- 所以它适用于 header-only 项目
总结重点句式
- PRIVATE:我用,你别管(不向下传播)
- PUBLIC:我用,你也得用(向下传播)
- INTERFACE:我不用,你用就行(传给 dependers)
最重要的一句话
Modern CMake 是关于 target 和 property 的管理,PUBLIC 和 INTERFACE 是 可传递依赖(transitive dependencies),而 PRIVATE 不是。
现代 CMake 在物理设计(physical design)层面带来的转变,以下是逐条解释,帮助你理解:
PHYSICAL DESIGN: MODERN CMAKE
“More than a linking diagram”
- 现代 CMake 不只是“谁链接谁”的图(如早期 Makefile 那种)
- 它捕捉的是逻辑依赖:谁真的需要谁的头文件、编译选项、定义等
“Logical dependencies included”
- CMake 中的
target_link_libraries()
不只是“链接” - 它还传播头文件路径、宏定义、编译选项等(如果用
PUBLIC
或INTERFACE
)
“PRIVATE and PUBLIC dependencies make a difference”
- 如前所述:
PRIVATE
: 自己用,但不传下去(不会污染依赖方)PUBLIC
: 自己用,也传下去(接口依赖)- 它体现了清晰的模块边界,避免“编译污染”
“Now we see real design problems…”
- 当你采用 Modern CMake 模式后,依赖链就变成了设计表达
- 如果你的模块之间有过度耦合、不清晰的接口,会在构建结构中直接暴露出来
NO CYCLIC PHYSICAL DEPENDENCIES!
物理设计中不允许循环依赖!
- CMake 的依赖结构是有向无环图(DAG)
- A → B → C → A 是非法的
- 如果你在物理层面(模块间)形成循环,说明你的设计本身就有问题
现代 CMake 依赖结构本身能揭示架构缺陷(模块滥用、设计不清晰)
ALIAS TARGETS(别名目标)
add_library(MyLibrary lib_source.cpp)
add_library(MyCompany::MyLibrary ALIAS MyLibrary)
作用:
MyLibrary
是一个普通目标MyCompany::MyLibrary
是一个命名空间别名- 让你的依赖更像现代语言的模块导入,例如 C++ 的
std::vector
好处:
好处 | 说明 |
---|---|
命名空间结构 | 避免全局名字冲突,体现所属(如公司/库) |
更易维护 | 可统一更换内部实现,只要别名不变 |
跨项目清晰 | 适合安装导出的库(比如用 install(EXPORT) 时) |
总结:现代 CMake 带来什么?
特性 | 意义 |
---|---|
目标是核心(Targets) | 不再是全局变量控制一切 |
依赖是语义化的(PUBLIC/PRIVATE/INTERFACE) | 表达真实的逻辑接口依赖 |
没有循环依赖 | 构建系统暴露架构问题 |
别名目标 | 体现模块设计与命名规范 |
现代 CMake 变成了一个模块化构建表达语言,不仅是构建工具,它让你的构建结构直接反映了你的设计结构。 |
内容集中讲的是 Modern CMake 的两大核心高级特性:
一、ALIAS TARGETS(别名目标)
示例:
add_library(MyLibrary lib_source.cpp)
add_library(MyCompany::MyLibrary ALIAS MyLibrary)
target_link_libraries(MyLibraryTestPUBLICMyCompany::MyLibraryGTest::Main
)
目的和优点:
点 | 解释 |
---|---|
模块命名空间 | MyCompany::MyLibrary 表达清晰层级结构,像 std::vector 一样规范 |
统一接口 | 和 find_package() 兼容:像 find_package(fmt) 给你 fmt::fmt ,你也可以提供 MyCompany::MyLibrary |
防止拼写错误 | :: 强制语法,不能用 MyCompany:: 作为目标,这种结构只能别名已有 target,错了就编译错误 |
适用于安装和导出目标 | 更适合跨项目复用,如通过 install(EXPORT ...) 导出别名给下游用 |
二、GENERATOR EXPRESSIONS(生成器表达式)
概念:
- 写在
$<...>
里的表达式,不是立即求值 - 是 在生成构建系统时 由 CMake 评估的
- 常用于按平台、配置、变量值等条件切换行为
BAD 示例(传统方式):
if(CMAKE_BUILD_TYPE STREQUAL DEBUG)target_sources(hello PRIVATE helper_debug.cpp)
else()target_sources(hello PRIVATE helper_release.cpp)
endif()
问题:
CMAKE_BUILD_TYPE
不是可靠来源(不适用于多配置生成器如 Visual Studio)- 无法切换构建类型后自动改变行为(必须重新配置)
GOOD 示例(生成器表达式):
add_executable(hello main.cpp$<IF:$<CONFIG:Debug>:helper_debug.cpp,helper_release.cpp>
)
优点:
CONFIG:Debug
检测构建配置(支持 VS/Xcode 多配置构建)- 表达式在构建系统生成时才判断,不影响配置时的 CMake 脚本流程
- 更可移植、更声明式、更适合现代项目
示例:使用 VERBOSE 控制宏定义
target_compile_definitions(foo PRIVATE"VERBOSITY=$<IF:$<BOOL:${VERBOSE}>,30,10>"
)
- 如果变量
VERBOSE
为真,则定义VERBOSITY=30
,否则是10
- 这样可以在不写 if() 的情况下控制条件编译宏
三、BUILD_INTERFACE vs INSTALL_INTERFACE
用法示例:
target_include_directories(mylibINTERFACE$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>$<INSTALL_INTERFACE:include>
)
表达式 | 用途 |
---|---|
BUILD_INTERFACE | 当前项目被构建时用的路径(如源代码 include/) |
INSTALL_INTERFACE | 安装后导出给别人的路径(如 CMAKE_INSTALL_PREFIX/include ) |
意义: |
- 避免安装目标引用本地源码路径
- 让构建时和安装后的 include 目录各自独立、正确
总结
特性 | 说明 |
---|---|
ALIAS TARGETS | 定义命名空间别名,统一目标命名,便于导出 |
GENERATOR EXPRESSIONS | 生成阶段判断条件,替代 if(),适配多构建配置 |
NEVER USE CMAKE_BUILD_TYPE in if() | 多配置构建器无法使用该变量正确判断 |
BUILD/INSTALL INTERFACE | 精准区分构建时 vs 安装后的接口信息,防止路径污染 |
Modern CMake 的库设计最佳实践(MODERN LIBRARY EXAMPLE) 的详细说明,重点在于:
1. GENERATOR EXPRESSIONS 用于接口路径管理
target_include_directories(Foo PUBLIC$<BUILD_INTERFACE:${Foo_BINARY_DIR}/include>$<BUILD_INTERFACE:${Foo_SOURCE_DIR}/include>$<INSTALL_INTERFACE:include>)
理解:
表达式 | 含义 |
---|---|
$<BUILD_INTERFACE:...> | 表示:构建当前项目时 应该使用的 include 路径(源码路径、构建目录等) |
$<INSTALL_INTERFACE:...> | 表示:安装之后被别人使用时 暴露的路径(一般是 install/include ) |
这是为了解决一个关键问题: |
构建时我可以包含本地源文件夹,但安装后不应该暴露它!
2. MODERN CMake 库项目结构
这是一套推荐写法,是现代 CMake 项目模板的基础。
示例完整解释:
cmake_minimum_required(VERSION 3.8)
project(MyLibrary VERSION 0.0.1) # 避免自定义变量在这里
- 避免在
project()
中添加自定义变量,比如不要写LANGUAGES CXX C
冗余参数。 - 推荐只写名称和版本,简洁明了。
依赖查找:
find_package(Foo 1.0 REQUIRED)
- 使用
Foo::Foo
的方式链接依赖库 - 与
target_link_libraries()
兼容,便于封装模块化依赖
添加库:
add_library(MyLibrary lib_source.cpp lib_source.h include/MyLibrary/lib_header.h)
- 一个真实的源文件库
- 包含
.cpp
和.h
,同时列出头文件方便 IDE 识别
设置 C++ 标准:
target_compile_features(MyLibrary PUBLIC cxx_std_17)
- 设置为 C++17
PUBLIC
表示:依赖MyLibrary
的项目也要使用 C++17
设置头文件路径:
target_include_directories(MyLibrary PUBLIC$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>$<INSTALL_INTERFACE:include>)
- 构建时用项目自身路径
- 安装后只暴露
include/
路径 - 避免把私有源码路径“泄露”出去
链接依赖:
target_link_libraries(MyLibrary PRIVATE Foo::Foo)
PRIVATE
表示:MyLibrary 自己用 Foo,但依赖 MyLibrary 的人不需要关心 Foo
创建别名目标(ALIAS):
add_library(MyCompany::MyLibrary ALIAS MyLibrary)
- 给库设置命名空间别名
- 为安装导出、find_package 使用提供统一接口
3. 避免在 project()
中加自定义变量(推荐写法)
project(MyLibrary VERSION 0.0.1)
- 推荐只传名称 + 版本号
- 不建议在
project()
中混入路径、自定义宏、toolchain 等 - 现代 CMake 项目建议用 目标属性 控制项目行为,而非全局变量
总结:现代 CMake 库设计最佳实践
原则 | 说明 |
---|---|
使用 Targets 管理构建 | 不再用全局变量 |
分清 PRIVATE / PUBLIC / INTERFACE | 明确依赖的传播性 |
用 Generator Expressions 控制构建/安装路径 | 避免路径污染 |
使用 命名空间 ALIAS | 模块化设计,统一接口命名 |
安装友好,find_package 兼容 | 便于重用和分发 |
避免 if(CMAKE_BUILD_TYPE) | 用 $<CONFIG:...> 替代 |
现代 CMake 项目的 文件组织结构(FILES ORGANIZATION),目的是让项目结构清晰、模块解耦、易于维护和安装。它强调将源码、测试和顶层配置分开处理,每一部分职责明确。
我来帮你完整地理解和总结这一结构的设计思想:
典型目录结构(现代 CMake 项目)
MyLibrary/
├── CMakeLists.txt <-- 项目入口(顶层)
├── src/
│ ├── CMakeLists.txt <-- 只负责库构建和安装
│ └── my_library.cpp/hpp <-- 库源码
├── test/
│ ├── CMakeLists.txt <-- 只负责测试目标
│ └── unit_tests.cpp
└── include/└── MyLibrary/└── lib_header.h <-- 公共头文件
各部分职责说明
顶层 CMakeLists.txt
(项目入口)
“Simple project wrapper / Entry point for development”
- 设置项目名称和版本
- 添加编译选项(比如
CMAKE_CXX_STANDARD
、BUILD_SHARED_LIBS
) - 添加子目录:
add_subdirectory(src)
、add_subdirectory(test)
- 不直接定义库或测试逻辑
src/CMakeLists.txt
(只负责库)
“Standalone library definition and installation”
“Does not change compiler’s warnings!”
- 添加库(
add_library(...)
) - 设置接口、包含路径、版本信息、安装规则
- 不负责控制编译警告(应由顶层或 toolchain 设置)
保持纯净,这个 CMake 文件应是可以被其他项目add_subdirectory()
或add_external_project()
使用的!
test/CMakeLists.txt
(只负责测试)
“Standalone unit tests definition”
- 查找或引入测试框架(如 GTest)
- 添加测试目标:
add_executable(MyTests ...)
- 链接库:
target_link_libraries(MyTests MyLibrary GTest::Main)
- 使用
enable_testing()
+add_test()
注册测试
示例:顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyLibrary VERSION 1.0 LANGUAGES CXX)
add_subdirectory(src)
add_subdirectory(test)
示例:src/CMakeLists.txt
add_library(MyLibrary my_library.cpp)
target_include_directories(MyLibraryPUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>$<INSTALL_INTERFACE:include>
)
target_compile_features(MyLibrary PUBLIC cxx_std_17)
# 安装规则
install(TARGETS MyLibrary EXPORT MyLibraryTargets)
install(DIRECTORY ../include/ DESTINATION include)
# 可选:命名空间别名
add_library(MyCompany::MyLibrary ALIAS MyLibrary)
示例:test/CMakeLists.txt
enable_testing()
add_executable(MyLibraryTests unit_tests.cpp)
target_link_libraries(MyLibraryTests PRIVATE MyLibrary GTest::Main)
add_test(NAME UnitTests COMMAND MyLibraryTests)
总结
模块位置 | 作用与原则 |
---|---|
/src | 构建库本身,专注模块定义和安装,不修改全局选项 |
/test | 定义测试目标,只负责测试 |
顶层 CMakeLists | 项目入口,只做配置和引导,不定义目标 |
现代 CMake 鼓励模块化、清晰分工、无全局污染 —— 这也是可维护性和可复用性的关键。
Modern CMake 项目中如何组织“开发入口(wrapper)”与“测试目标”。下面是完整的理解与总结:
WRAPPER: LOCAL DEVELOPMENT ENTRY POINT
这是指顶层 CMakeLists.txt
的职责,它作为 项目开发的入口点,方便本地构建、运行测试、使用 IDE 等。
示例:
cmake_minimum_required(VERSION 3.5)
project(MyLibrary)
# 添加主库代码(不直接定义库)
add_subdirectory(src)
# 添加单元测试(可选)
enable_testing()
add_subdirectory(test)
理解要点:
项目顶层职责 | 描述 |
---|---|
项目入口 | 提供给开发者(或 IDE)一个统一的构建入口 |
集成 src/test 子模块 | add_subdirectory() 将各模块构建图统一 |
不需要安装库也能测试 | 本地开发时不需要 make install ,直接构建测试 |
IDE 兼容性好 | CLion、Visual Studio 可识别该项目入口作为工作空间 |
MODERN LIBRARY USAGE: TESTS
这是你在 /test/CMakeLists.txt
中组织测试的方式。即使库被安装到系统中,测试代码也能独立存在并运行。
示例:test/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(MyLibraryTests)
enable_testing()
find_package(GTest MODULE REQUIRED)
# 如果本地开发,MyLibrary 已存在;如果只在测试项目中构建,需要通过 find_package 找到安装版本
if(NOT TARGET MyCompany::MyLibrary)find_package(MyLibrary CONFIG REQUIRED)
endif()
add_executable(MyLibraryTests tests_source.cpp)
target_link_libraries(MyLibraryTests PRIVATEMyCompany::MyLibraryGTest::Main
)
add_test(NAME MyLibrary.UnitTests COMMAND MyLibraryTests)
理解要点:
点 | 解释 |
---|---|
if(NOT TARGET ...) | 支持两种方式:本地源构建(直接 add_subdirectory(src) )或使用安装包(find_package ) |
add_test() | 让 ctest 可以自动执行测试用例 |
使用命名空间 MyCompany::MyLibrary | 与 find_package() 的 alias 兼容,保持一致接口 |
总结一句话:
现代 CMake 项目将“库构建”、“测试逻辑”和“开发入口”清晰分离,通过顶层 wrapper 汇总,而测试模块具备独立运行能力,无需依赖安装。
构建 & 测试流程简要回顾
mkdir build && cd build
cmake ..
cmake --build .
ctest -V # 运行测试
COMPILED, BUILT, TESTED — Done?
是的,这是现代 CMake 项目开发的基本完整流程:
- 本地构建:
- 测试验证:
- 不需要安装库就能测试:
- 可迁移到 CI/CD:
- 可被
find_package()
使用:
现在正在学习 如何使用 CMake 正确导出(export)你的库接口,也就是说:
让别人可以通过
find_package(MyLibrary)
使用你的库
这正是现代 CMake 项目发布和复用的关键部分。
目标:EXPORT YOUR LIBRARY INTERFACE
你希望做什么?
把 MyLibrary
变成一个可以被别的项目使用的“包”(package):
- 安装
.so
/.a
/.dll
文件 - 安装头文件
- 安装导出目标信息(
MyLibraryTargets.cmake
) - 提供
MyLibraryConfig.cmake
(+版本) - 支持
find_package(MyLibrary CONFIG REQUIRED)
调用
分步解释你笔记中的 CMake 脚本
基础定义
cmake_minimum_required(VERSION 3.8)
project(MyLibrary VERSION 0.0.1)
find_package(Foo 1.0 REQUIRED)
add_library(MyLibrary lib_source.cpp lib_source.h include/MyLibrary/lib_header.h)
这是定义你的库,并链接依赖 Foo(可能是别人的库)
安装目标(库)
install(TARGETS MyLibrary EXPORT MyLibraryTargetsLIBRARY DESTINATION libARCHIVE DESTINATION libRUNTIME DESTINATION binINCLUDES DESTINATION include
)
这一段的意思是安装你的库产物(
.a
,.so
,.dll
)以及包含头文件路径信息。
安装目标导出文件(targets.cmake)
install(EXPORT MyLibraryTargetsDESTINATION lib/cmake/MyLibraryFILE MyLibraryTargets.cmakeNAMESPACE MyCompany::
)
这会生成
MyLibraryTargets.cmake
,它保存MyCompany::MyLibrary
这个 target 的元信息。
依赖方可以通过
find_package(MyLibrary)
加载它。
安装头文件目录
install(DIRECTORY include/MyLibrary DESTINATION include)
把你的公共头文件安装到系统或指定安装目录。
创建 MyLibraryConfigVersion.cmake
include(CMakePackageConfigHelpers)
write_basic_package_version_file(MyLibraryConfigVersion.cmakeCOMPATIBILITY SameMajorVersion
)
自动生成
MyLibraryConfigVersion.cmake
文件,用于支持find_package(MyLibrary VERSION ...)
时的版本检查。
安装配置文件 + 版本文件
install(FILESMyLibraryConfig.cmake${CMAKE_CURRENT_BINARY_DIR}/MyLibraryConfigVersion.cmakeDESTINATION lib/cmake/MyLibrary
)
这一段把你的
*.cmake
文件(配置+版本)放到安装路径下,让find_package()
能找到。
MyLibraryConfig.cmake
的内容(必须手写!)
include(CMakeFindDependencyMacro)
find_dependency(Foo 1.0)
include("${CMAKE_CURRENT_LIST_DIR}/MyLibraryTargets.cmake")
解释:
- 告诉
find_package(MyLibrary)
时:- 先找
Foo
(你的依赖) - 然后加载导出的
MyLibraryTargets.cmake
,即注册MyCompany::MyLibrary
- 先找
总结:完整导出流程
步骤 | 作用 |
---|---|
install(TARGETS ...) | 安装库文件和头文件 |
install(EXPORT ...) | 导出 target 接口 |
write_basic_package_version_file() | 生成版本控制文件 |
install(FILES ...) | 安装配置与版本文件 |
手写 MyLibraryConfig.cmake | 定义依赖和目标注册 |
安装后的 find_package 工作方式
其他项目只需这样:
find_package(MyLibrary CONFIG REQUIRED)
target_link_libraries(app PRIVATE MyCompany::MyLibrary)
即可安全链接、复用你的库(包括传递依赖、编译特性等)。
下面是一个 完整导出式 CMake 项目模板,包含:
- 模块化的项目结构(
src/
、include/
、test/
) - 安装导出支持(
install() + export()
) - 可被
find_package()
使用(包含MyLibraryConfig.cmake
与版本控制) - Modern CMake 实践(target-based,命名空间、INTERFACE 编译特性等)
目录结构
MyLibrary/
├── CMakeLists.txt # 顶层入口(wrapper)
├── include/
│ └── MyLibrary/
│ └── MyLibrary.hpp # 公共头文件
├── src/
│ ├── CMakeLists.txt
│ └── MyLibrary.cpp # 库实现
├── test/
│ ├── CMakeLists.txt
│ └── MyLibraryTests.cpp # 单元测试
├── MyLibraryConfig.cmake # 手写的 config 文件
顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyLibrary VERSION 1.0 LANGUAGES CXX)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加模块
add_subdirectory(src)
add_subdirectory(test)
src/CMakeLists.txt
(定义库 + 安装 + 导出)
add_library(MyLibraryMyLibrary.cpp${CMAKE_CURRENT_SOURCE_DIR}/../include/MyLibrary/MyLibrary.hpp
)
target_include_directories(MyLibraryPUBLIC$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>$<INSTALL_INTERFACE:include>
)
target_compile_features(MyLibrary PUBLIC cxx_std_17)
# 安装目标
install(TARGETS MyLibraryEXPORT MyLibraryTargetsARCHIVE DESTINATION libLIBRARY DESTINATION libRUNTIME DESTINATION binINCLUDES DESTINATION include
)
# 安装头文件
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../include/DESTINATION include)
# 导出 CMake targets
install(EXPORT MyLibraryTargetsFILE MyLibraryTargets.cmakeNAMESPACE MyCompany::DESTINATION lib/cmake/MyLibrary
)
# 生成版本文件
include(CMakePackageConfigHelpers)
write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/MyLibraryConfigVersion.cmake"VERSION ${PROJECT_VERSION}COMPATIBILITY SameMajorVersion
)
# 安装 config & version
install(FILES${CMAKE_CURRENT_LIST_DIR}/../MyLibraryConfig.cmake${CMAKE_CURRENT_BINARY_DIR}/MyLibraryConfigVersion.cmakeDESTINATION lib/cmake/MyLibrary
)
# 添加别名 target
add_library(MyCompany::MyLibrary ALIAS MyLibrary)
MyLibraryConfig.cmake
(手写)
# Optional dependencies here:
# include(CMakeFindDependencyMacro)
# find_dependency(SomeDep REQUIRED)
include("${CMAKE_CURRENT_LIST_DIR}/MyLibraryTargets.cmake")
test/CMakeLists.txt
(本地测试)
enable_testing()
find_package(GTest REQUIRED)
add_executable(MyLibraryTests MyLibraryTests.cpp)
target_link_libraries(MyLibraryTestsPRIVATEMyCompany::MyLibraryGTest::Main
)
add_test(NAME MyLibrary.UnitTests COMMAND MyLibraryTests)
include/MyLibrary/MyLibrary.hpp
#pragma once
namespace MyLibrary {int add(int a, int b);
}
src/MyLibrary.cpp
#include "MyLibrary/MyLibrary.hpp"
namespace MyLibrary {int add(int a, int b) { return a + b; }
}
xiaqiu@xz:~/test/CppCon/day205/code$ ls
CMakeLists.txt MyLibraryConfig.cmake include main.cpp src test
xiaqiu@xz:~/test/CppCon/day205/code$ tree
.
├── CMakeLists.txt
├── MyLibraryConfig.cmake
├── include
│ └── MyLibrary
│ └── MyLibrary.hpp
├── main.cpp
├── src
│ ├── CMakeLists.txt
│ └── MyLibrary.cpp
└── test├── CMakeLists.txt└── MyLibraryTests.cpp
5 directories, 8 files
xiaqiu@xz:~/test/CppCon/day205/code$
构建 + 安装
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=../install
cmake --build .
cmake --install .
下面流程
xiaqiu@xz:~/test/CppCon/day205/code$ mkdir build && cd build
xiaqiu@xz:~/test/CppCon/day205/code/build$ cmake … -DCMAKE_INSTALL_PREFIX=…/install
– The CXX compiler identification is GNU 13.3.0
– Detecting CXX compiler ABI info
– Detecting CXX compiler ABI info - done
– Check for working CXX compiler: /usr/bin/c++ - skipped
– Detecting CXX compile features
– Detecting CXX compile features - done
– Found GTest: /usr/lib/x86_64-linux-gnu/cmake/GTest/GTestConfig.cmake (found version “1.14.0”)
– Configuring done (1.0s)
– Generating done (0.0s)
– Build files have been written to: /home/xiaqiu/test/CppCon/day205/code/build
xiaqiu@xz:~/test/CppCon/day205/code/build$
xiaqiu@xz:~/test/CppCon/day205/code/build$ cmake --build .
[ 25%] Building CXX object src/CMakeFiles/MyLibrary.dir/MyLibrary.cpp.o
[ 50%] Linking CXX static library libMyLibrary.a
[ 50%] Built target MyLibrary
[ 75%] Building CXX object test/CMakeFiles/MyLibraryTests.dir/MyLibraryTests.cpp.o
[100%] Linking CXX executable MyLibraryTests
[100%] Built target MyLibraryTests
xiaqiu@xz:~/test/CppCon/day205/code/build$ cmake --install .
– Install configuration: “”
– Installing: /home/xiaqiu/test/CppCon/day205/code/install/lib/libMyLibrary.a
– Installing: /home/xiaqiu/test/CppCon/day205/code/install/include
– Installing: /home/xiaqiu/test/CppCon/day205/code/install/include/MyLibrary
– Installing: /home/xiaqiu/test/CppCon/day205/code/install/include/MyLibrary/MyLibrary.hpp
– Installing: /home/xiaqiu/test/CppCon/day205/code/install/lib/cmake/MyLibrary/MyLibraryTargets.cmake
– Installing: /home/xiaqiu/test/CppCon/day205/code/install/lib/cmake/MyLibrary/MyLibraryTargets-noconfig.cmake
– Installing: /home/xiaqiu/test/CppCon/day205/code/install/lib/cmake/MyLibrary/MyLibraryConfig.cmake
– Installing: /home/xiaqiu/test/CppCon/day205/code/install/lib/cmake/MyLibrary/MyLibraryConfigVersion.cmake
xiaqiu@xz:~/test/CppCon/day205/code/build$
xiaqiu@xz:~/test/CppCon/day205/code/build$ ls …/install/
include lib
xiaqiu@xz:~/test/CppCon/day205/code/build$ tree …/install/
…/install/
├── include
│ └── MyLibrary
│ └── MyLibrary.hpp
└── lib
├── cmake
│ └── MyLibrary
│ ├── MyLibraryConfig.cmake
│ ├── MyLibraryConfigVersion.cmake
│ ├── MyLibraryTargets-noconfig.cmake
│ └── MyLibraryTargets.cmake
└── libMyLibrary.a
6 directories, 6 files
使用这个库(示例项目)
find_package(MyLibrary CONFIG REQUIRED)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyCompany::MyLibrary)
关于 CMake 和现代 C++ 项目管理的经典思路,尤其是针对依赖管理和构建流程的讨论。总结一下核心要点:
1. Package Testing Workflow
- 先在源码目录单独构建并安装(
cmake .. -DCMAKE_INSTALL_PREFIX=...
+cmake --build . --target install
) - 再在测试目录单独构建,使用已安装的库,运行测试(
ctest
) - 这种方式是纯 CMake 的“安装-再使用”模式
2. Pure CMake Dependencies — The Wrong Way
- 每个库单独构建安装,生成自己的配置文件
- 其他项目用
find_package()
查找已安装的库再链接 - 缺点:
- 多层依赖时,更新很麻烦,要重复手动重新构建和安装
- 多配置(Release/Debug)、多平台支持复杂,易出错
- 依赖版本冲突难解决
- 不能自动下载和管理依赖
3. What We Want?
- 一键构建:一次构建完成所有依赖,避免手动管理
- 智能重用:只重建变更部分,其他部分用缓存或预编译库
- 自动化依赖管理:无需手动下载安装依赖
- 自定义版本支持:可以用自定义版本替代系统版本(比如指定特定版本的 Boost、ZLib)
现代 C++ 依赖管理的解决方案示例
方案 | 特点 | 适用场景 |
---|---|---|
纯 CMake + find_package() | 简单但不适合复杂依赖 | 小型项目 |
FetchContent / ExternalProject | 下载源码,统一构建,易集成 | 中小型项目 |
包管理工具(Conan / vcpkg) | 专业依赖管理,版本控制,二进制缓存 | 大型跨平台项目和团队协作 |
你可以考虑用: |
- FetchContent 实现统一构建依赖,减少手动步骤
- Conan 做专业依赖版本管理,构建缓存和跨项目共享
这两种方法比纯find_package()
+ 安装更适合现代 C++ 项目。
后面Conan包管理可以参考ppt后面内容和Conan官网
https://github.com/CppCon/CppCon2018/blob/master/Presentations/git_cmake_conan_how_to_ship_and_reuse_our_cpp_projects/git_cmake_conan_how_to_ship_and_reuse_our_cpp_projects__mateusz_pusz__cppcon_2018.pdf
https://docs.conan.io/2/introduction.html