Skip to content
C++

Which C++ build system should I use?

Decision guide for choosing between CMake, Meson, Bazel, xmake, and build2 — with real code, package manager integration, and CI setup.

Quick answer

cpp
Existing project?
  → Use what's already there. Migration cost is almost never worth it.

New project:
  Needs Windows + Visual Studio / CLion / VS Code IDE support?
    → CMake  (universal lingua franca)

  Linux/macOS only, new codebase, care about clean syntax?
    → Meson  (faster config, readable DSL, good defaults)

  Large monorepo, hermetic builds, distributed caching?
    → Bazel or Buck2

  Want build + packages in one tool, minimal config?
    → xmake

  Embedded / bare-metal cross-compilation?
    → CMake with a toolchain file

CMake — the industry standard

CMake generates build files for Ninja, Make, MSBuild, and Xcode. The key insight is to use modern CMake (3.15+): everything is expressed in terms of targets with properties, never global variables.

Minimal modern project

cmake
cmake_minimum_required(VERSION 3.20)
project(myapp VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_executable(myapp
    src/main.cpp
    src/app.cpp
)

target_include_directories(myapp PRIVATE include)
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

Library with public/private headers

cmake
add_library(mylib STATIC
    src/widget.cpp
    src/parser.cpp
)

# PUBLIC: consumers of mylib get this include path too
target_include_directories(mylib
    PUBLIC  include          # consumers see include/
    PRIVATE src              # only mylib sees src/
)

# Link against another library — PUBLIC means consumers also link it
target_link_libraries(mylib PUBLIC fmt::fmt)
target_link_libraries(mylib PRIVATE spdlog::spdlog)

The three link visibility levels:

  • PUBLIC — affects mylib and everything that links mylib
  • PRIVATE — affects only mylib
  • INTERFACE — affects only consumers (useful for header-only libs)

CMake workflow

bash
# Configure (generate build files):
cmake -B build -DCMAKE_BUILD_TYPE=Release

# Build:
cmake --build build -j$(nproc)

# Install:
cmake --install build --prefix /usr/local

# Run tests:
ctest --test-dir build --output-on-failure

# Presets (modern, replaces command-line flags):
# CMakePresets.json defines "debug", "release", "ci" etc.
cmake --preset release
cmake --build --preset release

FetchContent — pull dependencies at configure time

cmake
include(FetchContent)

FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        10.2.1
)
FetchContent_Declare(googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG        v1.14.0
)
FetchContent_MakeAvailable(fmt googletest)

target_link_libraries(myapp PRIVATE fmt::fmt)
target_link_libraries(tests PRIVATE GTest::gtest_main)

When to use FetchContent vs a package manager:

  • FetchContent is ideal for: small projects, when a dependency doesn't have a vcpkg/Conan package, or when you want a specific git commit
  • FetchContent downloads source and rebuilds every dependency from scratch — slow for large deps
  • vcpkg/Conan cache pre-built binaries — much faster in CI
bash
# Install vcpkg (one-time):
git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh

# Manifest mode (recommended): vcpkg.json in project root
json
{
  "name": "myapp",
  "version": "1.0.0",
  "dependencies": [
    "fmt",
    "boost-algorithm",
    { "name": "opencv", "version>=": "4.8.0" }
  ]
}
cmake
# CMakeLists.txt — vcpkg packages become CMake targets:
find_package(fmt CONFIG REQUIRED)
find_package(OpenCV REQUIRED)

target_link_libraries(myapp PRIVATE fmt::fmt ${OpenCV_LIBS})
bash
# Build with vcpkg toolchain:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build

GitHub Actions with vcpkg:

yaml
- uses: lukka/run-cmake@v10
  with:
    cmakeListsTxtPath: CMakeLists.txt
    configurePreset: release
    buildPreset: release

Conan integration

Conan is more mature for enterprise use and supports binary caching across teams:

ini
# conanfile.txt
[requires]
fmt/10.2.1
boost/1.83.0

[generators]
CMakeDeps
CMakeToolchain
bash
# Generate CMake integration files:
conan install . --output-folder=build --build=missing

# Configure with Conan's toolchain:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
cmake --build build

Meson — the clean alternative

Meson uses a Python-like DSL (meson.build), always generates Ninja files, and has better defaults than CMake out of the box. Config is 2-10× faster than CMake for the same project.

python
# meson.build
project('myapp', 'cpp',
  version: '1.0',
  default_options: ['cpp_std=c++23', 'warning_level=3'])

# Dependencies from system or wrap (Meson's package mechanism):
fmt_dep     = dependency('fmt')
threads_dep = dependency('threads')

