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
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 fileCMake — 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_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
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 mylibPRIVATE— affects only mylibINTERFACE— affects only consumers (useful for header-only libs)
CMake workflow
# 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 releaseFetchContent — pull dependencies at configure time
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
vcpkg integration (recommended for most projects)
# Install vcpkg (one-time):
git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
# Manifest mode (recommended): vcpkg.json in project root{
"name": "myapp",
"version": "1.0.0",
"dependencies": [
"fmt",
"boost-algorithm",
{ "name": "opencv", "version>=": "4.8.0" }
]
}# 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})# Build with vcpkg toolchain:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build buildGitHub Actions with vcpkg:
- uses: lukka/run-cmake@v10
with:
cmakeListsTxtPath: CMakeLists.txt
configurePreset: release
buildPreset: releaseConan integration
Conan is more mature for enterprise use and supports binary caching across teams:
# conanfile.txt
[requires]
fmt/10.2.1
boost/1.83.0
[generators]
CMakeDeps
CMakeToolchain# 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 buildMeson — 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.
# 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
# subprojects/fmt.wrap
[wrap-git]
url = https://github.com/fmtlib/fmt.git
revision = 10.2.1
depth = 1
[provide]
fmt = fmt_depmeson setup build
meson compile -C build
meson test -C buildMeson 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.buildfiles thanCMakeLists.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.
# 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.
-- 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")xmake # build
xmake run # run
xmake test # testBest 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.
# 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
| FetchContent | vcpkg | Conan | xrepo | |
|---|---|---|---|---|
| Type | CMake module | Standalone + CMake | Standalone + CMake | xmake-only |
| Binary caching | No | Yes | Yes | Yes |
| Version pinning | Git tag/hash | Manifest mode | conanfile.txt | xmake.lua |
| Private packages | Via git URL | Custom registry | Artifactory/server | Custom repo |
| Windows story | Good | Excellent | Good | Good |
| CI integration | Good | Excellent (GH Actions) | Good | Good |
| Package count | N/A | 2,400+ | 1,700+ | 1,000+ |
CI/CD skeleton
CMake + vcpkg + GitHub Actions
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-failureDecision summary
| Situation | Choose |
|---|---|
| New project, any platform, IDE required | CMake |
| New project, Linux/macOS only, clean codebase | Meson |
| Large monorepo, multi-language, need caching | Bazel or Buck2 |
| Want one tool for build + packages | xmake |
| Publishing a library with proper packaging | build2 |
| Embedded / cross-compile | CMake with toolchain file |
| Existing project | Keep 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