본문으로 건너뛰기 C++ Build Systems Compared: CMake, Meson, Bazel, Makefile, and Choosing a Package Manager

C++ Build Systems Compared: CMake, Meson, Bazel, Makefile, and Choosing a Package Manager

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

  1. Problem scenarios in detail
  2. Build-system taxonomy and architecture
  3. Comparison tables
  4. CMake in depth
  5. Meson in depth
  6. Bazel in depth
  7. Makefile in depth
  8. Package managers: vcpkg & Conan
  9. Common errors and fixes
  10. Migration guide
  11. Production patterns
  12. Decision guide
  13. 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

TermMeaning
Meta buildTools like CMake and Meson that read project files and emit native build files
Native buildTools like Make, Ninja, and MSBuild that actually compile and link
Package managerTools like vcpkg and Conan that fetch, build, and expose third-party libraries
ToolchainCompiler + linker + flags (e.g. GCC 12, Clang 15, MSVC 2022)

3. Comparison tables

Core build-system comparison

ItemCMakeMesonBazelMakefile
RoleMeta buildMeta buildUnified buildNative build
Market share~83% (2024)~5%~3%Widespread legacy
Config languageCMake DSLMeson (Python-like)Starlark (Python-like)Makefile syntax
Learning curveMedium–highLow–mediumHighMedium
Cross-platformStrongStrongStrongManual
Configure speedTypicalFastFastN/A
Build speedFast with NinjaNinja by defaultFastMake by default
IDE supportExcellentFairFairLimited
Package integrationvcpkg, Conan, FetchContentwrap, Conanrules_cc, ConanManual
Very large projectsViableViableOptimizedHard
EcosystemLargestGrowingGoogle-centricLegacy-heavy

Package managers: vcpkg vs Conan

ItemvcpkgConan
RoleC++ package managerC++ package manager
Build integrationCMake-centricEmits CMake (and others)
Package count2000+1500+
InstallGit clone + bootstrappip install conan
Manifestvcpkg.jsonconanfile.txt / .py
Version pinningbuiltin-baseline, vcpkg.lockExplicit versions, conan.lock
CMake integrationToolchain fileconan install → toolchain
Microsoft ecosystemStrongModerate
Private packagesOverlay portsPrivate remotes (e.g. Artifactory)

Recommendations by use case

Use caseFirst choiceSecond choiceNotes
New C++ projectCMake + vcpkgMeson + wrapEcosystem and IDE support
Windows-firstCMake + vcpkgVisual StudioMSVC integration
Large monorepoBazelCMakeGoogle-style workflows
Legacy MakefileMigrate to CMakeKeepPrefer incremental migration
EmbeddedCMake + ConanCMake + vcpkgCross-compilation
Open-source libraryCMakeMesonConsumer 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: WORKSPACE and BUILD conventions 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

SituationvcpkgConan
Windows / Visual StudioStrongModerate
Private in-house librariesOverlay portsArtifactory / remotes
Cross-compilationTripletsProfiles
Quick startStrongModerate
Fine-grained versionsBaselineStrong

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

ErrorLayerCauseFix
Could not find packageCMakeNo toolchainCMAKE_TOOLCHAIN_FILE
undefined referenceAnyMissing linktarget_link_libraries
No such headerCMakeInclude pathtarget_include_directories
Version conflictvcpkg/ConanVersion skewLock files / overrides
missing separatorMakefileSpaces vs tabUse tabs
dependency not foundMesonNo wrapmeson 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:

  1. Extract SRCS, LDFLAGS, CXXFLAGS, and dependencies.
  2. Author a minimal CMakeLists.txt (example below).
  3. Compare outputs with the old build (e.g. diff on artifacts where applicable).
  4. Change CI from make to cmake --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:

  1. Write meson.build with the same structure (example below).
  2. Resolve dependencies with wrap.
  3. Add Meson to CI.
  4. 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:

  1. Map vcpkg.json dependencies into conanfile.txt / .py.
  2. Validate with conan install and builds.
  3. Remove vcpkg from CI and delete vcpkg.json.

Conan → vcpkg:

  1. Map Conan requirements into vcpkg.json.
  2. Point CMAKE_TOOLCHAIN_FILE at vcpkg.
  3. Drop conan install from 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

QuestionCMakeMesonBazel
Team already knows CMake?Yes
Build speed is the top priority?Use NinjaStrongStrong
Google-style monorepo?Strong
Stuck on Makefiles?Migrate
Windows-first shop?StrongModerateModerate
Private library distribution?ConanConanrules_cc

Final recommendations

SituationRecommendation
Most C++ projectsCMake + vcpkg
Configure speed & simple syntaxMeson
Huge monorepoBazel
LegacyMakefile → 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-baseline or vcpkg.lock
  • Set CMAKE_TOOLCHAIN_FILE
  • Configure CI cache

Conan

  • Author conanfile.txt or conanfile.py
  • Create and commit conan.lock
  • Run conan install before 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

TaskCMakeMesonBazel
Project filesCMakeLists.txtmeson.buildBUILD, WORKSPACE
Dependenciesfind_package, vcpkg, Conanwrap, dependencyrules_cc, http_archive
Buildcmake --buildmeson compilebazel build
EcosystemLargestGrowingGoogle-centric

Principles:

  1. One standard per team: build system and package manager.
  2. Reproducible builds: lock files and pinned versions.
  3. Cache in CI: shorter feedback loops.
  4. Migrate incrementally: especially from legacy Makefiles.

Keywords covered: C++ build systems, CMake, Meson, Bazel, vcpkg, Conan, Makefile, comparison, migration.


  • CMake intro: automating multi-file builds (CMakeLists.txt basics)
  • C++ CMake guide: cross-platform builds, CMake 3.28+, presets, modules
  • CMake Presets: multi-platform, vcpkg, Conan, CI/CD