Skip to content
C++
Package Manager
Updated 2026-07-14T00:00:00.000Z

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.

cmake
# 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/:

bash
mkdir -p cmake
curl -fsSL \
  https://raw.githubusercontent.com/cpp-pm/gate/master/cmake/HunterGate.cmake \
  -o cmake/HunterGate.cmake

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

bash
sha1sum hunter-v0.25.3.tar.gz

Alternatively, 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.

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

Libraryhunter_add_package(...)find_package(...)Primary target
BoostBoost COMPONENTS systemBoost CONFIG REQUIREDBoost::system
OpenSSLOpenSSLOpenSSL REQUIREDOpenSSL::SSL
zlibZLIBZLIB REQUIREDZLIB::ZLIB
fmtlibfmtfmt CONFIG REQUIREDfmt::fmt
nlohmann/jsonnlohmann_jsonnlohmann_json CONFIG REQUIREDnlohmann_json::nlohmann_json
Protocol BuffersProtobufProtobuf CONFIG REQUIREDprotobuf::libprotobuf
GoogleTestGTestGTest CONFIG REQUIREDGTest::gtest_main
spdlogspdlogspdlog CONFIG REQUIREDspdlog::spdlog
Catch2Catch2Catch2 CONFIG REQUIREDCatch2::Catch2WithMain
cURLCURLCURL REQUIREDCURL::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:

cpp
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():

cmake
# 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
# 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:

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:

bash
cmake -B build-arm \
      -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-linux-gnueabihf.cmake \
      -DCMAKE_BUILD_TYPE=Release
cmake --build build-arm

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

cpp
~/.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():

cmake
# 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:

cmake
# 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
# 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:

bash
cmake -B build -DHUNTER_USE_CACHE_SERVERS=YES

Hunter'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:

bash
# 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 build

To override where Hunter stores packages on disk, set HUNTER_ROOT before the HunterGate() call or via the environment:

bash
export HUNTER_ROOT=/opt/hunter-cache
cmake -B build

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

cpp
~/.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 files

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

cmake
# In CMakeLists.txt — before HunterGate()
set(HUNTER_ROOT "/opt/shared-hunter-cache" CACHE PATH "Hunter root")

include("cmake/HunterGate.cmake")
HunterGate(...)
bash
# Or via environment variable — applies to all projects
export HUNTER_ROOT=/opt/shared-hunter-cache

In Docker, mount the hunter cache as a volume to survive container rebuilds:

bash
docker run --rm \
  -v "$HOME/.hunter:/root/.hunter" \
  -v "$(pwd):/src" \
  -w /src \
  my-build-image \
  cmake -B build && cmake --build build

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

yaml
# .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-failure

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

ScenarioConfigure + 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
# 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:

cpp
cmake/
└── Hunter/
    └── config.cmake
hunter/
└── projects/
    └── my_lib/
        └── hunter.cmake      # version list

The hunter.cmake recipe file for a simple CMake-based library:

cmake
# 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():

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

FeatureHunterCPMvcpkgConan
CMake integrationNative — lives inside CMakeLists.txtNative — FetchContent wrapperToolchain fileGenerator + toolchain
External tool requiredNoneNoneNone (after clone)Python + pip
Packages from sourceAlwaysAlwaysAlways (or binary cache)Always (or binary cache)
Binary cachingLocal disk + Polly serversLocal disk (CPM_SOURCE_CACHE)Azure DevOps / NuGet / GHArtifactory / ConanCenter
Cross-compilationExcellent — inherits toolchainGood — inherits toolchainGood with tripletsGood with profiles
Ecosystem size~300 packagesAny GitHub/URL source2000+ ports1500+ packages (ConanCenter)
Version pinningSHA1 + config.cmakeGit tag / commit hashvcpkg.json baselineconanfile.txt/py lockfile
Offline supportYes — after first downloadYes — via local SOURCE_DIRPartialPartial
Maintenance statusMaintenance mode (no new features ~2023)ActiveActive (Microsoft)Active (JFrog)
Best forLegacy CMake projects, embedded/crossNew projects, minimal setupWindows/.NET teams, large ecosystemEnterprise, 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
Edit on GitHubUpdated 2026-07-14T00:00:00.000Z