vcpkg is a C/C++ package manager from Microsoft. It has two operating modes:
- Classic mode — a global install tree, populated by manual
vcpkg install foocommands. Simple, but not reproducible across machines. - Manifest mode — each project has a
vcpkg.jsondeclaring its dependencies, likepackage.jsonin npm. This is what you want for almost every real project.
This doc focuses on manifest mode and explains the central concept that trips most people up: baselines.
- 1. Setup
- 2. Manifest-mode workflow with CMake
- 3. Baselines (the key concept)
- 4. Version constraints
- 5. Triplets
- 6. Ports and registries
- 7. Common tasks
- 8. Speeding up builds
- 9. Troubleshooting and tips
Add vcpkg as a submodule of your project:
git submodule add https://github.com/microsoft/vcpkg
./vcpkg/bootstrap-vcpkg.sh # macOS / Linux
.\vcpkg\bootstrap-vcpkg.bat # WindowsThe bootstrap script downloads the vcpkg executable. Tell vcpkg where it lives:
export VCPKG_ROOT=$PWD/vcpkg
export PATH=$VCPKG_ROOT:$PATH(Optional) Pick a default triplet so you don't have to type :x64-linux everywhere:
export VCPKG_DEFAULT_TRIPLET=x64-linux # or x64-windows, arm64-osx, etc.Two files in the project root:
vcpkg.json — what your project depends on:
{
"name": "myapp",
"version-string": "1.0.0",
"dependencies": [
"zlib",
"fmt"
],
"builtin-baseline": "d68d1ecd932982ed7ee0cb98d557ef1d52ee9016"
}CMakeLists.txt — point CMake at the vcpkg toolchain:
cmake_minimum_required(VERSION 3.16)
if(NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE
"${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake"
CACHE PATH "vcpkg toolchain")
endif()
project(myapp)
find_package(ZLIB REQUIRED)
find_package(fmt CONFIG REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE ZLIB::ZLIB fmt::fmt)Configure once:
cmake -S . -B build -G "Ninja Multi-Config"vcpkg automatically reads vcpkg.json, downloads and builds the listed dependencies, and exposes them through find_package. No separate vcpkg install step.
A baseline is a git commit SHA from the vcpkg repository. It pins your project to a specific snapshot of every port version that was available at that commit.
Think of it as a lockfile for the entire vcpkg ecosystem — same SHA today and six months from now resolves to the same library versions.
Without one, your build pulls "whatever versions my local vcpkg clone happens to have right now." Two developers on the same project, or your CI vs your laptop, can easily get different library versions and produce different binaries.
With a baseline:
- Reproducibility — same SHA → same versions, always.
- Floor for transitive dependencies — if your direct dep needs
boost, vcpkg picks theboostversion that was canonical at your baseline, not whatever's newest. - Team-wide consistency — every clone resolves identically.
{
"name": "myapp",
"version-string": "1.0.0",
"dependencies": ["zlib"],
"builtin-baseline": "d68d1ecd932982ed7ee0cb98d557ef1d52ee9016"
}What vcpkg does when you build:
- Look up the file
versions/baseline.jsonat commitd68d1ec...in the vcpkg repository. - That file maps every port to its canonical version at that snapshot. The relevant entry might be:
"zlib": { "baseline": "1.3.1", "port-version": 0 }
- vcpkg installs zlib 1.3.1 — period. Doesn't matter what the latest zlib is; doesn't matter when you build.
If you change the baseline SHA to a newer commit, the canonical zlib at that commit might be 1.3.2. The next configure picks up the upgrade. Updating the baseline = upgrading the whole ecosystem in one move.
Inside your vcpkg checkout:
cd vcpkg
git rev-parse HEAD
# d68d1ecd932982ed7ee0cb98d557ef1d52ee9016Paste that into vcpkg.json. Done.
When you want newer library versions:
cd vcpkg
git pull
git rev-parse HEAD # new SHAReplace builtin-baseline in vcpkg.json. Run cmake again — vcpkg will rebuild any dependencies whose versions changed.
- Quick experiments where "latest works for me" is fine.
- Classic mode (no
vcpkg.json).
For anything checked into git, anything in CI, anything on a teammate's machine — always set a baseline.
Baselines give you a floor. Use these to constrain further.
vcpkg models your project as a directed graph: each node is a port, each edge is "depends on." Same shape as a Python import graph or a Maven dependency tree.
How vcpkg builds it:
- Start at your manifest's
dependenciesarray — these are your direct dependencies. - For each direct dep, read that port's own
vcpkg.jsonfrom the vcpkg repo (at your baseline SHA). - Pull in its dependencies. Repeat. Everything reached this way is a transitive dependency.
A small project might look like:
myapp
├── fmt
├── boost-asio
│ ├── boost-system
│ ├── boost-coroutine
│ │ └── boost-context
│ └── boost-config (...and ~30 more boost-* internals)
└── curl
├── openssl
└── zlib
You declared three deps; the resolved graph is much larger. vcpkg depend-info myapp --format=tree prints exactly this.
Things that grow the graph:
- Features —
"curl[openssl,http2]"addsopensslandnghttp2edges thatcurlwithout features wouldn't have. Different feature sets → different graphs. - Host dependencies — tools that run during your build, not in your final binary: protobuf compilers, code generators, CMake helper modules. Marked
"host": truein port manifests. When cross-compiling forarm64-android,protocstill runs onx64-linux— host deps follow the host triplet, target deps follow the target triplet. Effectively two graphs in one resolution. - Platform-conditional deps — a port may pull
pthreadsonly on Linux, ordbghelponly on Windows. The graph shape can differ per triplet.
The graph has no cycles — vcpkg rejects cyclic port definitions outright.
Once the graph is built, vcpkg has to pick one concrete version for every node:
- Walk every node, collect every constraint anyone placed on that port:
- The baseline version from your
builtin-baseline. - Every
version>=from any consumer along the way (yours and transitive). - Any
overridesentry — these jump the line.
- The baseline version from your
- For each port, pick the version:
- If your top-level
overridesnames this port → that exact version wins. Done. - Otherwise, take the maximum among the floors (
version>=and baseline). "Newest only when needed" still applies, but the chosen version must clear every floor.
- If your top-level
- Verify the chosen version exists in the port's history. If not, error.
- Resolve per triplet — host and target deps resolve independently when they differ.
Inspect step 1's collection with vcpkg install --dry-run. Inspect the graph itself with vcpkg depend-info <port>.
"I need at least this version, picked from those available at the baseline":
{
"dependencies": [
{ "name": "zlib", "version>=": "1.3.1" }
],
"builtin-baseline": "d68d1ec..."
}vcpkg picks the lowest version that's >= 1.3.1 AND available in the baseline. There is no version<= — the rule is "newest only when needed."
The 1.2.11#9 syntax means version 1.2.11 with port revision 9 (vcpkg's own packaging-fix counter, independent of upstream).
When version>= won't do (e.g., you need exactly 8.2.0 of VTK, not 8.2.0 or anything newer):
{
"dependencies": ["vtk"],
"overrides": [
{ "name": "vtk", "version": "8.2.0" }
],
"builtin-baseline": "d68d1ec..."
}overrides is a sledgehammer — it can break dependencies that legitimately need a newer version. Use sparingly.
The builtin-baseline is global — one SHA for the whole manifest. But two mechanisms let one package diverge from the rest:
1. overrides for "one package at an exact old version" — most common case. Baseline pulls everything to a recent snapshot; overrides drags one package back:
{
"dependencies": ["fmt", "boost-asio", "vtk"],
"builtin-baseline": "d68d1ec...",
"overrides": [
{ "name": "vtk", "version": "8.2.0" }
]
}fmt and boost-asio resolve at the recent baseline; vtk is forced to 8.2.0. The version just has to exist in the port's history (versions/v-/vtk.json on GitHub) — it doesn't need to be at your baseline.
2. Per-package baseline via a custom registry — when you want "track an older snapshot of the whole vcpkg repo just for this one port" (e.g., VTK 8.2.0 plus its era-appropriate transitive deps). In vcpkg-configuration.json:
{
"default-registry": {
"kind": "builtin",
"baseline": "d68d1ec..."
},
"registries": [
{
"kind": "git",
"repository": "https://github.com/microsoft/vcpkg",
"baseline": "abc1234...",
"packages": ["vtk"]
}
]
}vtk resolves against the old SHA, everything else against the new one. Heavier machinery — reach for it only when overrides isn't enough.
vcpkg installs one version of each port per triplet — same as a system package manager, unlike npm's per-consumer node_modules. So if libA and libB both depend on boost, the graph has only one boost node and they have to agree.
Suppose:
libA/vcpkg.json:{ "dependencies": [{"name": "boost", "version>=": "1.75.0"}] }libB/vcpkg.json:{ "dependencies": [{"name": "boost", "version>=": "1.83.0"}] }- Your baseline ships boost
1.84.
vcpkg collects the constraints on boost: >=1.75, >=1.83, baseline 1.84. It takes the maximum — 1.84 — and installs that. Both libraries compile against 1.84. No conflict. version>= means "newer is fine," and the highest floor satisfies everyone.
Now imagine libA is stricter — it pins boost exactly:
// libA/vcpkg.json
{ "overrides": [{"name": "boost", "version": "1.75.0"}] }Constraints on boost: ==1.75 (libA), >=1.83 (libB). No version satisfies both. vcpkg fails:
error: version conflict:
boost-system@1.75.0 required by libA
boost-system>=1.83.0 required by libB
This trips up everyone coming from npm or cargo, where node_modules/libA/node_modules/boost@1.75/ happily coexists with libB's boost@1.83. Those tools nest dependencies because JavaScript and Rust support per-module namespacing.
C++ does not. Every translation unit that includes <boost/asio.hpp> emits symbols like boost::asio::io_context::run(). Two boost versions in one program → either the linker errors with "multiple definition," or it silently picks one and half your code calls the wrong implementation: mismatched struct layouts, memory corruption, exceptions that can't be caught.
This is the One Definition Rule (ODR) — C++ assumes one definition per symbol across the whole linked program. No package manager can fake its way around it. So vcpkg, like apt or brew, picks one version per port per triplet.
In rough order of pragmatism:
-
Top-level
overridesto force one version. Your project'soverridesoutranks any port's. Forceboost 1.83; libA gets the newer boost it claimed it couldn't handle. Often it just works — boost is mostly ABI-stable across minors, and library authors over-pin out of caution. Test thoroughly. -
Overlay-port the stricter library. Copy
vcpkg/ports/libA/tomy-overlays/libA/, edit its manifest to accept the newer boost, add a patch file if libA's source actually needs adjusting, then build withcmake -S . -B build -DVCPKG_OVERLAY_PORTS=./my-overlays. The "official" fix — patch the constraint locally without forking all of vcpkg. See §6. -
Process boundary. Put libA in its own executable that statically links boost 1.75. Talk to the rest of the app over pipes, sockets, or shared memory. Each process has its own private boost. Heavy but bulletproof.
-
Shared library with hidden symbol visibility. Wrap libA + boost 1.75 in a
.so/.dllbuilt with-fvisibility=hidden, exposing only a C ABI or a PIMPL'd C++ interface. The host links boost 1.83. Works in theory; STL types and C++ exceptions cannot cross the boundary safely. -
Vendor and patch. Copy libA's source into your repo, modify it for the new boost, drop the package dependency entirely.
In practice, options 1 and 2 cover almost every real case. Options 3+ only come up for genuine major-version splits — boost 1.x vs a hypothetical boost 2.x that broke everything.
Some ports expose optional features:
{
"dependencies": [
{ "name": "curl", "features": ["openssl", "http2"] }
]
}Or in classic mode:
vcpkg install curl[openssl,http2]Two directions to navigate, depending on what you know.
Browse versions/baseline.json on GitHub at the SHA you care about — it lists the canonical version of every port at that snapshot. For one port's full history:
versions/o-/opencv4.json — every version of OpenCV ever shipped, with port revisions.
Sometimes you know the version you need and want a baseline that delivers it. Two methods:
Method 1 — pickaxe in your local vcpkg checkout:
cd vcpkg
git log --oneline -S '"baseline": "1.3.1"' -- versions/baseline.jsonThe -S (pickaxe) flag finds commits that added or removed a literal string. The first commit shown is when zlib was bumped to 1.3.1 in the global baseline. Use that commit's SHA — or any later commit before zlib was bumped again. To see the window of validity:
git log --oneline -S '"baseline": "1.3' -- versions/baseline.json | head -20Method 2 — port's history file on GitHub:
Open versions/z-/zlib.json and use GitHub's "Blame" view to find the commit that introduced the "version": "1.3.1" entry. That commit's SHA is a valid baseline.
Method 3 (usually the right answer) — skip baseline hunting, use overrides:
If you just need the version installed and don't care which baseline ships it, pick any recent baseline and pin via overrides:
{
"builtin-baseline": "<recent SHA>",
"overrides": [
{ "name": "zlib", "version": "1.3.1" }
]
}vcpkg fetches that exact version regardless of the baseline, as long as 1.3.1 appears anywhere in versions/z-/zlib.json. This avoids dragging the rest of your dependencies backward to whatever was canonical when zlib 1.3.1 was the baseline.
A triplet describes the full build target: CPU + OS + linkage + runtime. Examples:
| Triplet | Meaning |
|---|---|
x64-linux |
64-bit Linux, dynamic linkage |
x64-windows |
64-bit Windows, dynamic CRT |
x64-windows-static |
64-bit Windows, static CRT |
arm64-osx |
Apple Silicon macOS |
arm64-android |
64-bit Android |
Set globally:
export VCPKG_DEFAULT_TRIPLET=x64-linuxOr per-CMake:
cmake -S . -B build \
-DVCPKG_TARGET_TRIPLET=x64-linux-static \
-DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmakeFull list: ./vcpkg help triplet.
A port is the recipe for a single library: a directory in vcpkg/ports/<name>/ with a portfile.cmake (build recipe), vcpkg.json (metadata), and any patches.
A registry is a collection of ports plus their version history. The default one is the vcpkg repository itself (github.com/microsoft/vcpkg). You can add custom registries (git-based or filesystem-based) for your own internal libraries.
An overlay port is a local directory that shadows an upstream port of the same name. vcpkg looks at your overlay first; only if the port isn't there does it fall back to the builtin port. You change one library locally, the rest of vcpkg keeps working.
Copy vcpkg/ports/<name>/ to my-overlays/<name>/, edit what you need, then point CMake or vcpkg install at the overlay directory:
cmake -S . -B build -DVCPKG_OVERLAY_PORTS=./my-overlays ...
# or:
vcpkg install <name> --overlay-ports=./my-overlaysRecall the diamond conflict from §4 — libA pins boost 1.75, libB needs boost >= 1.83. The cleanest fix is to relax libA's constraint with an overlay port. Three steps:
1. Copy the upstream port:
mkdir -p my-overlays
cp -r vcpkg/ports/libA my-overlays/2. Edit my-overlays/libA/vcpkg.json — change the boost constraint:
{
"name": "liba",
"version": "2.0.0",
"port-version": 1,
"description": "Local overlay: relaxed boost constraint for compat with libB.",
"dependencies": [
{ "name": "boost", "version>=": "1.83.0" }
]
}(Bump port-version so vcpkg's binary cache doesn't reuse a stale build.)
3. Leave my-overlays/libA/portfile.cmake exactly as upstream. A typical portfile downloads the source, configures CMake, installs:
vcpkg_from_github(
OUT_SOURCE_PATH SOURCE_PATH
REPO upstream/libA
REF v2.0.0
SHA512 abc123... # upstream's tarball hash
HEAD_REF main
)
vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}")
vcpkg_cmake_install()
vcpkg_cmake_config_fixup()
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")If libA's source actually doesn't compile against newer boost, add a patch:
# Make your changes in a checkout of libA, then:
git diff > my-overlays/libA/fix-boost-1.83.patch…and reference it from the portfile:
vcpkg_from_github(
...
PATCHES
fix-boost-1.83.patch
)4. Tell CMake about the overlay:
cmake -S . -B build -DVCPKG_OVERLAY_PORTS=./my-overlaysThat's it. Resolution now sees libA wanting boost >= 1.83, libB wanting boost >= 1.83, and both happily resolve to whatever the baseline ships.
- Enabling a build option upstream doesn't expose (
-DVTK_MODULE_ENABLE_*=YESinvcpkg_cmake_configure). - Adding a one-off patch while you wait for an upstream PR to merge.
- Carrying a bleeding-edge version of a port that hasn't been picked up by the central registry yet.
Overlay ports are local-only — they're not checked into the vcpkg repo. Commit my-overlays/ to your project's repo so teammates and CI get the same patched ports.
vcpkg search opencvvcpkg depend-info opencv4
vcpkg depend-info opencv4 --format=tree
vcpkg depend-info opencv4 --format=dot > deps.dot && dot -Tpng deps.dot -o deps.pngvcpkg list./vcpkg integrate install # apply
./vcpkg integrate remove # undovcpkg defaults toward thoroughness — both Debug and Release configurations, every default feature, full source compile from scratch. On heavy ports (LLVM, OpenCV, Boost) that's hours you don't need to spend. The levers below are independent; stack them.
By default each port builds twice: Debug and Release. If you only ship Release:
export VCPKG_BUILD_TYPE=releaseRoughly halves build time. For per-project control without affecting other vcpkg consumers on your machine, drop a custom triplet file at triplets/x64-linux-release.cmake:
include("${VCPKG_ROOT}/triplets/x64-linux.cmake")
set(VCPKG_BUILD_TYPE release)Then point CMake at it:
cmake -S . -B build -DVCPKG_TARGET_TRIPLET=x64-linux-release ...Ports often enable a default feature set you don't need. List a port's features:
vcpkg search opencv4Disable defaults and pick only what you use:
{
"dependencies": [
{
"name": "opencv4",
"default-features": false,
"features": ["core", "imgproc"]
}
]
}"default-features": false is the key — without it, vcpkg adds the upstream's default features on top of what you list.
For Boost, don't depend on the meta-port boost (which pulls all 100+ Boost libraries). Depend only on the sub-libraries you #include:
{
"dependencies": ["boost-asio", "boost-system", "boost-fiber"]
}Same idea for LLVM, Qt, and other mega-libraries: select components, don't take the kitchen sink.
For macOS universal binaries (x64+arm64) or Android multi-ABI builds, vcpkg compiles each port once per architecture. Pin to one:
export VCPKG_DEFAULT_TRIPLET=arm64-osx # not a "universal" tripletFor Android, set ANDROID_ABI to a single value (arm64-v8a) rather than a list. If you genuinely need multiple ABIs, build them in separate output trees so binary caching kicks in per-ABI.
This is the biggest win once your team is past day one. Once a port builds with a given (version, triplet, features, compiler) tuple, vcpkg caches the result. Next time anyone — your CI, a teammate's laptop, this same machine after rm -rf build/ — needs the same tuple, vcpkg unpacks the cache instead of rebuilding from source.
Default cache lives at ~/.cache/vcpkg/archives (Linux/macOS) or %LOCALAPPDATA%\vcpkg\archives (Windows). To share across a team, set VCPKG_BINARY_SOURCES:
# Shared NFS / network mount
export VCPKG_BINARY_SOURCES="clear;files,/mnt/team-cache,readwrite"
# GitHub Actions
export VCPKG_BINARY_SOURCES="clear;x-gha,readwrite"S3, Azure Blob, GCS, and NuGet feeds also work — see the binary caching docs.
The cache key is sensitive to everything that could affect the build — compiler version, triplet, features, port-version. Change any of those and you get a cache miss, which is correct behavior but worth knowing when debugging "why did it rebuild?"
export VCPKG_MAX_CONCURRENCY=$(nproc) # Linux/macOS
set VCPKG_MAX_CONCURRENCY=8 # Windows cmdThe default is conservative on memory-constrained machines. Bump to your core count if you have RAM to spare — figure ~2 GB per concurrent port build as a safe rule, more for LLVM or Qt.
Means the version you requested with version>= doesn't exist at your baseline. Either:
- Bump the baseline to a newer SHA that includes the version, or
- Lower your
version>=constraint, or - Add an
overridesentry (last resort).
Set X_VCPKG_ASSET_SOURCES to redirect downloads through a mirror — useful in restricted networks or for build farms. See the asset caching docs.
- Per-project (manifest mode):
<project>/build/vcpkg_installed/<triplet>/ - Global (classic mode):
$VCPKG_ROOT/installed/<triplet>/
Delete the per-project tree to force a rebuild from scratch.
- vcpkg GitHub
- vcpkg.io/packages — searchable package index
- Manifest mode docs
- Versioning reference
- Triplets reference
- Registries
- versions/baseline.json — current canonical versions of every port