C++ Build Systems Compared: CMake, Meson, Bazel, Makefile, and Choosing a Package Manager
이 글의 핵심
Full comparison of C++ build systems: CMake, Meson, Bazel, Makefile, and Ninja; vcpkg vs Conan; common errors; Makefile→CMake migration; reproducible builds, CI cache, and CMake Presets—so you can pick and operate a stack with confidence.
Introduction: “Which build system should we use?”
Real-world pain scenarios
"We're starting a new project—CMake? Meson? Bazel? What do we pick?"
"We only have Makefiles in a legacy codebase and new hires can't read them."
"It builds on Windows but CI on Linux fails."
"Team A uses vcpkg and Team B uses Conan—can we use both?"
"Full builds take 30 minutes—will Meson or Bazel fix that?"
"We tried Bazel but the learning curve looks brutal."
"Our CMakeLists.txt is 3,000 lines—maintenance is hell."
This post covers:
- Pain scenarios: choosing, maintaining, and migrating build setups
- A full comparison: CMake, Meson, Bazel, Makefile, Ninja, and package managers
- Common errors and fixes
- Migration paths: Makefile→CMake, CMake→Meson, and more
- Production patterns: CI/CD, caching, reproducible builds
To compare across language ecosystems, contrast how Rust Cargo, npm / Node modules, Go modules, and Python pip·uv·Poetry bundle build, dependencies, and cache—then map that to the usual C++ stack of CMake plus Conan or vcpkg. See also Makefile when you maintain rules by hand.
Prerequisites: C++17 or later; recent versions of the tools discussed.
Production note: This article is grounded in issues and fixes from large C++ codebases, including pitfalls and debugging tips that textbooks often skip.
Table of contents
- Problem scenarios in detail
- Build-system taxonomy and architecture
- Comparison tables
- CMake in depth
- Meson in depth
- Bazel in depth
- Makefile in depth
- Package managers: vcpkg & Conan
- Common errors and fixes
- Migration guide
- Production patterns
- Decision guide
- Implementation checklist
1. Problem scenarios in detail
Scenario 1: Choosing a build system for a new project
Context: A three-person team starts a C++ server. One developer each on Windows, Linux, and macOS.
Question: CMake? Meson? Bazel?
Consider:
- Cross-platform is mandatory
- Third-party libraries: fmt, spdlog, Boost.Asio, etc.
- CI on GitHub Actions
- The team cannot afford a steep learning curve
Direction: CMake + vcpkg is the most pragmatic default. Meson can be faster to configure but has a smaller ecosystem. Bazel fits Google-style monorepos and teams that already invest in it.
Scenario 2: Maintaining legacy Makefiles
Context: A ten-year-old C++ project only has Makefiles. New hires say Makefiles are hard to read.
Question: Keep Makefiles or migrate to CMake?
Consider:
- Dependency rules are complex
- IDEs (CLion, VS Code) support CMake far better than raw Makefiles
- Whether you can migrate incrementally
Direction: Incremental migration to CMake, or wrap the existing Makefile with CMake ExternalProject to get IDE integration first.
Scenario 3: Build time explosion
Context: ~100k LOC; full build ~30 minutes, incremental ~5 minutes.
Question: Will Meson or Bazel help?
Consider:
- CMake + Ninja is often already near the practical ceiling
- Bottlenecks are usually compilation itself (parallelism, PCH, ccache)
- Cost vs benefit of switching build systems
Direction: First tune ccache, PCH, and parallel builds (-j). Meson can speed up configure. Bazel shines at incremental builds at very large scale.
Scenario 4: Package manager conflict
Context: Team A used vcpkg; Team B used Conan. The teams merged.
Question: Must we pick one? Can we use both?
Consider:
- CMake can consume vcpkg or Conan toolchains
- Mixing both in one project is usually a bad idea (dependency conflicts)
- You need one standard per repo
Direction: One package manager per project. When teams merge, choose based on library coverage and existing CI investment.
Scenario 5: Build reproducibility
Context: "It worked yesterday; CI fails today." The vcpkg baseline may have moved.
Question: How do we keep reproducible builds?
Consider:
- vcpkg:
builtin-baseline,vcpkg.lock - Conan:
conan.lock, pinned versions - Docker to pin the OS/toolchain
Direction: Commit lock files (vcpkg.lock, conan.lock). Pin baselines and versions explicitly.
Scenario 6: Cross-compilation
Context: Build for ARM embedded from an x86_64 host.
Question: Which build system supports cross-compilation best?
Consider:
- CMake: toolchain file
- Meson: cross file
- Conan: profile
- Bazel: platforms
Direction: CMake + Conan or vcpkg triplets have the most documentation and examples.
2. Build-system taxonomy and architecture
Categories
flowchart TB
subgraph Meta["Meta build systems (config → native build)"]
CMake[CMake]
Meson[Meson]
Bazel[Bazel]
end
subgraph Native["Native build (direct execution)"]
Make[Make]
Ninja[Ninja]
MSBuild[MSBuild]
end
subgraph Package["Package managers (deps + build)"]
vcpkg[vcpkg]
Conan[Conan]
end
CMake --> Make
CMake --> Ninja
CMake --> MSBuild
Meson --> Ninja
Meson --> MSBuild
Bazel --> BazelInternal[Bazel internal engine]
vcpkg --> CMake
Conan --> CMake
End-to-end pipeline
sequenceDiagram
participant Dev as Developer
participant Meta as Meta build (CMake/Meson)
participant Native as Native build (Ninja/Make)
participant PM as Package manager (optional)
Dev->>PM: Install dependencies (vcpkg/Conan)
PM->>Dev: Toolchain paths
Dev->>Meta: Configure (generate build files)
Meta->>Native: Emit Makefile/Ninja/etc.
Dev->>Native: Build (compile and link)
Native->>Dev: Binaries and libraries
Terminology
| Term | Meaning |
|---|---|
| Meta build | Tools like CMake and Meson that read project files and emit native build files |
| Native build | Tools like Make, Ninja, and MSBuild that actually compile and link |
| Package manager | Tools like vcpkg and Conan that fetch, build, and expose third-party libraries |
| Toolchain | Compiler + linker + flags (e.g. GCC 12, Clang 15, MSVC 2022) |
3. Comparison tables
Core build-system comparison
| Item | CMake | Meson | Bazel | Makefile |
|---|---|---|---|---|
| Role | Meta build | Meta build | Unified build | Native build |
| Market share | ~83% (2024) | ~5% | ~3% | Widespread legacy |
| Config language | CMake DSL | Meson (Python-like) | Starlark (Python-like) | Makefile syntax |
| Learning curve | Medium–high | Low–medium | High | Medium |
| Cross-platform | Strong | Strong | Strong | Manual |
| Configure speed | Typical | Fast | Fast | N/A |
| Build speed | Fast with Ninja | Ninja by default | Fast | Make by default |
| IDE support | Excellent | Fair | Fair | Limited |
| Package integration | vcpkg, Conan, FetchContent | wrap, Conan | rules_cc, Conan | Manual |
| Very large projects | Viable | Viable | Optimized | Hard |
| Ecosystem | Largest | Growing | Google-centric | Legacy-heavy |
Package managers: vcpkg vs Conan
| Item | vcpkg | Conan |
|---|---|---|
| Role | C++ package manager | C++ package manager |
| Build integration | CMake-centric | Emits CMake (and others) |
| Package count | 2000+ | 1500+ |
| Install | Git clone + bootstrap | pip install conan |
| Manifest | vcpkg.json | conanfile.txt / .py |
| Version pinning | builtin-baseline, vcpkg.lock | Explicit versions, conan.lock |
| CMake integration | Toolchain file | conan install → toolchain |
| Microsoft ecosystem | Strong | Moderate |
| Private packages | Overlay ports | Private remotes (e.g. Artifactory) |
Recommendations by use case
| Use case | First choice | Second choice | Notes |
|---|---|---|---|
| New C++ project | CMake + vcpkg | Meson + wrap | Ecosystem and IDE support |
| Windows-first | CMake + vcpkg | Visual Studio | MSVC integration |
| Large monorepo | Bazel | CMake | Google-style workflows |
| Legacy Makefile | Migrate to CMake | Keep | Prefer incremental migration |
| Embedded | CMake + Conan | CMake + vcpkg | Cross-compilation |
| Open-source library | CMake | Meson | Consumer convenience |
4. CMake in depth
Overview
CMake is a meta build system: it reads CMakeLists.txt and generates Makefiles, Ninja files, Visual Studio projects, and more. As of 2024 it represents on the order of 83% of the C++ build-tooling market.
Strengths
- Ecosystem: Most C++ libraries ship CMake support
- IDEs: CLion, Visual Studio, and VS Code all prioritize CMake
- Package managers: Straightforward integration with vcpkg and Conan
- Cross-platform: Windows, Linux, macOS, embedded
Weaknesses
- Syntax: Complex, with historical baggage
- Configure time: Can be slow on huge projects
- Diagnostics: Error messages are not always intuitive
Minimal example
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(myapp
src/main.cpp
src/utils.cpp
)
target_link_libraries(myapp PRIVATE
fmt::fmt
spdlog::spdlog
)
find_package example
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(Boost REQUIRED COMPONENTS system)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE
fmt::fmt
spdlog::spdlog
Boost::system
)
vcpkg toolchain
cmake -B build -S . \
-DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build
Ninja for faster builds
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
5. Meson in depth
Overview
Meson targets a fast, easy workflow. It uses a Python-like DSL and defaults to Ninja as the backend.
Strengths
- Configure speed: Often faster than CMake
- Syntax: Short and readable
- Dependencies: Builds in the right order automatically
Weaknesses
- Ecosystem: Smaller than CMake’s
- wrap: Fewer packages than vcpkg/Conan
- Windows: Compiler selection in VS Code can feel limited
Minimal example
project('myapp', 'cpp',
version : '1.0.0',
default_options : ['cpp_std=c++17']
)
fmt_dep = dependency('fmt')
spdlog_dep = dependency('spdlog')
executable('myapp',
'src/main.cpp',
dependencies : [fmt_dep, spdlog_dep]
)
wrap file (vendored dependency)
# subprojects/fmt.wrap
[wrap-file]
directory = fmt-9.1.0
source_url = https://github.com/fmtlib/fmt/archive/9.1.0.zip
source_filename = fmt-9.1.0.zip
source_hash = ...
Cross-compilation
# cross-arm.txt
[binaries]
c = 'arm-linux-gnueabihf-gcc'
cpp = 'arm-linux-gnueabihf-g++'
meson setup build --cross-file cross-arm.txt
meson compile -C build
6. Bazel in depth
Overview
Bazel is Google’s unified build system, optimized for large monorepos and reproducible builds. Rules are written in Starlark (Python-like).
Strengths
- Reproducibility: Hermetic builds, caching
- Scale: Incremental builds, remote cache
- Polyglot: C++, Java, Go, Python, and more
Weaknesses
- Learning curve: Steep
- Ecosystem: Stronger for Java/Go than for typical OSS C++
- Project layout:
WORKSPACEandBUILDconventions are opinionated
Minimal cc_binary
# BUILD
cc_binary(
name = "myapp",
srcs = ["src/main.cpp"],
deps = [
"@fmt//:fmt",
"@spdlog//:spdlog",
],
)
WORKSPACE (fetch dependency)
# WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "fmt",
urls = ["https://github.com/fmtlib/fmt/archive/refs/tags/9.1.0.zip"],
strip_prefix = "fmt-9.1.0",
)
Example cc_library for a vendored layout
# third_party/fmt/BUILD
cc_library(
name = "fmt",
srcs = glob(["src/**/*.cc"]),
hdrs = glob(["include/**/*.h"]),
includes = ["include"],
visibility = ["//visibility:public"],
)
7. Makefile in depth
Overview
Make is one of the oldest build tools; many legacy projects still rely on it. You invoke make at the command line.
Strengths
- Simplicity: Fine for small projects
- Ubiquity: Available on most Unix systems
- Flexibility: Easy to mix with shell scripts
Weaknesses
- Cross-platform: Extra work on Windows
- Dependencies: Hard to express complex graphs correctly
- IDEs: Tools like CLion prefer CMake
Minimal example
CXX = g++
CXXFLAGS = -std=c++17 -Wall -O2
LDFLAGS = -lfmt -lspdlog
SRCS = src/main.cpp src/utils.cpp
OBJS = $(SRCS:.cpp=.o)
TARGET = myapp
$(TARGET): $(OBJS)
$(CXX) $(OBJS) -o $(TARGET) $(LDFLAGS)
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
Auto-generated dependencies
-include $(SRCS:.cpp=.d)
%.d: %.cpp
@$(CXX) -MM $(CXXFLAGS) $< > $@.tmp && mv $@.tmp $@
8. Package managers: vcpkg & Conan
Roles
A build system (CMake, Meson) and a package manager (vcpkg, Conan) solve different problems:
- Build system: source → compile → link → binaries
- Package manager: download, build, and expose third-party libraries
vcpkg basics
// vcpkg.json
{
"name": "myapp",
"version": "1.0.0",
"dependencies": [
"fmt",
"spdlog",
"nlohmann-json"
]
}
# Point CMake at the vcpkg toolchain
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
cmake --build build
Conan basics
# conanfile.txt
[requires]
fmt/9.1.0
spdlog/1.11.0
[generators]
CMakeDeps
CMakeToolchain
conan install . --output-folder=build --build=missing
cmake -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
cmake --build build
vcpkg vs Conan
| Situation | vcpkg | Conan |
|---|---|---|
| Windows / Visual Studio | Strong | Moderate |
| Private in-house libraries | Overlay ports | Artifactory / remotes |
| Cross-compilation | Triplets | Profiles |
| Quick start | Strong | Moderate |
| Fine-grained versions | Baseline | Strong |
9. Common errors and fixes
CMake
Error 1: “Could not find a package configuration file”
CMake Error: Could not find a package configuration file provided by "fmt"
Cause: CMAKE_TOOLCHAIN_FILE not set, vcpkg/Conan not installed, or package not built.
Fix:
# vcpkg
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
# Conan: run conan install first
conan install . --output-folder=build --build=missing
cmake -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
Error 2: “undefined reference” / MSVC LNK2019
undefined reference to `fmt::v9::format(...)'
Cause: Missing target_link_libraries, or Debug/Release mismatch.
Fix:
# Link all dependencies
target_link_libraries(myapp PRIVATE fmt::fmt spdlog::spdlog)
# Align Debug/Release across deps
# Align vcpkg triplet or Conan profile build type
Error 3: “No such file or directory” (header)
fatal error: mylib.h: No such file or directory
Cause: Missing target_include_directories, or wrong PUBLIC/PRIVATE propagation.
Fix:
# Expose includes from libraries with PUBLIC when needed
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# Or rely on transitive usage requirements
target_link_libraries(myapp PRIVATE mylib)
vcpkg
Error 4: “Version conflict” / builtin-baseline
Error: Version conflict: fmt 9.1.0 vs 10.1.0
Cause: Dependency version clash or moving baseline.
Fix:
// vcpkg.json — pin versions
{
"dependencies": [
"fmt",
{"name": "spdlog", "version>=": "1.11.0"}
],
"builtin-baseline": "<pinned commit hash>"
}
git add vcpkg.lock
Conan
Error 5: “Version conflict”
ERROR: Version conflict: fmt/9.1.0 vs fmt/10.1.0
Cause: Conflicting requirements in the graph.
Fix:
# conanfile.py — override
def requirements(self):
self.requires("fmt/9.1.0", override=True)
conan lock create . --lockfile=conan.lock
conan install . --lockfile=conan.lock
Meson
Error 6: “dependency not found”
meson.build:5:0: ERROR: Dependency "fmt" not found
Cause: Missing wrap or system package.
Fix:
meson wrap install fmt
# Or install system packages, e.g. apt install libfmt-dev / brew install fmt
Makefile
Error 7: “missing separator”
Makefile:10: *** missing separator. Stop.
Cause: Spaces instead of a leading tab for recipe lines.
Fix:
# Wrong — spaces
target:
g++ main.cpp -o target
# Correct — tab
target:
g++ main.cpp -o target
Error summary
| Error | Layer | Cause | Fix |
|---|---|---|---|
| Could not find package | CMake | No toolchain | CMAKE_TOOLCHAIN_FILE |
| undefined reference | Any | Missing link | target_link_libraries |
| No such header | CMake | Include path | target_include_directories |
| Version conflict | vcpkg/Conan | Version skew | Lock files / overrides |
| missing separator | Makefile | Spaces vs tab | Use tabs |
| dependency not found | Meson | No wrap | meson wrap install |
10. Migration guide
Makefile → CMake
flowchart LR
A[Analyze Makefile] --> B[Author CMakeLists.txt]
B --> C[Verify parallel builds]
C --> D[Update CI]
Steps:
- Extract
SRCS,LDFLAGS,CXXFLAGS, and dependencies. - Author a minimal
CMakeLists.txt(example below). - Compare outputs with the old build (e.g.
diffon artifacts where applicable). - Change CI from
maketocmake --build build.
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(myapp
src/main.cpp
src/utils.cpp
)
target_link_libraries(myapp PRIVATE
fmt::fmt
pthread
)
CMake → Meson
When it fits: Medium-sized projects where CMake complexity hurts more than it helps.
Steps:
- Write
meson.buildwith the same structure (example below). - Resolve dependencies with
wrap. - Add Meson to CI.
- Run CMake and Meson in parallel until parity, then remove CMake.
project('myapp', 'cpp', default_options : ['cpp_std=c++17'])
executable('myapp', 'src/main.cpp', 'src/utils.cpp', dependencies : [dependency('fmt'), dependency('spdlog')])
vcpkg → Conan (or the reverse)
Warning: Do not mix both in one project—plan a full migration.
vcpkg → Conan:
- Map
vcpkg.jsondependencies intoconanfile.txt/.py. - Validate with
conan installand builds. - Remove vcpkg from CI and delete
vcpkg.json.
Conan → vcpkg:
- Map Conan requirements into
vcpkg.json. - Point
CMAKE_TOOLCHAIN_FILEat vcpkg. - Drop
conan installfrom CI.
11. Production patterns
Pattern 1: Reproducible builds
Goal: Same source and config produce the same result everywhere.
CMake + vcpkg:
// vcpkg.json — pin baseline
{
"builtin-baseline": "abc123def456...",
"dependencies": ["fmt", "spdlog"]
}
git add vcpkg.lock
CMake + Conan:
conan lock create .
conan install . --lockfile=conan.lock
Pattern 2: CI cache
vcpkg:
# GitHub Actions
- uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/vcpkg/buildtrees
${{ github.workspace }}/vcpkg/packages
key: vcpkg-${{ runner.os }}-${{ hashFiles('vcpkg.json', 'vcpkg.lock') }}
Conan:
- uses: actions/cache@v4
with:
path: ~/.conan2
key: conan-${{ runner.os }}-${{ hashFiles('conanfile.*', 'conan.lock') }}
Pattern 3: CMake Presets
// CMakePresets.json
{
"version": 3,
"configurePresets": [
{
"name": "vcpkg-default",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake"
}
}
]
}
cmake --preset vcpkg-default
cmake --build --preset vcpkg-default
Pattern 4: Docker build image
# Dockerfile.build
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y cmake g++ ninja-build git
RUN git clone https://github.com/microsoft/vcpkg.git /vcpkg && \
/vcpkg/bootstrap-vcpkg.sh
WORKDIR /src
COPY . .
RUN cmake -B build -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake && \
cmake --build build
Pattern 5: Standard repo layout
my-project/
├── CMakeLists.txt
├── vcpkg.json
├── vcpkg.lock
├── CMakePresets.json
├── src/
│ └── main.cpp
├── include/
├── tests/
└── .github/
└── workflows/
└── build.yml
Pattern 6: Split build types
# Debug vs Release flags
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(myapp PRIVATE DEBUG_MODE)
endif()
# Pick static vs shared via vcpkg triplet, e.g. x64-windows-static
12. Decision guide
Flowchart
flowchart TD
A[New project?] -->|Yes| B{Team size?}
A -->|No| C{Legacy?}
B -->|Small–medium| D[CMake + vcpkg]
B -->|Large| E[Consider Bazel]
C -->|Makefile| F[Migrate to CMake]
C -->|CMake| G[Keep or try Meson]
D --> H[Ecosystem and IDE support]
E --> I[Scale and reproducibility]
Checklist by question
| Question | CMake | Meson | Bazel |
|---|---|---|---|
| Team already knows CMake? | Yes | — | — |
| Build speed is the top priority? | Use Ninja | Strong | Strong |
| Google-style monorepo? | — | — | Strong |
| Stuck on Makefiles? | Migrate | — | — |
| Windows-first shop? | Strong | Moderate | Moderate |
| Private library distribution? | Conan | Conan | rules_cc |
Final recommendations
| Situation | Recommendation |
|---|---|
| Most C++ projects | CMake + vcpkg |
| Configure speed & simple syntax | Meson |
| Huge monorepo | Bazel |
| Legacy | Makefile → CMake |
13. Implementation checklist
Choosing the stack
- Team size and skill set
- Cross-platform requirements
- Package manager: vcpkg vs Conan
- IDE support
CMake projects
- Set
CMAKE_CXX_STANDARD - Link all deps via
target_link_libraries - Use PUBLIC/PRIVATE correctly for includes
- Prefer Ninja (
-G Ninja)
vcpkg
- Author
vcpkg.json - Pin with
builtin-baselineorvcpkg.lock - Set
CMAKE_TOOLCHAIN_FILE - Configure CI cache
Conan
- Author
conanfile.txtorconanfile.py - Create and commit
conan.lock - Run
conan installbefore CMake - Configure CI cache
Reproducibility
- Commit lock files
- Pin baselines and versions
- Optional: Docker image for builds
CI/CD
- Build caching
- CMake Presets or equivalent shared flags
- Multi-platform builds when required
Summary
| Task | CMake | Meson | Bazel |
|---|---|---|---|
| Project files | CMakeLists.txt | meson.build | BUILD, WORKSPACE |
| Dependencies | find_package, vcpkg, Conan | wrap, dependency | rules_cc, http_archive |
| Build | cmake --build | meson compile | bazel build |
| Ecosystem | Largest | Growing | Google-centric |
Principles:
- One standard per team: build system and package manager.
- Reproducible builds: lock files and pinned versions.
- Cache in CI: shorter feedback loops.
- Migrate incrementally: especially from legacy Makefiles.
Keywords covered: C++ build systems, CMake, Meson, Bazel, vcpkg, Conan, Makefile, comparison, migration.
Related posts
- CMake intro: automating multi-file builds (
CMakeLists.txtbasics) - C++ CMake guide: cross-platform builds, CMake 3.28+, presets, modules
- CMake Presets: multi-platform, vcpkg, Conan, CI/CD