Hunter — CMake Package Manager
Hunter CMake package manager — setup, using packages, toolchains, binary caching, and comparison with vcpkg and Conan.
TL;DR
Hunter is a CMake-native package manager by Ruslan Baratov that lives entirely inside your CMakeLists.txt. Unlike vcpkg or Conan, Hunter requires no external installer, no Python runtime, and no separate manifest file — you bootstrap it with a single include(), then call hunter_add_package() before find_package() and CMake handles the rest. Every package is built from source for the exact target platform, which makes Hunter particularly strong for cross-compilation scenarios where binary compatibility matters.
Use Hunter when your team already lives in CMake and you want zero friction for cross-platform or embedded builds. If you are starting a new project today, consider CPM instead — Hunter has been in maintenance mode since ~2023 and CPM offers a simpler API with active development.
# CMakeLists.txt — minimal Hunter hello world
cmake_minimum_required(VERSION 3.21)
include("cmake/HunterGate.cmake")
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.25.3.tar.gz"
SHA1 "0dfbc2cb5c4cf7e83533733bdfd2125ff96680cb"
)
project(my_app CXX)
hunter_add_package(fmt)
find_package(fmt CONFIG REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE fmt::fmt)The build is self-contained: cmake -B build && cmake --build build downloads, configures, and builds fmt on first run; subsequent runs hit the local cache in ~/.hunter/.
Setup — HunterGate
Hunter bootstraps itself through a single CMake module called HunterGate.cmake that you commit into your repository. No package manager needs to be installed separately — CMake downloads the Hunter archive the first time you configure the project.
Download HunterGate.cmake from the Hunter repository and place it under cmake/:
mkdir -p cmake
curl -fsSL \
https://raw.githubusercontent.com/cpp-pm/gate/master/cmake/HunterGate.cmake \
-o cmake/HunterGate.cmakeThen wire it into your CMakeLists.txt. The HunterGate() call must come before the project() command — Hunter needs to set up toolchain variables before CMake initializes the compiler:
cmake_minimum_required(VERSION 3.21)
# 1. Include the gate module
include("cmake/HunterGate.cmake")
# 2. Specify which Hunter release to use
# URL — tarball of the Hunter release on GitHub
# SHA1 — checksum of that tarball (find it on the Releases page)
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.25.3.tar.gz"
SHA1 "0dfbc2cb5c4cf7e83533733bdfd2125ff96680cb"
)
# 3. project() comes AFTER HunterGate
project(my_project VERSION 1.0 LANGUAGES CXX)
cmake_standard_required(CXX 17)To find the SHA1 for a given Hunter release, visit the Hunter GitHub Releases page, download the .tar.gz manually, and run:
sha1sum hunter-v0.25.3.tar.gzAlternatively, GitHub Releases pages display checksums directly in the release notes for Hunter. Keep both the URL and SHA1 pinned in source control — they uniquely identify your dependency graph and ensure reproducible builds across machines.
Adding Packages
The two-step pattern is always the same: hunter_add_package() fetches and builds the dependency, then find_package() imports its targets. Hunter translates this into a standard CMake find-module flow so the rest of your CMakeLists.txt stays idiomatic.
project(my_project CXX)
# Step 1: tell Hunter to make the package available
hunter_add_package(Boost COMPONENTS system filesystem)
hunter_add_package(OpenSSL)
hunter_add_package(fmt)
hunter_add_package(nlohmann_json)
hunter_add_package(GTest)
hunter_add_package(spdlog)
# Step 2: import targets exactly as you would without Hunter
find_package(Boost CONFIG REQUIRED COMPONENTS system filesystem)
find_package(OpenSSL REQUIRED)
find_package(fmt CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(GTest CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE
Boost::system
Boost::filesystem
OpenSSL::SSL
OpenSSL::Crypto
fmt::fmt
nlohmann_json::nlohmann_json
spdlog::spdlog
)
# GTest integrates with CTest
enable_testing()
add_executable(my_tests tests/test_main.cpp)
target_link_libraries(my_tests PRIVATE GTest::gtest_main)
gtest_discover_tests(my_tests)The following table lists commonly used Hunter packages along with the hunter_add_package name and the corresponding find_package call:
| Library | hunter_add_package(...) | find_package(...) | Primary target |
|---|---|---|---|
| Boost | Boost COMPONENTS system | Boost CONFIG REQUIRED | Boost::system |
| OpenSSL | OpenSSL | OpenSSL REQUIRED | OpenSSL::SSL |
| zlib | ZLIB | ZLIB REQUIRED | ZLIB::ZLIB |
| fmtlib | fmt | fmt CONFIG REQUIRED | fmt::fmt |
| nlohmann/json | nlohmann_json | nlohmann_json CONFIG REQUIRED | nlohmann_json::nlohmann_json |
| Protocol Buffers | Protobuf | Protobuf CONFIG REQUIRED | protobuf::libprotobuf |
| GoogleTest | GTest | GTest CONFIG REQUIRED | GTest::gtest_main |
| spdlog | spdlog | spdlog CONFIG REQUIRED | spdlog::spdlog |
| Catch2 | Catch2 | Catch2 CONFIG REQUIRED | Catch2::Catch2WithMain |
| cURL | CURL | CURL REQUIRED | CURL::libcurl |
For packages with components (e.g., Boost), pass the COMPONENTS list to hunter_add_package() and mirror it in find_package(). Hunter builds only the requested components, which reduces compile time significantly for large libraries.
Package Versions — HunterGate URL
Each Hunter release ships a fixed set of package versions baked into its configuration. When you point HunterGate at v0.25.3, you get the exact package versions that Hunter 0.25.3 ships by default — changing the Hunter release URL is how you move the entire dependency graph to a newer baseline.
The relationship between Hunter releases and available packages looks like this:
HunterGate(URL "...v0.25.3.tar.gz" SHA1 "...")
│
└─> ~/.hunter/_Base/Download/v0.25.3/
└─> cmake/projects/
├── Boost/ (default version defined here)
├── OpenSSL/
├── fmt/
└── ...To upgrade to a newer Hunter release, update the URL and SHA1 in HunterGate():
# Before (Hunter v0.24.x defaults)
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.24.18.tar.gz"
SHA1 "1292e4a39565ca5ed4d5b03584bfdbcbf3ca574d"
)
# After (Hunter v0.25.x defaults — newer package versions available)
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.25.3.tar.gz"
SHA1 "0dfbc2cb5c4cf7e83533733bdfd2125ff96680cb"
)If you want a specific package version that differs from the Hunter release default, do not change the Hunter URL — use a Hunter configuration file instead. Mixing these mechanisms keeps upgrades predictable: bump the Hunter version to get the ecosystem update, override individual packages only when you have a concrete reason.
Hunter Configuration File
A per-project cmake/Hunter/config.cmake lets you pin individual package versions or pass custom CMake options without changing your Hunter release baseline. Hunter loads this file automatically after the HunterGate() call if it exists at that path.
Create cmake/Hunter/config.cmake next to your HunterGate.cmake:
# cmake/Hunter/config.cmake
include(hunter_config)
# Pin OpenSSL to exactly 3.1.2
hunter_config(OpenSSL VERSION 3.1.2)
# Pin Boost to 1.83.0 and pass a custom CMake argument
hunter_config(Boost
VERSION 1.83.0
CMAKE_ARGS
BOOST_PYTHON_VERSION=3.11
)
# Disable shared libraries for fmt
hunter_config(fmt
VERSION 10.1.1
CMAKE_ARGS
BUILD_SHARED_LIBS=OFF
)
# Use the latest GTest with AddressSanitizer enabled
hunter_config(GTest
VERSION 1.14.0
CMAKE_ARGS
CMAKE_CXX_FLAGS=-fsanitize=address
)Hunter merges these overrides with the defaults from the Hunter release. The VERSION argument must be a version string that Hunter's project recipes know about — if the version is not in the recipe list, CMake errors at configure time with a list of available versions.
Tell HunterGate() where to find this file with the FILEPATH parameter if it lives in a non-default location. Otherwise Hunter resolves it automatically from ${CMAKE_SOURCE_DIR}/cmake/Hunter/config.cmake:
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.25.3.tar.gz"
SHA1 "0dfbc2cb5c4cf7e83533733bdfd2125ff96680cb"
FILEPATH "${CMAKE_SOURCE_DIR}/cmake/Hunter/config.cmake"
)Cross-Compilation
Hunter's most compelling advantage over vcpkg and Conan is transparent cross-compilation: point Hunter at a toolchain file and all dependencies rebuild for the target platform automatically. There is no separate "cross-compilation mode" to enable — Hunter inherits CMAKE_TOOLCHAIN_FILE and propagates it to every dependency build.
Pass the toolchain the standard CMake way:
cmake -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-linux-gnueabihf.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-armHunter sees CMAKE_TOOLCHAIN_FILE and stores the compiled dependencies under a separate cache key that includes the toolchain, so host and cross builds never collide:
~/.hunter/_Base/v0.25.3/
└── <config-hash>/
├── <host-toolchain-hash>/ # native build artifacts
│ └── Root/
└── <arm-toolchain-hash>/ # cross-compiled artifacts
└── Root/For Android NDK builds, set the platform variables before HunterGate():
# Before HunterGate — Android-specific variables
set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_ANDROID_NDK $ENV{ANDROID_NDK_ROOT})
set(CMAKE_ANDROID_ARCH_ABI arm64-v8a)
set(CMAKE_ANDROID_STL_TYPE c++_static)
# Control parallel package compilation (default: number of CPU cores)
set(HUNTER_JOBS_NUMBER 4)
include("cmake/HunterGate.cmake")
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.25.3.tar.gz"
SHA1 "0dfbc2cb5c4cf7e83533733bdfd2125ff96680cb"
)
project(my_android_app CXX)For iOS, Hunter supports multi-architecture fat libraries through the HUNTER_CONFIGURATION_TYPES variable:
# Build Debug and Release variants for device + simulator
set(HUNTER_CONFIGURATION_TYPES "Debug;Release")
set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH NO)Hunter then caches a separate artifact set for each configuration type, preventing debug symbols from leaking into release builds and vice versa.
Binary Caching — Upload/Download
Hunter supports a tiered binary cache system that can dramatically cut CI build times for large dependency trees. The simplest form is the local disk cache at ~/.hunter/ — packages built once are reused across all projects on the same machine. For teams, Hunter can push and pull pre-built binaries from a remote server.
Enable remote binary cache download by setting HUNTER_USE_CACHE_SERVERS:
# cmake/HunterGate.cmake call with remote cache
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.25.3.tar.gz"
SHA1 "0dfbc2cb5c4cf7e83533733bdfd2125ff96680cb"
)
# Enable download from GitHub Releases binary cache
set(HUNTER_USE_CACHE_SERVERS YES CACHE BOOL "Use Hunter binary cache")Or pass it on the command line:
cmake -B build -DHUNTER_USE_CACHE_SERVERS=YESHunter's Polly infrastructure (https://github.com/ruslo/polly) uses GitHub Releases as a binary cache backend. Each release asset is a compressed archive named after the package, version, and toolchain hash. To upload a built artifact to a private cache server, set the upload variables before configuring:
# Upload built packages to a cache server after building
cmake -B build \
-DHUNTER_USE_CACHE_SERVERS=YES \
-DHUNTER_RUN_UPLOAD=YES \
-DHUNTER_PASSWORD=<your-server-token>
cmake --build buildTo override where Hunter stores packages on disk, set HUNTER_ROOT before the HunterGate() call or via the environment:
export HUNTER_ROOT=/opt/hunter-cache
cmake -B buildThis is particularly useful in shared build environments where you want all developers to hit the same local NFS cache rather than each maintaining a personal ~/.hunter/ directory.
HUNTER_ROOT — Where Packages Live
Hunter stores all downloaded sources and built artifacts under a single root directory, defaulting to ~/.hunter/. Understanding this layout helps when debugging why a package is not being reused or when optimizing Docker layer caching.
The directory structure under HUNTER_ROOT looks like this:
~/.hunter/
└── _Base/
├── Download/
│ └── v0.25.3/ # Hunter release tarball
└── <hunter-release-hash>/
└── <config-hash>/
└── <toolchain-hash>/
└── Root/
├── include/ # installed headers
├── lib/ # installed static/shared libs
└── share/ # CMake config filesThe nested hashes make each unique combination of Hunter version, project config, and toolchain fully independent. Two projects on the same machine that share the same Hunter version and config will share the compiled artifacts — no redundant builds.
Override HUNTER_ROOT per-project or machine-wide:
# In CMakeLists.txt — before HunterGate()
set(HUNTER_ROOT "/opt/shared-hunter-cache" CACHE PATH "Hunter root")
include("cmake/HunterGate.cmake")
HunterGate(...)# Or via environment variable — applies to all projects
export HUNTER_ROOT=/opt/shared-hunter-cacheIn Docker, mount the hunter cache as a volume to survive container rebuilds:
docker run --rm \
-v "$HOME/.hunter:/root/.hunter" \
-v "$(pwd):/src" \
-w /src \
my-build-image \
cmake -B build && cmake --build buildThe volume mount means a cold build in Docker only pays the compilation cost once — subsequent docker run invocations reuse the pre-built packages from the host's ~/.hunter/ directory.
Gatling: Hunter in CI
GitHub Actions can cache the ~/.hunter/ directory between runs, turning cold Hunter builds into near-instant warm builds after the first pipeline execution. The cache key should include the Hunter release hash and your config.cmake so it invalidates automatically when you upgrade packages.
A complete GitHub Actions workflow with Hunter caching:
# .github/workflows/build.yml
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
# Cache ~/.hunter/ keyed on Hunter version + config hash
- name: Cache Hunter packages
uses: actions/cache@v4
with:
path: ~/.hunter
key: hunter-${{ runner.os }}-${{ hashFiles('cmake/HunterGate.cmake', 'cmake/Hunter/config.cmake') }}
restore-keys: |
hunter-${{ runner.os }}-
- name: Configure
run: |
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DHUNTER_JOBS_NUMBER=4
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest --test-dir build --output-on-failureThe hashFiles() expression covers both the Hunter version (embedded in HunterGate.cmake) and any version overrides in config.cmake. When either file changes, the cache key changes and Hunter rebuilds from scratch, then stores the new artifacts for future runs.
Typical timings on a standard GitHub-hosted runner:
| Scenario | Configure + Build time |
|---|---|
| Cold (no cache, all packages compile) | 8–25 min (depends on Boost, OpenSSL) |
| Warm (full cache hit) | 30–90 sec |
| Partial (cache hit, one new package) | 2–5 min |
Set HUNTER_JOBS_NUMBER based on the runner's available CPUs. GitHub-hosted runners typically have 2 cores, so HUNTER_JOBS_NUMBER=2 is a safe default. Self-hosted runners with more cores benefit from higher values.
Custom Hunter Packages
Hunter ships recipes for several hundred popular C++ libraries, but you can add packages that are not in the official catalog by writing a Hunter package recipe. A recipe is a small CMakeLists.txt that describes how to download and build a library using CMake's ExternalProject mechanism.
For a quick local override, point Hunter at a source directory you already have:
# cmake/Hunter/config.cmake
# Use a locally checked-out version of a library during development
hunter_config(my_lib
URL "file://${CMAKE_CURRENT_SOURCE_DIR}/vendor/my_lib-1.0.tar.gz"
SHA1 "da39a3ee5e6b4b0d3255bfef95601890afd80709"
)The proper way to add a fully new package to Hunter involves creating a recipe directory:
cmake/
└── Hunter/
└── config.cmake
hunter/
└── projects/
└── my_lib/
└── hunter.cmake # version listThe hunter.cmake recipe file for a simple CMake-based library:
# hunter/projects/my_lib/hunter.cmake
hunter_add_version(
PACKAGE_NAME my_lib
VERSION 1.2.3
URL "https://github.com/acme/my_lib/archive/v1.2.3.tar.gz"
SHA1 "abcdef1234567890abcdef1234567890abcdef12"
)
hunter_pick_scheme(DEFAULT url_sha1_cmake)
hunter_cacheable(my_lib)
hunter_configuration_options(my_lib
CONFIGURATION_OPTIONS
BUILD_SHARED_LIBS=OFF
)Point Hunter to your local recipes with the LOCAL parameter on HunterGate():
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.25.3.tar.gz"
SHA1 "0dfbc2cb5c4cf7e83533733bdfd2125ff96680cb"
LOCAL # load recipes from ${CMAKE_SOURCE_DIR}/hunter/
)Contributing a new package to the official Hunter repository involves submitting a pull request to cpp-pm/hunter with your recipe under cmake/projects/<PackageName>/. Hunter maintainers review recipes for correctness and cache-ability before merging. Given Hunter's maintenance-mode status, the review cycle may be slow — for new packages, CPM with a GIT_REPOSITORY source is often a faster path.
Hunter vs vcpkg vs CPM vs Conan
Hunter was the first serious CMake-native package manager, but the ecosystem has evolved — understand the tradeoffs before committing. Hunter is in maintenance mode as of approximately 2023: it receives bug fixes and security patches but no new features. For new projects, CPM is the recommended lightweight alternative; vcpkg and Conan serve teams that need large ecosystems or advanced binary caching.
| Feature | Hunter | CPM | vcpkg | Conan |
|---|---|---|---|---|
| CMake integration | Native — lives inside CMakeLists.txt | Native — FetchContent wrapper | Toolchain file | Generator + toolchain |
| External tool required | None | None | None (after clone) | Python + pip |
| Packages from source | Always | Always | Always (or binary cache) | Always (or binary cache) |
| Binary caching | Local disk + Polly servers | Local disk (CPM_SOURCE_CACHE) | Azure DevOps / NuGet / GH | Artifactory / ConanCenter |
| Cross-compilation | Excellent — inherits toolchain | Good — inherits toolchain | Good with triplets | Good with profiles |
| Ecosystem size | ~300 packages | Any GitHub/URL source | 2000+ ports | 1500+ packages (ConanCenter) |
| Version pinning | SHA1 + config.cmake | Git tag / commit hash | vcpkg.json baseline | conanfile.txt/py lockfile |
| Offline support | Yes — after first download | Yes — via local SOURCE_DIR | Partial | Partial |
| Maintenance status | Maintenance mode (no new features ~2023) | Active | Active (Microsoft) | Active (JFrog) |
| Best for | Legacy CMake projects, embedded/cross | New projects, minimal setup | Windows/.NET teams, large ecosystem | Enterprise, reproducible binary distribution |
Key consideration for new projects: Hunter's maintenance-mode status means its default package versions are aging. As of 2024, several Hunter recipes point to library versions that are multiple major releases behind the current stable. If you are starting from scratch, CPM gives you the same CMake-first experience with the ability to pull any Git tag or commit — effectively giving you access to the latest version of any library on GitHub without waiting for a Hunter recipe update.
Hunter remains a strong choice for:
- Projects already using Hunter that cannot afford a migration
- Environments where the fixed, audited package versions in a Hunter release are a compliance requirement
- Cross-compilation workflows where you have already built up a Polly-based binary cache
See Also
- CPM — lightweight CMake package manager built on FetchContent; recommended for new projects
- vcpkg — Microsoft's package manager with 2000+ ports and strong Windows integration
- Conan — enterprise-grade package manager with ConanCenter and lockfile support
- CMake — build system that Hunter, CPM, and vcpkg all build on top of