How vcpkg Support Works¶
Blade consumes vcpkg libraries as
first-class deps via vcpkg#<port>:<lib> references. This document explains
how it is implemented (issue #1236);
for the user-facing guide see build_rules/vcpkg.md.
Source files involved:
| File | Responsibility |
|---|---|
src/blade/vcpkg.py |
All vcpkg logic: triplet derivation, .pc/CMake parsing, manifest/triplet generation, the vcpkg# dep handler, and the managed-install orchestration (setup) |
src/blade/target.py |
register_dep_scheme + _unify_scheme_dep — the <scheme>#<coordinate> dispatch that routes vcpkg#... into the handler |
src/blade/cc_targets.py |
VcpkgLibrary — the auto-created target that links a resolved port artifact |
src/blade/build_manager.py |
setup_vcpkg() — calls vcpkg.setup(self) between config load and BUILD parsing |
src/blade/load_build_files.py |
imports blade.vcpkg so the scheme registers |
src/blade/config.py |
the vcpkg_config(...) schema |
1. Two phases, cleanly split¶
vcpkg support is organized so that the pure, testable logic is separable from
the side-effecting orchestration. vcpkg.py's module docstring calls these out:
- Reference resolution (analysis phase): turn a
vcpkg#port:libstring into concrete install-tree paths, and create a target for it. Pure functions plus one handler. - Managed install (
setup, build phase): generate a manifest + overlay triplet and runvcpkg install. The only step that touches the network / disk heavily.
Most of the module is pure functions over plain inputs (triplet_for,
parse_pkgconfig, manifest_json, port_required_libs, …), unit-tested in
isolation; the stateful parts (_vcpkg_dep_handler, setup) are thin wrappers
that gather inputs from the toolchain/config and call them.
2. The vcpkg# reference is a dep scheme¶
vcpkg extends Blade's #name system-library family. A dependency of the form
<scheme>#<coordinate> is dispatched by target._unify_scheme_dep, and at
import time vcpkg.py registers itself:
The import happens in load_build_files.py, so the scheme is live before any
BUILD file is parsed. #pthread (no scheme) still means "a system lib resolved
by name"; vcpkg#fmt:fmt means "resolve fmt:fmt through the vcpkg provider".
3. Resolution: _vcpkg_dep_handler → VcpkgLibrary¶
When a target depends on vcpkg#fmt:fmt, _vcpkg_dep_handler(referrer, 'fmt:fmt'):
- Governance — if
vcpkg_config.direct_use_allowedis set, reject a reference from a path outside the allowlist (mirrors the wrapper-library discipline large repos use). - Triplet —
triplet_for_toolchain(toolchain)derives the triplet from Blade's resolved toolchain (x64-linux,arm64-osx, …);'auto'/ unspecified resolves the static variant (on Windows that means the-statictriplet, since vcpkg defaults to dynamic there). resolve_reference(...)— pure validation + path math: splitport:lib, enforce the whitelist (port in packages, else a hard error), computelib_dir(thedebug/libsubtree for an MSVC-ABI debug build, elselib) and the sharedinclude_dir.lib == 'hdrs'marks a header-only port.- Target creation — auto-create one
VcpkgLibrary(the same pattern as_add_system_library), keyed byvcpkg#port:libso it is created once and reused. For an'auto'-linkage port in managed mode it also computes the shared sibling'sdynamic_lib_dirin the separate-sharedinstall tree.
The root / triplet are stashed on the target so it can resolve the port's
pkg-config private system libs and transitive Requires: siblings lazily at
generate time — at analysis time the install tree may not exist yet (the
install runs in between; see §5).
4. Parsing what a port actually needs¶
A single vcpkg#protobuf:protobuf reference must pull in the right sibling
archives and system libs. vcpkg.py reads that from the install tree:
parse_pkgconfig(text)parses a port's.pcfile —Libs/Libs.private(the-l<name>exports and the private system libs) andRequires/Requires.private(sibling vcpkg modules)._expandresolves${prefix}-style pkg-config variables.port_required_libs(...)follows thoseRequires:to the sibling archives — this is how protobuf (v22+) transparently links the wholeabsl_*+utf8_rangeset without the user listing them.port_system_libs(...)extracts the OS/SDK libs fromLibs.privateand the CMake link interface (_cmake_link_libs).
These are pure text functions, which is what makes them straightforward to
unit-test against captured .pc fixtures.
5. Managed install: setup¶
build_manager.setup_vcpkg() calls vcpkg.setup(builder) once, after config
load and after analyze, before the install artifacts are needed. Key behaviors:
- No-op unless needed — returns early unless
manage=True(default) with a non-emptypackages, and_build_uses_vcpkg(builder)is true. This demand-driven gate lets a workspace declarevcpkg_configunconditionally (it is a fixed project property) while a build that references novcpkg#...dep pays nothing — and never needs the vcpkg tool. - Overlay triplet — it generates a
blade-<triplet>overlay (overlay_triplet_cmake) that chainloads Blade's resolved compiler (chainload_cmake) so the artifacts are ABI-compatible with the rest of the build. MSVC is the exception:cl.exeuses vcpkg's native toolchain (no chainload) so vcpkg sets up the full MSVC environment. - Hermetic tree — manifest (
vcpkg.json), the chainload cmake, and the overlay triplet are written under<build>/.cache/vcpkg/;vcpkg installinstalls intoinstalled/<overlay>there. Only the artifacts live under the build dir; the vcpkg tool is still found viavcpkg_config.root/$VCPKG_ROOT/PATH(_find_vcpkg_tool). - Stamp-skip — an md5 over
(manifest + chainload + triplet_cmake + overlay)is compared against.blade-vcpkg-stamp; an unchanged stamp with an existinginstalled/<overlay>skips the install entirely. - Second (shared) tree on demand —
_auto_dynamic_ports(builder, packages)computes which'auto'ports adynamic_linkbinary actually depends on (from the analyzed graph —setupruns after analyze). Only those are installed a second time as shared libraries into a separate-sharedinstall root (_install_shared). An all-static build skips this completely.
6. Linkage model¶
port_options / _effective_linkage resolve a port's linkage
('auto' / 'static' / 'dynamic'). The 'auto' default mirrors
cc_library: the static archive always exists, and a shared build is produced
only on demand when a dynamic_link binary depends on the port — which is
why the shared build lands in a separate blade-<triplet>-shared tree (vcpkg
builds one linkage per triplet). dynamic_ports / auto_ports feed the triplet
and second-install computations above. This is what prevents the
singleton-duplication problem (one shared instance of gflags/protobuf/… across
all dylibs) without per-port configuration.
7. Where to look first¶
- A resolution bug (wrong path, missing sibling, whitelist message) →
resolve_reference/port_required_libs/_vcpkg_dep_handler. - An install bug (wrong triplet, ABI mismatch, re-installs every build) →
setup/overlay_triplet_cmake/chainload_cmake/ the stamp logic. - A linkage bug (static where shared expected, singleton collision) →
port_options/_effective_linkage/_auto_dynamic_ports.