Why Is C++ Package Management So Hard?
Pick any halfway-decent C++ project at random, and the thing you slam into first is, nine times out of ten, neither the syntax nor the performance. It's — "Shift! Why won't the dependencies run? The build blew up again, and why does it crash the instant it starts?!"
In Python, one pip install and you're done; in Node, npm install and you wait out the progress bar; in Go, go get plus a go mod tidy, crisp and clean; Rust needs no introduction — Cargo practically is the Rust experience, and when you type cargo add you don't even feel that "package management" is a thing that exists.
But the moment you land in C++, the picture changes. So almost everyone who writes C++ sooner or later asks the same question: it's 2026 — why does C++ still not have a unified, smooth, works-out-of-the-box package manager like Cargo, npm, or pip?
It looks like a tooling problem, but dig deep enough and you find it's really a bill that decades of engineering reality in this language have been racking up — and the kind you're asked to settle in one lump sum.
The Hard Part Was Never Pulling the Library Down
When many people hear "package management", the first reaction is "just download the dependencies, right?" A perfectly nice solution — but sadly, speaking as someone who has written C++ from fairly small projects up to fairly large ones, downloading turns out to be the least taxing link in the entire chain. The real trouble only begins the moment the download finishes:
- Whoa, the build blew up — the compiler can't stomach this antique!
- Why can't the linker find the symbol AGAIN???
- How is it crashing again when I run it!
- It's over — the project says the dependency needs upgrading??? And the build blew up again the moment the compiler got upgraded?
I may have just stirred up some less-than-friendly memories for you. Back to the point — in other languages, a package can roughly pass as: a name (what this package is called — just as I'm called CharlieChen114514, and the package I pulled is called fmt), a version number (which software milestone of this library passed testing and review, the point deemed reasonably safe at that stage), and a blob of code that at least looks directly usable (source / bytecode / modules).
And C++? Unfortunately, the scenarios it gets used in tend to face — at a minimum, though not only — these, on top of what I discussed above:
- And you, venerable sir, what build system might that be? I'm CMake, haha, let's shake hands — shake with both hands... What? You're Autotools? You're some kind of Make immortal? You're from the Meson side???
- What do you mean, I'm on gcc 16.1 but the binary you distributed was compiled with gcc 4?
- What do you mean, the two sides used different standard libraries so the symbols can't be found? An ABI mismatch blew my program to pieces?
- What do you mean, the library you distributed is actually a Debug build
- What do you mean, the target platform isn't the same as mine?
- Wait, you developed this back then against a default assumption of Ubuntu 14.04??
See? Because C++ lives right here, it naturally has to face this class of problem head-on: the same fmt, boost, openssl, or protobuf can take a completely different shape the moment you change environments. Is the compiler GCC, Clang, or MSVC? Is the standard library linked against 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 enabled? Which C++ standard is in use? And which version of OpenSSL is sitting on the system? Get any single one of those wrong and the dependency won't connect. Compared with that, I'd honestly rather pray for compile and link errors than get slapped twice across the face by a runtime crash after going live (lessons paid in blood...)
So "installing a package" in C++ was never something a single install package could settle. In my view, it has to at minimum handle a far thornier problem: under the current toolchain, this platform, this set of compile options, and this linking model, reassemble — from source or from prebuilt artifacts — a dependency that your specific project can actually consume.
I trust your head is swimming by now — and that, I believe, is precisely why a genuinely good, usable package manager for C++ is so hard to bring into this world.
So Why Do Python, JS, and Go Look Less Painful
Why do Python, JS, and Go get to feel this smooth? I've never written Go myself, so most of what follows is hearsay — if I'm spouting nonsense, you're welcome to come correct me.
Take Python. A pure-Python package is basically 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, as if nothing were happening. But the moment one foot steps into native-extension territory, the picture flips instantly — with numpy, scipy, opencv-python, pytorch and friends, you're suddenly juggling Python version, platform, CPU architecture, system libraries, CUDA, ABI, and a whole pile more. How does the Python ecosystem carry that load? With wheels: the native complexity gets packaged ahead of time into prebuilt binaries covering an entire matrix of platforms. What you see is a featherlight pip install numpy; what's behind it is a bunch of people who did all the dirty work for you in advance.
JavaScript / TypeScript is the same story. The vast majority of npm packages are just JS files running in the Node.js or browser runtime; packages exchange JS objects, not the memory layout of C++ objects. But once you touch a Node native addon — sharp, sqlite3, canvas, that kind of thing — it's immediately Node ABI, prebuilt packages, compiling on your machine, system libraries, a heap of business. In other words, JS doesn't lack these pitfalls; it's just that most ordinary npm packages never come anywhere near the native boundary.
Go takes a different road (PS: asked other people + an LLM filled in the gaps — caution! Consume with caution!). It feels comfortable because there is one unified official toolchain: Go modules usually enter that same Go build system as source code, and the Go toolchain then compiles and links them uniformly. That skips straight past the whole interrogation you go through in the C++ world — is this library CMake or Meson? GCC or Clang? Linked against libstdc++ or libc++? How do the compile options get passed through? Will find_package actually find it? But once Go reaches for cgo and truly crosses swords into the C/C++ world, system libraries, ABI, cross-compilation, and linker flags all come charging back.
Reading this far, I think we can confirm it: the other languages didn't wipe out the whole native mess — most of their ordinary packages simply never have to walk as far as the native boundary. And C++ has no such boundary to hide behind: from day one, it has kept one foot planted squarely inside native land.
ABI: The Hurdle C++ Package Management Cannot Get Around
My own understanding of ABI is fairly shallow, but here is roughly how I see it: an ABI is a set of secret handshakes that two binaries must jointly observe when calling each other.
An API stays up at the source level. It looks like this — friendly and approachable:
std::string get_name();The ABI is the entire rulebook both calling parties must know without a word being exchanged, once that thing has been compiled into binary:
- Big bro, what's your symbol name once it's been through mangling?
- Where exactly did the arguments get stuffed? And how?
- Where did the return value get stuffed?
- For the standard-library objects in use, are the two sides' byte arrangements identical?
- Virtual functions — how do you each implement them?
- How does an exception get thrown across the boundary from one library into another?
- Can a Debug build and a Release build be linked together, mixed?
- Which C++ standard library is everyone actually linking against? Which libc++ are you?
Get any single one of these rules wrong and you will harvest the most maddening species of bug the C++ world has to offer: it compiles, but the link fails; it links, but it crashes when run; it runs without visibly crashing, yet the memory has already quietly gone bad — until one day you're chasing some inexplicable crash, follow it eight hundred miles out, and only then find the root cause sitting here. By the time I finally tracked one of these down, I was glowing red all over — fully overheated!
Other languages have ABIs too, of course, but they usually don't make ordinary package dependencies face them directly. Python, JS, Java, and C# lean on interpreters, virtual machines, or a unified runtime to keep most package dependencies at the runtime-object or bytecode level; Go and Rust lean more on a unified toolchain that pulls source dependencies into the same build process and compiles them fresh. And C++? It has no unified interpreter, no unified VM, and no unified toolchain or unified build system — so it was born carrying this complexity on its own shoulders.
C++ Has No Cargo — Really, Not for Lack of Trying
Cargo is pleasant to use because the Rust ecosystem — from the language, the compiler, package management, the build system, the crate registry, down to version resolution — is basically one unified, self-consistent worldview. Go is similar: the official toolchain holds strong control over project structure, the module system, and the build process; what it says goes.
But 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 one; it might use CMake, Autotools, Meson, or just a hand-written blob of Makefiles; it might expose itself via pkg-config, hard-depend on whatever OpenSSL happens to be on the system, or simply vendor zlib into its own repository; what it exposes might be a well-behaved C API, or a pile of C++ APIs carrying STL types and templates — and the ABI of the latter is all but destined to be non-portable.
This is the real world a C++ package manager inherits. What it manages is not a clean, unified, freshly erected ecosystem, but a shambles: projects from different eras across decades, with different platform philosophies, different build systems, and different binary constraints, all forcibly crammed into your current project environment.
Can C++20 Modules Be the Savior
At this point you're surely asking: what about C++20 modules? Weren't modules supposed to end header-file hell, massively boost compilation speed, and reshape the dependency-management paradigm? Could they go ahead and rescue package management while they're at it?
The answer is fairly cruel: modules haven't just failed to make package management simpler — they've tossed another bundle of firewood under this pot.
The reason is that a module's dependencies can no longer be settled the way headers were, by the preprocessor's textual expansion. Which module interface an import actually depends on, and in what order, requires the build system to first scan every translation unit, work out what it exports and what it imports, and from that derive a correct topological order for all the compilation jobs. To standardize this business, there is 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.
Can you see where the problem lies? In the header era, C++ "dependencies" could at least pretend to be a text problem; in the modules era, they have flat-out become a topological problem the build system must genuinely understand. And C++, of all things, has no unified build system — every build system differs in how far its modules support goes, how it scans, and how it manages artifacts. The already-fragmented build ecosystem now gets shoved onto the tabletop with nowhere to hide. So modules are a good thing, but they can't save package management. All they do is take the old C++ pain point — "the build system must understand dependencies" — and turn it from implicit into explicit, from soft into hard. You have to face it.
vcpkg, Conan, FetchContent… They're Actually Answering Different Questions
And precisely because of that long chain above, tools like vcpkg, Conan, FetchContent, xmake, system package managers, and vendoring are really not in a simple "who is more advanced than whom" relationship — at bottom, they're answering questions at different levels.
What FetchContent is asking: can I pull this dependency's source straight in and compile it together with my project?
What vcpkg is asking: 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, per triplet, it unifies every dependency into the same static/dynamic choice, the same CRT, and the same linking style. Worry-free; the cost is that flexibility gets pressed down under that mold.
What Conan is asking is a more serious business: can I manage binary packages seriously — even private packages — across a matrix of multiple platforms, multiple compilers, and multiple sets of build options? It lets you specify shared or static, and whether fPIC is on, for each dependency individually, and then nails the ABI down through settings and options — and the price paid for that flexibility is the learning curve and configuration complexity.
System package managers are asking about something else altogether: can this library be uniformly distributed, uniformly upgraded, and uniformly security-patched as part of the whole operating-system ecosystem? And vendoring is another mindset taken to its extreme — I trust no external environment; I nail the dependency's source firmly inside my own repository, and from compiling to maintaining, I own it all the way to the 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 allocated: who exactly are you planning to hand this dependency to? The system distribution? vcpkg or Conan? FetchContent pulling the source and compiling it yourself? Vendoring it pinned inside the repository? CI? Or just tossing it to the board vendor's SDK? Every one of those answers corresponds to a completely different profile of controllability, reproducibility, and long-term maintenance cost. That is the question C++ package management really has to answer.
Move It Onto Embedded and the Complexity Multiplies All Over Again
If all of the above still reads to you as "hmm, that's not so bad", try moving it into the world of embedded Linux, BSPs, and cross-compilation — the complexity can multiply several more times over.
Because here, the problem is no longer just "installing a library". You also have to grind it out with the cross-toolchain, the target CPU architecture, the libc version, the kernel headers, the board vendor's SDK, the root filesystem, Buildroot or Yocto, size trimming, and whether the runtime dependencies are even present on the board — and at the end of it all, the thing still has to reproduce reliably on that board and be debuggable. Fantasizing that one C++ package manager can conquer all of this in a scenario like that is unrealistic.
The more dependable approach is layering: hand system-level dependencies to the distribution, Buildroot, or Yocto; manage the smaller C++ dependencies internal to the project with vendoring, FetchContent, a Conan profile, or manually pinned source. The core goal is not npm-grade silkiness but builds that are controllable, reproducible, and explainable — on the embedded track, those three words are worth far more than "handy".
Tired of Writing — Just a Few Loose Words to End On
After that long, winding lap, we can finally close out the question we opened with.
C++ package management is hard — hard not because of getting dependencies downloaded, but because, once the download finishes, you must do the work of turning that dependency into the exact binary form that fits the current project. And C++ dependencies are, inevitably, always bound hard to a long chain of things: the compiler, the standard library, the compile options, the target platform, the system libraries, the linking model, and the ABI. C++ has almost never unified any of these!
Other languages look comfortable because they either have a unified runtime or a unified toolchain, or because they've simply packed the native-ABI business away into special channels and hidden it. C++, meanwhile, 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 world that is deeply shaped by history, heavily fragmented, and unusually low-level. Good package management for C++ was always going to be an uphill road!