Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion documents/community/incoming/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ documents/community/incoming/my-first-modern-cpp-note.md

## 当前文章

暂无初刊文章。
从下载、ABI、构建系统碎片化到 C++20 modules,讲透为什么 C++ 至今没有一个像 Cargo、npm、pip 那样统一顺手的包管理器。作者 CharlieChen114514。

<ChapterNav variant="sub">
<ChapterLink href="why-cpp-package-manager-hard">为什么 C++ 包管理这么难?</ChapterLink>
</ChapterNav>
138 changes: 138 additions & 0 deletions documents/community/incoming/why-cpp-package-manager-hard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: "为什么 C++ 包管理这么难?"
description: "从下载、ABI、构建系统碎片化到 modules——讲透为什么 C++ 至今没有统一顺手的包管理器"
chapter: 1
order: 1
tags:
- host
- cpp-modern
- intermediate
- 工具链
- CMake
difficulty: intermediate
platform: host
reading_time_minutes: 12
---

# 为什么 C++ 包管理这么难?

我们随便挑一个稍微像样的 C++ 项目,最先撞上的十有八九不是语法,也不是性能,而是——“Shift! 怎么依赖跑不起来。编译又炸了,运行怎么起手崩溃了?!”

在 Python 里,`pip install` 一把梭;在 Node 里,`npm install` 等进度条跑完;到了 Go,`go get` 加一句 `go mod tidy`,干净利落;Rust 更不用说,Cargo 几乎就是 Rust 体验本身,你写 `cargo add` 的时候甚至感觉不到"包管理"这件事的存在。

可一到 C++,画风就变了。于是几乎所有写 C++ 的人,迟早都会问出同一个问题:为什么都 2026 年了,C++ 还没有一个像 Cargo、npm、pip 那样统一、顺手、开箱即用的包管理器?

这看着像个工具问题,但你真往深里挖就会发现,它其实是 C++ 这门语言几十年工程现实攒下来的一笔账——而且是要你一次性结清的那种。

## 难的从来不是把库拽下来

很多人一听"包管理",第一反应都是"把依赖下载下来嘛"。这个解决方案挺好,但是很遗憾,笔者写C++,从稍微小的项目到相对较大的项目而言,发现下载反而是整条链路里最不费劲的一环。真正的麻烦从下载完那一刻才刚刚开始:

- 我去,编译炸了,编译器编译不了这个老古董!
- 链接怎么又找不到符号了???
- 运行的时候怎么有崩溃了啊!
- 完蛋了怎么项目说要升级依赖了???怎么一升级编译器又炸了?

我可能让您回忆起来一些不太友好的记忆了。我们回归正题——在别的语言里,一个包大致可以堪称:名称(这个包叫什么名字,就像我叫CharlieChen114514,我下的包叫fmt),版本号(这个库是什么时候通过了测试和审查,是那个阶段认为较为安全的软件里程碑)和一坨至少看起来能直接用的代码(源码 / 字节码 / 模块)

C++呢?很不幸,他所应用的场景,往往至少但是不限于面对这些场景——除开我上面谈到的,还要有:

- 您老啥构建系统啊,我是CMake哈哈,握握手握握双手。。。啥?你是autotools? 你是Make仙人?你是meson那边的???
- 什么叫我用的 gcc 16.1 但是你分发的二进制是gcc 4编译的?
- 什么叫两边用的标准库不一样导致符号找不到?ABI不一致把我程序干炸了?
- 什么叫你的分发的库居然是Debug库
- 什么叫目标平台跟我的不一样?
- 什么你当时开发默认依赖的ubuntu 14.04??