mylib = static_library('mylib',
  sources: ['src/widget.cpp', 'src/parser.cpp'],
  include_directories: include_directories('include'),
  dependencies: [fmt_dep]
)

executable('myapp',
  sources: 'src/main.cpp',
  link_with: mylib,
  dependencies: [fmt_dep, threads_dep]
)

Meson wrap — built-in dependency management

ini
# subprojects/fmt.wrap
[wrap-git]
url = https://github.com/fmtlib/fmt.git
revision = 10.2.1
depth = 1

[provide]
fmt = fmt_dep
bash
meson setup build
meson compile -C build
meson test -C build

Meson strengths over CMake:

  • Cleaner syntax, no if-else spaghetti
  • Faster configuration
  • Cross-compilation built in (cross files)
  • Subprojects (wraps) are simpler than FetchContent

Meson weaknesses:

  • Fewer library consumers provide meson.build files than CMakeLists.txt
  • VS Code/CLion CMake integration is better polished than Meson integration
  • Less community Q&A available vs CMake

Bazel — hermetic, distributed, monorepo-scale

Bazel (and Meta's Buck2) enforce hermetic builds: every dependency is explicit, outputs are cached by content hash, and builds are reproducible across machines. This comes at significant up-front cost.

python
# BUILD (Starlark)
cc_binary(
    name = "myapp",
    srcs = ["src/main.cpp"],
    deps = [
        ":mylib",
        "@fmt//:fmt",
    ],
    copts = ["-std=c++23"],
)

cc_library(
    name = "mylib",
    srcs = ["src/widget.cpp"],
    hdrs = ["include/widget.hpp"],
    includes = ["include"],
)

When Bazel is worth it:

  • Monorepo with multiple languages (C++, Go, Python, Java)
  • Need distributed build caching (Remote Build Execution)
  • Reproducible builds are a hard requirement (compliance, security)
  • Team > 20 engineers working on the same codebase

When Bazel is overkill:

  • Single-language project
  • Team < 20
  • Dependencies are all from vcpkg/Conan (Bazel's C++ ecosystem support is weaker)

xmake — build + packages in one

xmake uses a Lua DSL and bundles its own package manager (xrepo). Zero external tool dependencies.

lua
-- xmake.lua
set_project("myapp")
set_languages("c++23")

add_requires("fmt", "spdlog")

target("myapp")
    set_kind("binary")
    add_files("src/*.cpp")
    add_packages("fmt", "spdlog")
    add_includedirs("include")
bash
xmake         # build
xmake run     # run
xmake test    # test

Best for: small to medium projects where you want one tool that does everything, especially on Windows where CMake's Windows story has friction.


build2 — integrated toolchain and package manager

build2 has the most principled design: a build system, package manager (bpkg), and repository all designed together. Reproducible builds, binary packages, dependency version resolution.

cpp
# buildfile
exe{myapp}: cxx{main} lib{mylib}
lib{mylib}: cxx{widget parser}

Best for: library authors who want to publish a properly packaged C++ library. Consumer story is still smaller than CMake.


Package manager comparison

FetchContentvcpkgConanxrepo
TypeCMake moduleStandalone + CMakeStandalone + CMakexmake-only
Binary cachingNoYesYesYes
Version pinningGit tag/hashManifest modeconanfile.txtxmake.lua
Private packagesVia git URLCustom registryArtifactory/serverCustom repo
Windows storyGoodExcellentGoodGood
CI integrationGoodExcellent (GH Actions)GoodGood
Package countN/A2,400+1,700+1,000+

CI/CD skeleton

CMake + vcpkg + GitHub Actions

yaml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { submodules: recursive }

      - name: Install vcpkg
        uses: lukka/get-cmake@latest

      - name: Configure
        run: cmake -B build -DCMAKE_BUILD_TYPE=Release
             -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake

      - name: Build
        run: cmake --build build -j$(nproc)

      - name: Test
        run: ctest --test-dir build -j$(nproc) --output-on-failure

Decision summary

SituationChoose
New project, any platform, IDE requiredCMake
New project, Linux/macOS only, clean codebaseMeson
Large monorepo, multi-language, need cachingBazel or Buck2
Want one tool for build + packagesxmake
Publishing a library with proper packagingbuild2
Embedded / cross-compileCMake with toolchain file
Existing projectKeep what it has

Package manager pairing:

  • CMake → vcpkg (open-source / personal) or Conan (enterprise / team)
  • Small CMake project → FetchContent (no extra tooling)
  • xmake → xrepo (built in)
  • Meson → wraps (built in) or system packages