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:
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.