看到了嘛,C++因为本身就在这里,导致他天然必须直接面对这类问题:同一个 `fmt`、`boost`、`openssl`、`protobuf`,换个环境就可能是完全不同的形态:编译器是 GCC、Clang 还是 MSVC?标准库链接的是 libstdc++、libc++ 还是 MSVC STL?Debug 还是 Release?静态库还是动态库?目标架构是 x86_64、arm64 还是 armhf?跑在 Windows、Linux、macOS 还是某个裁过的嵌入式 Linux?异常和 RTTI 开没开?用的是 C++ 哪个标准?系统里那版 OpenSSL 又是几?——这些问题里随便挑一个答错,依赖就接不上, 比起来笔者还真祈祷他编译和链接报错,而不是上线了送我两个大逼斗运行时崩溃(血的教训。。。

所以 C++ 的"装包"从来不是一句 `install package` 能了事的。笔者认为,他至少要能处理这个更加棘手的问题:在当前这套工具链、这个平台、这组编译选项、这种链接模型下,从源码或预编译产物里,重新拼出一个能被你这个具体工程消费的依赖。

我相信你头大了,这也是笔者认为C++ 真正意义上的,好用的包管理非常难以诞生的原因。

## 那为什么 Python、JS、Go 看着没那么痛?

凭什么 Python、JS、Go 用着就这么顺? 笔者自己没写过go,这些更多是道听途说,如果我在胡说八道,欢迎批评指正。

拿 Python 说,纯 Python 的包基本就是一堆 `.py` 文件,解释器负责执行,包和包之间交换的是运行时对象,不是 native 的二进制布局。所以你 `pip install requests`、`flask`、`pytest` 的时候,丝滑得像没事人一样。但只要一只脚迈进 native extension,画风立刻就变了——`numpy`、`scipy`、`opencv-python`、`pytorch` 这些,马上就是 Python 版本、平台、CPU 架构、系统库、CUDA、ABI 一大堆。那 Python 生态是怎么扛下来的?靠 wheel:那些 native 的复杂度,被人提前打包成了覆盖各种平台矩阵的预编译二进制。你看到的是一句轻飘飘的 `pip install numpy`,背后其实是一群人替你把脏活全干完了。

JavaScript / TypeScript 是同一个故事。绝大多数 npm 包就是 JS 文件,跑在 Node.js 或浏览器运行时里,包之间交换的是 JS 对象,不是 C++ 对象的内存布局。可一旦碰到 Node 的 native addon——`sharp`、`sqlite3`、`canvas` 这种——立刻又是 Node ABI、预编译包、本机编译、系统库一堆事儿。换句话说,JS 也不是没有这些坑,只是普通 npm 包大多压根碰不到 native 这条边界。

Go 走的是另一条路(PS下,问的是别人 + LLM补充,谨慎!谨慎参考!)。它舒服,是因为有一套统一的官方工具链:Go 模块通常以源码形式进入同一个 Go 构建体系,然后由 Go 工具链统一编译、链接。这就直接绕开了 C++ 世界里那一堆拷问——这个库是 CMake 还是 Meson?是 GCC 还是 Clang?链接 libstdc++ 还是 libc++?编译选项怎么传过去?`find_package` 到底找不找得到?但 Go 一旦用上 cgo,真刀真枪地碰进 C/C++ 世界,系统库、ABI、交叉编译、链接参数这些问题又全部杀回来了。

读到这里,我想我们可以确认了:其他语言不是消灭了 native 这堆烂账,而是大多数普通包压根不用走到 native 那条边界上。而 C++ 没有这条边界可以躲——它从第一天起,就一脚踏在 native 里。

## ABI:C++ 包管理绕不过去的那道坎

笔者对ABI的理解比较粗浅,我大致认为:ABI 是二进制之间互相调用时,必须共同遵守的一套暗号。

API 停留在源码层面, 他长这样,很和蔼可亲:

```cpp
std::string get_name();
```

而 ABI 是它被编译成二进制之后,调用双方必须心照神会的整套规矩:

- 大哥,你的mangling出来的符号名是啥啊?
- 你的参数是塞到哪里去了?怎么塞的?
- 返回值赛那里去了?
- 用到的标准库对象,你们双方是不是字节排列一致的?
- 虚函数,你们怎么实现的?
- 异常怎么从一个库跨边界抛到另一个库?
- Debug 编译出来的和 Release 编译出来的能不能混着链?
- 大家到底都链接的是哪个 C++ 标准库? 你是哪个libc++?

这套规矩里只要有一条对不上,你就会收获 C++ 世界里最让人抓狂的一类 bug:编译能过,链接挂了;链接过了,跑起来崩了;跑着看着不崩,内存却已经悄悄坏掉,等你某天排查一个莫名其妙的 crash,一路追到八百里之外才发现根因在这里——笔者的查出来是这类问题的时候,可谓是红光满面——红温完了!

别的语言当然也有 ABI,但通常不会让普通包依赖直接去面对它。Python、JS、Java、C# 靠解释器、虚拟机或统一运行时,把大多数包依赖维持在运行时对象或字节码层面;Go、Rust 则更多依赖统一工具链,把源码依赖拉进同一个构建过程里重新编译一遍。而 C++ 呢——它没有统一解释器,没有统一虚拟机,也没有统一工具链和统一构建系统,所以它天生就要把这套复杂度扛在自己肩上。

## C++ 没有 Cargo,真不是没人做

Rust 的 Cargo 好用,是因为 Rust 生态从语言、编译器、包管理、构建系统、crate registry 到版本解析,基本就是一套统一的、自洽的世界观。Go 也类似,官方工具链对工程结构、模块系统、构建流程有很强的控制力,说一不二。

但 C++ 完全是另一个剧本。

它没有官方包管理器,没有官方构建系统,没有统一 ABI,没有统一的标准库实现,没有统一的工程布局,也没有统一的二进制分发模型。一个库,可能是 header-only,也可能是静态库,还可能是动态库;它可能用 CMake,可能用 Autotools,可能用 Meson,也可能就是一坨手写的 Makefile;它可能靠 pkg-config 暴露自己,可能硬依赖系统里那版 OpenSSL,也可能干脆把 zlib 一起 vendoring 进自己仓库;它对外暴露的可能是规规矩矩的 C API,也可能是一堆带着 STL 类型、带着模板的 C++ API——而后者的 ABI 几乎注定不可移植。

这就是 C++ 包管理器接手的真实世界。它管理的不是一个干净、统一、新建起来的生态,而是一个把几十年里不同历史时期、不同平台哲学、不同构建系统、不同二进制约束的项目,硬塞进你当前工程环境的烂摊子。

## 那 C++20 的 modules,能当救星吗?

讲到这里,你肯定会问:那 C++20 的 modules 呢?模块不是号称要终结头文件地狱、大幅提升编译速度、重塑依赖管理的范式吗?它能不能顺手把包管理这块也一起救了?

答案挺残酷的:modules 不但没有让包管理变简单,反而往这口锅里又添了一把柴。

原因在于,模块的依赖关系不再像头文件那样,靠预处理器的文本展开就能搞定。一个 `import` 到底依赖哪个模块接口、依赖顺序怎么排,必须由构建系统先去**扫描**每个翻译单元,搞清楚它导出了什么、又导入了什么,然后据此给所有编译任务排出一个正确的拓扑顺序。为了把这件事标准化,有了 P1689 这份提案:它规定了一种统一的模块依赖扫描格式,三大编译器(GCC、Clang、MSVC)各自不同程度地实现了它,CMake 到 3.28 才把这个能力稳定下来。

你看出来问题在哪了吗?头文件时代,C++ 的"依赖"至少还能假装是个文本问题;到了 modules 时代,它彻底成了一个**构建系统必须真正理解的拓扑问题**。而 C++ 偏偏又没有一个统一构建系统——每个构建系统对 modules 的支持程度、扫描方式、产物管理都不一样。原本就碎片化的构建生态,这下被顶到了台面上,躲都没处躲。所以 modules 是好事,但它救不了包管理。它只是把"构建系统必须懂依赖"这个 C++ 的老痛点,从隐性的变成了显性的、从软的变成了硬的。你必须面对他。

## vcpkg、Conan、FetchContent……它们其实在回答不同的问题

也正因为上面这一长串,所以 vcpkg、Conan、FetchContent、xmake、系统包管理器、vendoring 这些工具,真不是简单的"谁比谁先进"的关系——它们回答的根本是不同层级的问题。

FetchContent 在问的是:我能不能把这个依赖的源码直接拉进来,和我的项目一起编?

vcpkg 在问的是:我能不能把常见的 C++ 开源库自动拉取、打补丁、编出来,然后比较自然地接进 CMake 或 Visual Studio?它的哲学是简单直接,默认按 triplet把所有依赖统一成同一套静态/动态、同一套 CRT、同一种链接方式,省心,代价是灵活度被这块模板压住了。

Conan 在问的则是更严肃的事:我能不能在多平台、多编译器、多组构建选项的矩阵里,严肃地管理二进制包,甚至私有包?它允许你对每一个依赖单独指定 shared 还是 static、fPIC 开不开,再通过 settings 和 options 把 ABI 钉死——为灵活付出的代价,就是学习曲线和配置复杂度。

系统包管理器问的是另一回事:这个库能不能作为整个操作系统生态的一部分,被统一分发、统一升级、统一打安全补丁?而 vendoring 则是另一种心态的极致——我不信任任何外部环境,我把依赖源码死死钉在自己仓库里,从编译到维护,我自己负责到底。

所以你看, C++ 的包管理从来不是一道工具单选题,而是一道**工程控制权怎么分配**的题:这个依赖,你到底打算交给谁?交给系统发行版?交给 vcpkg 或 Conan?交给 FetchContent 拉源码自己编?交给 vendoring 固定在仓库里?交给 CI?还是干脆甩给板厂那套 SDK?每一个答案,都对应着完全不同的可控性、可复现性和长期维护成本。这才是 C++ 包管理背后真正要回答的问题。

## 一旦搬到嵌入式,复杂度还得再翻几番

如果上面这些你看着还觉得"好像也就那样",那就把它搬到嵌入式 Linux、BSP、交叉编译的场景里看看——复杂度能再翻几番。

因为在这里,问题已经不止是"装一个库"了。你还要跟交叉工具链、目标 CPU 架构、libc 版本、内核头文件、板厂 SDK、根文件系统、Buildroot 或 Yocto、体积裁剪、运行时依赖在不在板子上这些事死磕,最后还要能在那块板子上稳定复现、能调试。在这种场景里幻想一个 C++ 包管理器能包打天下,是不现实的。

更靠谱的做法是分层:系统级的依赖,交给发行版、Buildroot 或 Yocto;项目内部那些小型的 C++ 依赖,可以用 vendoring、FetchContent、Conan profile 或者手动固定源码来管。核心目标不是像 npm 那样丝滑,而是**构建可控、可复现、可解释**——在嵌入式这条赛道上,这三个词比"顺手"值钱得多。

## 写累了,最后随手写点吧~

兜兜转转说了一大圈,我们可以把开头那个问题收个尾了。

C++ 包管理难,难的不是把依赖下载下来,而是下载完之后,要把**这个依赖变成适合当前工程的那个二进制形态**。而 C++ 的依赖,总是不可避免地绑死在编译器、标准库、编译选项、目标平台、系统库、链接方式和 ABI 这一长串东西上。C++几乎没有统一过这些!

别的语言之所以看着舒服,是因为它们要么有统一运行时、要么有统一工具链,要么干脆把 native ABI 这摊事打包藏进了特殊通道里。而 C++ 偏偏没有统一构建系统、没有统一包管理器、没有统一 ABI、也没有统一工程结构,所以它的包管理器,注定要面对一个高度历史化、高度碎片化、又异常底层的世界——想要有好的包管理注定艰难啊!
8 changes: 8 additions & 0 deletions documents/community/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ description: "社区来稿、初刊文章与已审阅收录内容"

社区文章不强制进入主线教程卷。它提供一个更开放的入口:投稿者可以先提交 Markdown,维护者做基础检查后上线展示,再根据讨论和审阅结果决定是否长期收录,或进一步整理进主线章节。

## 最新来稿

社区最新一篇投稿,讲透「为什么 C++ 至今没有一个统一顺手的包管理器」——从下载、ABI、构建系统碎片化一路聊到 C++20 modules。作者 CharlieChen114514。

<ChapterNav variant="main">
<ChapterLink num="1" href="incoming/why-cpp-package-manager-hard">为什么 C++ 包管理这么难?</ChapterLink>
</ChapterNav>

## 内容状态

<ChapterNav variant="main">
Expand Down
6 changes: 5 additions & 1 deletion documents/en/community/incoming/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ We suggest specifying the following at the beginning of the article:

## Current Articles

There are no articles in this first issue yet.
From downloading, ABI, and build-system fragmentation to C++20 modules — why C++ still has no unified, smooth package manager like Cargo, npm, or pip. By CharlieChen114514.

<ChapterNav variant="sub">
<ChapterLink href="why-cpp-package-manager-hard">Why Is C++ Package Management So Hard?</ChapterLink>
</ChapterNav>
Loading
Loading