Post

C++ Build with CMake, Ninja, Conan and vcpkg

How CMake builds C and C++ projects with Ninja and Conan or vcpkg, which files it needs, how to lint, scan, test and cover, and a complete GitHub Actions pipeline.

C++ Build with CMake, Ninja, Conan and vcpkg

C and C++ have no official build tool or package manager, but CMake has become the de facto standard: it generates native build files (Ninja, Make, Visual Studio) from one portable description, and integrates with Conan or vcpkg for dependencies. Most modern C++ projects, and nearly every IDE, expect a CMakeLists.txt.

This post is part of Build Ecosystems by Programming Language. Legacy and Unix-style builds are covered in Make and Autotools.

At a glance

CategoryCommon tools
Build toolsCMake (meta-build), Ninja (fast executor), Make, MSBuild; ccache / sccache for compiler caching
CompilersGCC, Clang, MSVC
Package managersConan 2, vcpkg, CMake FetchContent
Build filesCMakeLists.txt, CMakePresets.json, cmake/*.cmake, conanfile.txt / conanfile.py, conan.lock, vcpkg.json, vcpkg-configuration.json, .clang-format, .clang-tidy
Lint and formatclang-tidy, clang-format, cppcheck, include-what-you-use
SAST (open source)cppcheck, clang-tidy security checks, Clang Static Analyzer, CodeQL, Semgrep, Flawfinder
SAST (commercial)Coverity, Klocwork, Polyspace, CodeSonar, Parasoft C/C++test, Fortify, Checkmarx One, SonarQube Server
Runtime checksAddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer, libFuzzer / AFL++
SCABlack Duck (snippet matching), Mend, Snyk (unmanaged C/C++), Trivy / Grype on Conan / vcpkg manifests
Frameworks / librariesQt, Boost, gRPC, Abseil, fmt, spdlog
Test and coverageGoogleTest, Catch2, doctest, CTest; gcov + gcovr / lcov, llvm-cov
Build outputExecutables, .a / .lib, .so / .dll / .dylib, CPack packages (.deb, .rpm, .tar.gz, .zip)
CI cacheccache directory, ~/.conan2, vcpkg binary cache

How the build works

In CI, each stage of the generic build pipeline maps to a CMake command, plugin or scanner. Press play, or step through it:

  1. Source + CMakeLists.txt: A commit triggers CI. CMakeLists.txt and CMakePresets.json define the build; conanfile.txt or vcpkg.json declare the dependencies.
  2. Dependencies: conan install . --build=missing fetches or builds the dependencies (vcpkg manifest mode is the alternative); conan.lock pins versions.
  3. Configure: cmake --preset release generates the Ninja build files and compile_commands.json for the analyzers.
  4. Build: cmake --build --preset release compiles with Ninja; ccache or sccache avoid recompiling unchanged files.
  5. Test and coverage: ctest runs the GoogleTest or Catch2 tests; a coverage preset plus gcovr --fail-under-line 80 enforces coverage.
  6. Security scans in parallel: clang-tidy and cppcheck check our code, Trivy or Grype scan the Conan / vcpkg manifests (Black Duck adds snippet matching), and Gitleaks scans for secrets.
  7. Quality gate: The build, tests, coverage gate, analyzers and a sanitizer build must pass before packaging.
  8. Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
  9. Package (pass): cpack builds the release package (.tar.gz, .deb, .rpm or .zip) from the install rules.
  10. SBOM and signing (pass): Syft generates the SBOM from the package, and Sigstore cosign signs it.
  11. Store the package (pass): CI keeps the package with actions/upload-artifact; libraries can also be published to a Conan remote.
  • Configure reads CMakeLists.txt, detects the compiler, finds packages and writes native build files into a separate build folder (out-of-source).
  • Build runs Ninja, which compiles only what changed.
  • compile_commands.json (from CMAKE_EXPORT_COMPILE_COMMANDS) is what clang-tidy, cppcheck and IDEs use to know the exact compiler flags.
  • Presets (CMakePresets.json) put generator, build type, toolchain and cache variables in a file, so CI and developers run the same configuration with cmake --preset release.

Build files

FilePurposeCommit?
CMakeLists.txtTargets, sources, dependencies, tests (one per directory)Yes
CMakePresets.jsonShared configure / build / test presetsYes
CMakeUserPresets.jsonPersonal presetsNo
cmake/*.cmakeHelper modules and toolchain filesYes
conanfile.txt / conanfile.pyConan dependencies and generatorsYes
conan.lockLocked Conan dependency graphYes
vcpkg.json, vcpkg-configuration.jsonvcpkg manifest and registry baselineYes
.clang-format, .clang-tidyFormat and lint rulesYes
build/Generated build treeNo

Commands by category

1
2
3
4
5
6
7
8
conan install . --output-folder=build --build=missing   # dependencies (Conan)
cmake --preset release                           # configure
cmake --build --preset release                   # build
clang-tidy -p build src/*.cpp                    # lint + bugprone/cert checks
cppcheck --project=build/compile_commands.json --error-exitcode=1   # SAST
ctest --preset release --output-on-failure       # tests
cmake --preset coverage && cmake --build --preset coverage && ctest --preset coverage && gcovr -r . --fail-under-line 80   # coverage gate
cpack --config build/CPackConfig.cmake -G TGZ    # package

Minimal project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# CMakeLists.txt
cmake_minimum_required(VERSION 3.25)
project(demo VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_executable(demo src/main.cpp)
target_compile_options(demo PRIVATE
  $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Werror>
  $<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>)

include(FetchContent)
FetchContent_Declare(googletest
  URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz)
FetchContent_MakeAvailable(googletest)

enable_testing()
add_executable(demo_tests tests/demo_test.cpp)
target_link_libraries(demo_tests GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(demo_tests)

install(TARGETS demo)
include(CPack)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "version": 6,
  "configurePresets": [
    {
      "name": "release",
      "generator": "Ninja",
      "binaryDir": "build",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
    },
    {
      "name": "coverage",
      "generator": "Ninja",
      "binaryDir": "build-cov",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_CXX_FLAGS": "--coverage" }
    }
  ],
  "buildPresets": [
    { "name": "release", "configurePreset": "release" },
    { "name": "coverage", "configurePreset": "coverage" }
  ],
  "testPresets": [
    { "name": "release", "configurePreset": "release" },
    { "name": "coverage", "configurePreset": "coverage" }
  ]
}

Frameworks and libraries

LibraryAdd it to a CMake project
Qt 6find_package(Qt6 REQUIRED COMPONENTS Widgets) + target_link_libraries(app Qt6::Widgets)
Boostfind_package(Boost REQUIRED COMPONENTS filesystem) + Boost::filesystem
fmt / spdlogvcpkg add port fmt or Conan fmt/11.0.2, then find_package(fmt)
gRPCfind_package(gRPC CONFIG REQUIRED) + gRPC::grpc++
GoogleTestFetchContent (as above) or find_package(GTest)

CI with GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
name: cmake-ci
on: [push, pull_request]
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get update && sudo apt-get install -y ninja-build cppcheck clang-tidy gcovr
      - uses: hendrikmuhs/ccache-action@v1.2
        with:
          key: ${{ runner.os }}-release

      - name: Configure and build
        run: |
          cmake --preset release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
          cmake --build --preset release
      - run: cppcheck --project=build/compile_commands.json --error-exitcode=1
      - run: ctest --preset release --output-on-failure

      - name: Sanitizer build
        run: |
          cmake -S . -B build-asan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined"
          cmake --build build-asan
          ctest --test-dir build-asan --output-on-failure

      - uses: anchore/sbom-action@v0           # SBOM of the source tree (Syft)
        with:
          path: .
          format: cyclonedx-json

Best practices

  • Build out of source (-B build) and never commit the build tree.
  • Use target-based CMake (target_link_libraries, target_compile_options) instead of global include_directories / add_definitions.
  • Put configuration in presets so CI and developers share one source of truth.
  • Pin dependencies: Conan lockfiles, a vcpkg builtin-baseline, or FetchContent with a fixed tag or URL hash.
  • Treat warnings as errors and run a separate sanitizer job (ASan/UBSan) in CI.
  • Cache compiler output with ccache or sccache; it is the biggest CI speed-up for C++.
  • Plan SCA explicitly: manifest scanners only see what Conan/vcpkg declare, so vendored code needs snippet-level scanning (Black Duck) or a maintained inventory.
This post is licensed under CC BY 4.0 by the author.