From c80b37d9dcb237eb4fd939df9d20db95e673b358 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Fri, 26 Jun 2026 20:02:33 +0800 Subject: [PATCH] feat: add a passage of package manager --- documents/community/incoming/index.md | 6 +- .../incoming/why-cpp-package-manager-hard.md | 138 +++++++++++++++++ documents/community/index.md | 8 + documents/en/community/incoming/index.md | 6 +- .../incoming/why-cpp-package-manager-hard.md | 144 ++++++++++++++++++ documents/en/community/index.md | 8 + 6 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 documents/community/incoming/why-cpp-package-manager-hard.md create mode 100644 documents/en/community/incoming/why-cpp-package-manager-hard.md diff --git a/documents/community/incoming/index.md b/documents/community/incoming/index.md index 048b8a023..269cc652c 100644 --- a/documents/community/incoming/index.md +++ b/documents/community/incoming/index.md @@ -38,4 +38,8 @@ documents/community/incoming/my-first-modern-cpp-note.md ## 当前文章 -暂无初刊文章。 +从下载、ABI、构建系统碎片化到 C++20 modules,讲透为什么 C++ 至今没有一个像 Cargo、npm、pip 那样统一顺手的包管理器。作者 CharlieChen114514。 + + + 为什么 C++ 包管理这么难? + diff --git a/documents/community/incoming/why-cpp-package-manager-hard.md b/documents/community/incoming/why-cpp-package-manager-hard.md new file mode 100644 index 000000000..1f52c436c --- /dev/null +++ b/documents/community/incoming/why-cpp-package-manager-hard.md @@ -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、也没有统一工程结构,所以它的包管理器,注定要面对一个高度历史化、高度碎片化、又异常底层的世界——想要有好的包管理注定艰难啊! diff --git a/documents/community/index.md b/documents/community/index.md index 53424ff0a..dc09c50dd 100644 --- a/documents/community/index.md +++ b/documents/community/index.md @@ -9,6 +9,14 @@ description: "社区来稿、初刊文章与已审阅收录内容" 社区文章不强制进入主线教程卷。它提供一个更开放的入口:投稿者可以先提交 Markdown,维护者做基础检查后上线展示,再根据讨论和审阅结果决定是否长期收录,或进一步整理进主线章节。 +## 最新来稿 + +社区最新一篇投稿,讲透「为什么 C++ 至今没有一个统一顺手的包管理器」——从下载、ABI、构建系统碎片化一路聊到 C++20 modules。作者 CharlieChen114514。 + + + 为什么 C++ 包管理这么难? + + ## 内容状态 diff --git a/documents/en/community/incoming/index.md b/documents/en/community/incoming/index.md index 6846e831c..80d3aa2aa 100644 --- a/documents/en/community/incoming/index.md +++ b/documents/en/community/incoming/index.md @@ -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. + + + Why Is C++ Package Management So Hard? + diff --git a/documents/en/community/incoming/why-cpp-package-manager-hard.md b/documents/en/community/incoming/why-cpp-package-manager-hard.md new file mode 100644 index 000000000..e71e01f0b --- /dev/null +++ b/documents/en/community/incoming/why-cpp-package-manager-hard.md @@ -0,0 +1,144 @@ +--- +title: "Why Is C++ Package Management So Hard?" +description: "From downloading, ABI, and build-system fragmentation to modules — why C++ still has no unified, smooth package manager" +chapter: 1 +order: 1 +tags: + - host + - cpp-modern + - intermediate + - 工具链 + - CMake +difficulty: intermediate +platform: host +reading_time_minutes: 12 +translation: + source: documents/community/incoming/why-cpp-package-manager-hard.md + source_hash: 1cdcfa793fa8f714bf954e4b5994227061fa5e60e42ce77c2167595b0f8bff90 + translated_at: '2026-06-26T00:00:00+00:00' + engine: manual + token_count: 4104 +--- + +# Why Is C++ Package Management So Hard? + +Pick any halfway-serious C++ project, and the first thing you'll slam into probably isn't syntax, and it isn't performance. It's — *"WTF, why won't my dependencies build? Why did the compile just blow up? Why does it crash the moment I run it?!"* + +In Python, you just `pip install` and call it done. In Node, you run `npm install` and wait out the progress bar. In Go, `go get` plus a quick `go mod tidy` and you're finished — crisp and clean. Rust barely needs mentioning: Cargo basically *is* the Rust experience; when you type `cargo add`, you don't even register that "package management" is happening. + +But the moment you land in C++, the whole vibe shifts. Sooner or later, pretty much everyone who writes C++ asks the same question: it's 2026 — why doesn't C++ have a unified, smooth, works-out-of-the-box package manager like Cargo, npm, or pip? + +It looks like a tooling problem. But dig a little deeper and you realize it's really a bill that decades of C++ engineering reality have been running up — and it's asking you to settle it all at once. + +## The Hard Part Was Never Downloading the Library + +When most people hear "package management," their first thought is "just download the dependency, right?" That'd be a nice solution. Sadly — and I say this having written C++ for everything from small projects to fairly large ones — downloading is the *easiest* link in the whole chain. The real trouble only begins the moment the download finishes: + +- *Damn, the build blew up — the compiler can't even stomach this dinosaur!* +- *Why can't the linker find that symbol again???* +- *Why is it crashing when I run it?!* +- *Oh no, the project says we need to upgrade the dependency??? And now upgrading the compiler broke the build again?* + +Sorry if that stirred up some less-than-fond memories. Back on track — in most other languages, a "package" is, roughly: a **name** (what the package is called — hi, I'm CharlieChen114514, and the package I just pulled is `fmt`), a **version number** (which milestone of this library made it through testing and review and was deemed safe-ish), and a **blob of code** that, at the very least, *looks* ready to use (source / bytecode / modules). + +And C++? Unfortunately, the situations it gets thrown into mean it has to confront — at minimum — all of the following, on top of everything above: + +- *What build system even is this? I'm CMake, haha, let's shake on it — wait, you're Autotools? You're some kind of Make wizard? You're from the Meson camp???* +- *What do you **mean** I'm on gcc 16.1 but the binary you shipped was compiled with gcc 4?* +- *What do you **mean** we linked against different standard libraries so the symbols are gone? An ABI mismatch nuked my program?* +- *What do you **mean** the library you distributed is a Debug build?* +- *What do you **mean** the target platform doesn't match mine?* +- *Wait, you developed this assuming Ubuntu 14.04??* + +See what I mean? Because C++ lives right here, it has to face these problems head-on. The same `fmt`, `boost`, `openssl`, or `protobuf` can be a completely different beast the moment you change environments: is the compiler GCC, Clang, or MSVC? Is the standard library libstdc++, libc++, or the MSVC STL? Debug or Release? Static or dynamic? Is the target architecture x86_64, arm64, or armhf? Is it running on Windows, Linux, macOS, or some stripped-down embedded Linux? Are exceptions and RTTI on? Which C++ standard is it built against? And which version of OpenSSL happens to be on the system? Get any one of those wrong and the dependency won't link. Honestly, I'd rather pray for compile and link errors than have my production app slap me with two runtime crashes out of nowhere (lessons written in blood...). + +So "installing a package" in C++ is never just an `install package` and done. At minimum, it has to handle a far thornier problem: given *this* toolchain, *this* platform, *this* set of compile options, and *this* linking model, reassemble something — from source or from prebuilt artifacts — that your specific project can actually consume. + +Your head's probably spinning. And that, I believe, is exactly why a genuinely good package manager for C++ is so hard to birth. + +## So Why Do Python, JS, and Go Feel Less Painful? + +Why do Python, JS, and Go feel so smooth by comparison? Full disclosure: I haven't written Go myself, so what follows about it is largely hearsay — if I'm talking nonsense, please come correct me. + +Take Python. Pure-Python packages are basically just a pile of `.py` files that the interpreter executes; packages exchange runtime objects with each other, not native binary layouts. So when you `pip install requests`, `flask`, or `pytest`, it's frictionless. But the instant you step into native extensions — `numpy`, `scipy`, `opencv-python`, `pytorch` — you're suddenly juggling Python version, platform, CPU architecture, system libraries, CUDA, ABI, and more. How does the Python ecosystem survive this? Through **wheels**: all that native complexity gets pre-packaged into prebuilt binaries covering a whole matrix of platforms. What you see is a breezy `pip install numpy`; what's actually happening is a bunch of people did all the dirty work for you ahead of time. + +JavaScript / TypeScript is the same story. The vast majority of npm packages are just JS files running on the Node.js or browser runtime; packages exchange JS objects, not the memory layout of C++ objects. But the moment you touch a Node native addon — `sharp`, `sqlite3`, `canvas`, that sort of thing — it's right back to Node ABI, prebuilt binaries, compiling locally, system libraries, and a whole pile of headaches. In other words, JS isn't free of these traps; it's just that most ordinary npm packages never have to cross that native boundary. + +Go takes a different route *(side note: this part is based on asking others + an LLM filling in gaps — read with caution!)*. It feels comfortable because there's a unified, official toolchain: Go modules typically enter the same Go build system as source, and the Go toolchain compiles and links them uniformly. That sidesteps the whole interrogation you go through in the C++ world — is this library built with CMake or Meson? GCC or Clang? Linked against libstdc++ or libc++? How do I pass the compile options through? Will `find_package` even find it? But the moment Go uses **cgo** and actually steps into C/C++ territory, system libraries, ABI, cross-compilation, and linker flags all come rushing back. + +By now, I think we can confirm something: other languages didn't eliminate the native mess — most of their ordinary packages simply never have to reach the native boundary. C++ has no such boundary to hide behind. From day one, it's planted one foot squarely in native territory. + +## ABI: The Hurdle C++ Package Management Can't Sidestep + +My understanding of ABI is fairly shallow, but roughly: **ABI is a set of shared conventions that two pieces of binary must agree on to call into each other.** + +An API lives at the source level. It looks like this — friendly and approachable: + +```cpp +std::string get_name(); +``` + +But the ABI is the full set of rules the two sides have to silently agree on once it's compiled into binary: + +- *Hey, what's the mangled name of your symbol?* +- *Where are the arguments getting stuffed, and how?* +- *Where does the return value get passed back?* +- *For the standard-library objects you use, is the byte layout identical on both sides?* +- *How do you lay out virtual functions / vtables?* +- *How does an exception propagate across the boundary from one library into another?* +- *Can a Debug build and a Release build be linked together?* +- *Which C++ standard library is everyone actually linking against? Which libc++ are you?* + +Get any one of these wrong and you earn the single most maddening class of bug in the C++ world: it compiles, but the link fails; it links, but it crashes when you run it; it runs without crashing, but memory is quietly corrupted — and then one day you're tracking down some inexplicable crash, chasing it eight hundred miles away, only to find the root cause was right here. When I finally traced one of these down, I'm pretty sure I was literally glowing red — full rage mode. + +Other languages have ABIs too, of course, but they usually don't make ordinary package dependencies confront them directly. Python, JS, Java, and C# lean on interpreters, VMs, or unified runtimes to keep most package dependencies at the runtime-object or bytecode level; Go and Rust rely more on a unified toolchain that pulls source dependencies into a single build process and recompiles them. And C++? It has no unified interpreter, no unified VM, no unified toolchain, and no unified build system — so it was born carrying this complexity on its own shoulders. + +## C++ Has No Cargo — and It's Not for Lack of Trying + +Cargo works so well because the Rust ecosystem — the language, the compiler, package management, the build system, the crate registry, version resolution — is basically one unified, self-consistent worldview. Go is similar: the official toolchain has strong, top-down control over project structure, the module system, and the build process. What it says, goes. + +C++ is a completely different script. + +It has no official package manager, no official build system, no unified ABI, no unified standard-library implementation, no unified project layout, and no unified binary-distribution model. A library might be header-only, a static library, or a dynamic library; it might use CMake, Autotools, Meson, or just a hand-written pile of Makefiles; it might expose itself via pkg-config, hard-depend on whatever OpenSSL is on the system, or just vendor `zlib` straight into its own repo; its public interface might be a clean C API, or it might be a bunch of C++ APIs that leak STL types and templates — and the ABI of the latter is all but guaranteed to be non-portable. + +This is the real world a C++ package manager inherits. It's not managing a clean, unified, freshly-built ecosystem. It's managing the mess left behind by decades of projects from different eras, with different platform philosophies, different build systems, and different binary constraints — all crammed into your current project environment. + +## Can C++20 Modules Save the Day? + +At this point you're surely wondering: what about C++20 modules? Weren't modules supposed to end header hell, massively speed up compilation, and reshape the dependency-management paradigm? Could they rescue package management while they're at it? + +The answer is kind of brutal: modules didn't make package management easier. They threw more fuel on the fire. + +The reason is that a module's dependencies can no longer be resolved by textual preprocessor expansion the way headers can. Figuring out which module interface an `import` depends on, and in what order, requires the build system to first **scan** every translation unit — to learn what it exports and what it imports — and then derive a correct topological order for all the compilation jobs. To standardize this, there's proposal **P1689**: it specifies a unified module-dependency scanning format, which the three major compilers (GCC, Clang, MSVC) each implement to varying degrees. CMake didn't stabilize this capability until 3.28. + +See the problem? In the header era, C++ "dependencies" could at least pretend to be a text problem. In the modules era, they're unambiguously a **topological problem** the build system has to genuinely understand. And C++ just happens to lack a unified build system — every build system supports modules to a different degree, scans differently, and manages artifacts differently. The already-fragmented build ecosystem gets dragged out into the open, with nowhere to hide. So modules are a good thing — but they can't save package management. They just take *"the build system must understand dependencies,"* an old C++ pain point, and turn it from implicit to explicit, from soft to hard. You have to face it. + +## vcpkg, Conan, FetchContent… They're Answering Different Questions + +And precisely because of everything above, tools like vcpkg, Conan, FetchContent, xmake, system package managers, and vendoring aren't really in a "which one is more advanced" relationship. They're answering questions at fundamentally different levels. + +**FetchContent** asks: can I just pull this dependency's source straight in and compile it together with my project? + +**vcpkg** asks: can I automatically fetch, patch, and build common open-source C++ libraries, then plug them fairly naturally into CMake or Visual Studio? Its philosophy is simple and direct — by default it uses **triplets** to unify all dependencies into the same static/dynamic choice, the same CRT, and the same linking model. Convenient; the cost is that flexibility gets squeezed by that template. + +**Conan** asks something more serious: can I manage binary packages — even private ones — rigorously across a matrix of multiple platforms, compilers, and build options? It lets you specify shared vs. static, or whether fPIC is on, for each dependency individually, and then nails down the ABI through settings and options. The price of that flexibility is a steeper learning curve and more configuration complexity. + +**System package managers** ask something else entirely: can this library be distributed, upgraded, and security-patched as part of the whole OS ecosystem? And **vendoring** is the extreme end of another mindset — I don't trust any external environment, so I pin the dependency's source firmly inside my own repo and own everything, from compilation to maintenance, end to end. + +So you see, package management in C++ was never a multiple-choice question about tools. It's a question about how **engineering control** gets distributed: who are you handing this dependency to? The system distro? vcpkg or Conan? FetchContent pulling source and compiling it yourself? Vendoring it pinned in the repo? CI? Or just tossing it to the board vendor's SDK? Each answer comes with a completely different trade-off in controllability, reproducibility, and long-term maintenance cost. *That* is the real question behind C++ package management. + +## Move to Embedded, and the Complexity Multiplies + +If all of the above still feels like "eh, that's not so bad," try moving it into embedded Linux, BSPs, and cross-compilation — the complexity multiplies. + +Because here, the problem is no longer just "install a library." You're wrestling with cross-toolchains, target CPU architecture, the libc version, kernel headers, the board vendor's SDK, the root filesystem, Buildroot or Yocto, binary-size constraints, whether the runtime dependencies are even present on the board — and on top of all that, it has to reproduce reliably *on that board* and be debuggable. Fantasizing that a single C++ package manager can handle all of this is unrealistic. + +The more sensible approach is to **layer**: hand system-level dependencies to the distro, Buildroot, or Yocto; manage the smaller, project-internal C++ dependencies with vendoring, FetchContent, a Conan profile, or manually pinned source. The goal isn't npm-level smoothness. It's builds that are **controllable, reproducible, and explainable** — and on the embedded track, those three words are worth far more than "convenient." + +## Wrapping Up + +Having gone round and round, we can finally close out the question from the beginning. + +C++ package management is hard — but not because of downloading dependencies. It's hard because, once the download finishes, you have to turn that dependency into the *exact binary form* your current project needs. And C++ dependencies are invariably bound to a long chain of things: compiler, standard library, compile options, target platform, system libraries, linking model, and ABI. C++ has almost never unified any of these. + +Other languages look comfortable because they either have a unified runtime, a unified toolchain, or they've simply packed the native-ABI mess into a special channel and hidden it away. C++ has neither a unified build system, nor a unified package manager, nor a unified ABI, nor a unified project structure. So its package manager is destined to face a deeply historical, deeply fragmented, and unusually low-level world. A good package manager for C++ was always going to be a hard road. diff --git a/documents/en/community/index.md b/documents/en/community/index.md index 6697be7a5..9539b8041 100644 --- a/documents/en/community/index.md +++ b/documents/en/community/index.md @@ -14,6 +14,14 @@ This section hosts articles, notes, source code readings, engineering experience Community articles are not automatically merged into the main tutorial volumes. This section provides a more open entry point: contributors can submit Markdown files, maintainers will perform a basic review before publishing them for display, and then decide whether to include them permanently or integrate them into main chapters based on discussion and feedback. +## Latest Submission + +The newest community submission — a thorough take on why C++ still has no unified, smooth package manager, walking from downloading, ABI, and build-system fragmentation all the way to C++20 modules. By CharlieChen114514. + + + Why Is C++ Package Management So Hard? + + ## Content Status