vcpkg Commands and Versioning
Learn the day-to-day commands for managing dependencies with vcpkg and how to guarantee reproducible builds using baselines and version overrides
In the last lesson, we integrated vcpkg into our CMake project. By creating a simple vcpkg.json
manifest and pointing our toolchain file, we were able to have find_package()
automatically download and build our dependencies.
This is just the surface of what vcpkg can do. In this lesson, we'll explore the vcpkg command-line tool itself and the manifest features that give us fine-grained control over our dependencies.
We'll learn how to manage our manifest, search for libraries, and, most importantly, lock down dependency versions to ensure our builds are perfectly reproducible every time.
Using vcpkg
Commands
Aside from just fetching dependencies, the vcpkg
tool itself has a suite of commands that can be helpful for managing projects. For example, it allows us to easy add a dependency to our manifest using a command like vcpkg add spdlog
.
To use this effectively, we should update our system to allow us to access vcpkg
from any directory, not just the directory in which it was installed. We do this by adding the vcpkg
installation location to our system's PATH
environment variable.
For Windows
- In the Start Menu, search for "Edit the system environment variables" and open it.
- In the System Properties window that appears, click the "Environment Variables..." button.
- In the bottom section, "System variables", find the
Path
variable, select it, and click "Edit...". - Click "New", and paste in the full path to your vcpkg installation directory (e.g.,
C:\tools\vcpkg
). - Click OK on all the open windows to save the changes.
- You must close and reopen any open command prompts or terminals for the change to take effect.
For macOS and Linux
The process involves editing your shell's configuration file. First, determine your shell by running echo $SHELL
.
Then we open the appropriate configuration file in a text editor. This is typically ~/.zshrc
for zsh (the default on modern macOS) or ~/.bashrc
for bash.
Add the following line to the end of the file, replacing ~/vcpkg
with the actual path where you installed vcpkg.
export PATH="$PATH:~/vcpkg"
Save the file. To apply the changes, either restart your terminal or run source ~/.zshrc
(or source ~/.bashrc
).
For MSYS2
The process is the same as for Linux. From within the MSYS2 terminal, edit your ~/.bashrc
file and add the line:
export PATH="$PATH:/path/to/your/vcpkg/installation"
Adding Packages
We can confirm that vcpkg has been added to our path by running the following command from any directory:
vcpkg --version
If it worked, we should see vcpkg respond with its version:
vcpkg package management program version 2025-07-21
With vcpkg added to our path, we can now run vcpkg
commands anywhere.
For example, we can quickly add a package to our project using the vcpkg add SomePackage
command.
Let's add tabulate to our project. This is a library to make creating tables easier. From within our project directory, we can do that using:
vcpkg add port tabulate
Succeeded in adding ports to vcpkg.json file.
The "port" keyword is essentially what vcpkg calls a package definition.
Many of vcpkg's commands, including add
, will search upwards for the closest manifest file (vcpkg.json
) so running this from our /build
directory or any other directory within our project will work as expected.
It will find and update the vcpkg.json
in our project root which, if we now check, we should see tabulate
added to the dependencies
array.
vcpkg.json
{
"dependencies": [
"spdlog",
"tabulate"
]
}
We can now find and link it in our app/CMakeLists.txt
file in the usual way:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
find_package(spdlog CONFIG REQUIRED)
find_package(tabulate CONFIG REQUIRED)
target_link_libraries(GreeterApp
PRIVATE
GreeterLib
spdlog::spdlog
tabulate::tabulate
)
A basic table can be created like this:
app/src/main.cpp
#include <iostream>
#include <greeter/Greeter.h>
#include <spdlog/spdlog.h>
#include <tabulate/table.hpp> // <h>
int main() {
spdlog::info(get_greeting());
tabulate::Table Greetings;
Greetings.format().locale("C");
Greetings.add_row({"Language", "Greeting"});
Greetings.add_row({"English", "Hello, World!"});
Greetings.add_row({"German", "Hallo, Welt!"});
Greetings.add_row({"French", "Bonjour, le Monde!"});
std::cout << Greetings;
return 0;
}
[2025-08-14 14:49:19.095] [info] Hello from the modular greeter library!
+----------+--------------------+
| Language | Greeting |
+----------+--------------------+
| English | Hello, World! |
+----------+--------------------+
| German | Hallo, Welt! |
+----------+--------------------+
| French | Bonjour, le Monde! |
+----------+--------------------+
Ensuring Repeatable Builds
We've solved the speed and setup problems, but what about reproducibility? How do we ensure every developer on the team or everyone we share our project with uses the exact same version of dependencies that we're using?
Registry Versioning
Currently, we're not specifying what versions of spd
and tabulate
our project wants to use. We're just letting vcpkg decide.
This is a problem for reproducibility. When we or someone else rebuilds the project later, the vcpkg registry may have been updated to change what version of these libraries is should install by default. This may, in turn, change or break the behaviour of our overall program.
However, this process of letting vcpkg decide the versions also presents an interesting opportunity to quickly implement reproducibility. Rather than specifying the version of each individual library, we can instead specify the "version" of the vcpkg registry itself.
If someone builds our project with the same version of the registry, that means the version that vcpkg selects will be exactly the same across both builds. To implement this, we just need to specify what version of the registry we want to use. In vcpkg terminology, this is called the baseline.
Adding a Baseline
vcpkg tracks the baseline version using a "builtin-baseline"
value that we can add to our vcpkg.json
manifest. To add an initial value, we can run the following command:
vcpkg x-update-baseline --add-initial-baseline
updated registry 'https://github.com/microsoft/vcpkg'
If we now look in our vcpkg.json
, we should see this recorded:
vcpkg.json
{
"dependencies": [
"spdlog",
"tabulate"
],
"builtin-baseline": "44a1e4eca5211434b5fefcc25a69bba246c3f861"
}
On any future build of our software, vcpkg will see this builtin-baseline
and install the same versions of our dependencies that we have right now, even if the registry has been updated with newer versions since.
For software we're maintaining over time, we should occassionally update our baseline. We do that by running the command again, but without the --add-initial-baseline
argument:
vcpkg x-update-baseline
On our next build, we'll then be using the most recent versions of our dependencies. This lets us incorporate the latest changes and improvements, but in a controlled way. That is, after updating our baseline, we should do a clean build and test that our program still works as expected with the updated versions of our dependencies.
Per-Dependency Versions
The baseline mechanism described above can do a lot of heavy lifting, but sometimes, we need to specify minimum versioning requirements for some of our dependencies.
For example, if we wanted to specify the minimum version of spdlog we use, but let the vcpkg baseline determine the tabulate version, we'd replace our simple "spdlog"
string with a JSON object containing both the name
and version>=
(minimum version):
vcpkg.json
{
"dependencies": [
"spdlog",
{
"name": "spdlog",
"version>=": "1.15.0"
},
"tabulate"
],
"builtin-baseline": "44a1e4eca5211434b5fefcc25a69bba246c3f861"
}
Within the dependencies
array, vcpkg only allows us to specify a minimum version requirement. If we need to set a requirement at all, setting only a minimum is generally recommended, as it maximises compatibility. However, if we really need an exact version of a library, we can use the overrides
mechanism, which we cover next.
Version Overrides
If we need to specify the exact version of a package we want to use, we can add it to an "overrides"
section of our manifest. Below, we lock spdlog to exactly version 1.15.1:
vcpkg.json
{
"dependencies": [
"spdlog",
"tabulate"
],
"overrides": [{
"name": "spdlog",
"version": "1.15.1"
}],
"builtin-baseline": "44a1e4eca5211434b5fefcc25a69bba246c3f861"
}
We should be cautious here, as this will also override any transitive use of spdlog through other dependencies. That is, if one of our other dependencies were also using spdlog, we'd override their version too, which can have unintended consequences.
After running a build, we can see information about the packages installed, including the versions, by looking in the /vcpkg_installed/vcpkg/status
file within our build directory:
build/vcpkg_installed/vcpkg/status
Package: fmt
Version: 11.2.0
...
Package: spdlog
Version: 1.15.1
Depends: fmt
...
Package: tabulate
Version: 1.5
...
Overriding Transitive Dependencies
The fmt
library in this list is an example of a transitive dependency. We don't require it directly, but spdlog
does, so it gets downloaded and compiled it so spdlog
can link against it.
Even though it's not our direct dependency, we can still override the version that gets used in our build:
vcpkg.json
{
"dependencies": [
"spdlog",
"tabulate"
],
"overrides": [{
"name": "fmt",
"version": "11.0.2"
}],
"builtin-baseline": "44a1e4eca5211434b5fefcc25a69bba246c3f861"
}
After running a build, we can confirm the change reflected in the status
file:
build/vcpkg_installed/vcpkg/status
Package: fmt
Version: 11.0.2
...
One of the main use cases for this is to deal with security vulnerabilities or other major issues found in a package that we're indirectly depending on.
The authors of the fmt
library may have released a new version to fix the issue, but we may not want to update our entire baseline right now, so we instead override just the problematic version.
Overrides should be used sparingly, but they let us make small, targetted changes like this when the need arises. They should also be temporary - when an override is no longer needed, we should delete it from our manifest.
Summary
In this lesson, we moved beyond basic vcpkg integration and learned how to manage our dependencies with precision.
- vcpkg Commands: The vcpkg executable is a useful tool in its own right. We learned how to add it to our system PATH and use commands like
vcpkg add
to manage our manifest file. - Reproducibility with Baselines: By adding a
"builtin-baseline"
to ourvcpkg.json
, we lock the vcpkg registry to a specific commit. This guarantees that anyone building our project will get the exact same versions of all dependencies. - Versioning Control: We saw how to specify a minimum version for a dependency directly in the manifest.
- Overrides for Pinning: For ultimate control, the
"overrides"
feature allows us to pin the exact version of any dependency, including transitive ones. This useful for working around issues in specific library versions.
Cross-Compiling with vcpkg
Combine vcpkg with CMake's cross-compilation capabilities. Learn to use triplets to target different architectures and chain-load toolchain files