Advanced CMake for C++: Multi-Target Projects, External Dependencies, and Large-Scale Builds

Advanced CMake for C++: Multi-Target Projects, External Dependencies, and Large-Scale Builds

이 글의 핵심

Hands-on advanced CMake: split static libraries with add_library, propagate includes with PUBLIC/PRIVATE, integrate find_package and FetchContent, fix link and header errors, and ship installable packages.

Introduction: builds get complex as projects grow

“Every time I add a library, the build breaks”

Projects with several libraries and executables often suffer tangled build settings when new code is added.
Requirements: CMake 3.10+, g++/Clang or MSVC. If you know the basics from #4 CMake intro, this post extends the same toolchain with multi-target workflows, options, and package integration.

Use add_library for shared code and target_link_libraries for dependencies. Splitting by target clarifies build order, include paths, and platform options—and later integrates cleanly with vcpkg or Conan.


Pain scenarios from real CMake usage

Scenario 1: “Cannot find header”

fatal error: mylib.h: No such file or directory

Cause: target_include_directories only PRIVATE, or wrong link order—library A uses B but B’s include path does not propagate to A.

Scenario 2: “undefined reference”

Cause: Missing target_link_libraries, STATIC/SHARED confusion, or add_subdirectory order so a target is used before it exists.

Scenario 3: Windows-only failures

Cause: Missing platform libs (ws2_32, …), path separators, or DLL export macros.

Scenario 4: External library version skew

Cause: System find_package picking mixed Boost versions—pin versions with FetchContent.

Scenario 5: Slow builds

Cause: Duplicate compilation, no PCH, no parallel -j.

Scenario 6: Generated code not built

Cause: .proto or codegen not wired with add_custom_command—missing .pb.cc at link time.

Scenario 7: find_package after install

Cause: Missing Config.cmake / Version.cmake or CMAKE_PREFIX_PATH.

Scenario 8: CI fetch timeouts

Cause: FetchContent cloning full history—use GIT_SHALLOW TRUE.


Problem CMakeLists (anti-pattern)

Listing the same util.cpp, db.cpp in every executable duplicates work and rebuilds everything on any change.

# Bad: everything in one place
add_executable(app1 main1.cpp util.cpp db.cpp)
add_executable(app2 main2.cpp util.cpp db.cpp)
add_executable(app3 main3.cpp util.cpp db.cpp)

Better: add_library(util STATIC util.cpp), add_library(db STATIC db.cpp), then each app links util and db once.

add_library(util STATIC util.cpp)
add_library(db STATIC db.cpp)
add_executable(app1 main1.cpp)
target_link_libraries(app1 PRIVATE util db)

Project dependency architecture

flowchart TB
    subgraph apps["Executables"]
        app1[app1]
        app2[app2]
        app3[app3]
    end

    subgraph libs["Libraries"]
        util[util]
        db[db]
    end

    subgraph external["External"]
        boost[Boost]
        json[nlohmann_json]
    end

    app1 --> util
    app1 --> db
    app2 --> util
    app2 --> db
    app3 --> util
    app3 --> db
    db --> boost
    app1 --> json

Table of contents

  1. Project structure
  2. Targets and libraries
  3. Target properties
  4. External libraries
  5. Build options
  6. Common errors
  7. Practical patterns
  8. Best practices

Advanced topics: FetchContent, ExternalProject, add_custom_command, generator expressions, install + Config.cmake, protobuf integration.


1. Project structure

Use a root CMakeLists.txt for global settings (C++ standard, build type) and add_subdirectory for src, lib, tests, external.

myproject/
├── CMakeLists.txt
├── src/
│   ├── CMakeLists.txt
│   └── ...
├── lib/
│   └── mylib/
├── tests/
└── external/

Root CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

add_subdirectory(lib)
add_subdirectory(src)
add_subdirectory(tests)

2. Targets and libraries

  • STATIC: .a / .lib linked into the executable at link time.
  • SHARED: .so / .dll loaded at runtime.
  • INTERFACE: header-only—expose includes with target_include_directories(mylib INTERFACE include/).

PUBLIC vs PRIVATE vs INTERFACE

PUBLIC on target_link_libraries(B PUBLIC A) means consumers of B also need A. PRIVATE keeps A internal to B.

target_link_libraries(B PUBLIC A)
target_link_libraries(C PRIVATE B)
# C gets A automatically through B’s PUBLIC edge.

3. Target properties

Use target_include_directories, target_compile_options, target_link_libraries with PUBLIC/PRIVATE/INTERFACE as appropriate.

Generator expressions

$<CONFIG:Debug>, $<BUILD_INTERFACE:...>, $<INSTALL_INTERFACE:...> apply values at generate time, not configure time.

target_compile_definitions(mylib PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
)
target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

4. External libraries

find_package

find_package(Boost REQUIRED COMPONENTS system filesystem)
target_link_libraries(myapp PRIVATE Boost::system Boost::filesystem)

FetchContent

include(FetchContent)
FetchContent_Declare(
    json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(json)
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)

find_package first, else FetchContent

Try system/vcpkg package; fall back to git tag for reproducible builds.

ExternalProject

Builds in the build phase (not configure)—use for non-CMake deps or install-then-find workflows.

add_custom_command / protobuf

Wire generated sources as outputs and add them to an add_library so CMake knows when to run the generator.


5. Build options

option(BUILD_TESTS "Build tests" ON)
if(BUILD_TESTS)
    add_subdirectory(tests)
endif()

CMAKE_BUILD_TYPE: Debug (-g -O0), Release (-O3 -DNDEBUG), RelWithDebInfo.

Platform blocks: if(WIN32), elseif(APPLE), elseif(UNIX) for macros and extra libraries (ws2_32, pthread).


6. Common errors (summary)

IssueFix
Missing headersPUBLIC target_include_directories + correct usage
Undefined referenceList all libs in target_link_libraries; fix subdirectory order
find_package failsCMAKE_PREFIX_PATH, vcpkg toolchain, Conan
Duplicate symbolsinline / single definition in .cpp
DLL not foundAlign RUNTIME_OUTPUT_DIRECTORY with DLL location
Generator expr typoUse $<OR:$<...>,$<...>> not commas inside one $<CXX_COMPILER_ID:...> incorrectly
ExternalProject rebuildsSet BUILD_BYPRODUCTS
Custom command not runAttach OUTPUT to a target’s sources or a custom target dependency

7. Practical patterns

  • BUILD_INTERFACE / INSTALL_INTERFACE for reusable libraries.
  • CTest + FetchContent googletest for tests.
  • install(TARGETS … EXPORT) + install(EXPORT) for find_package consumers.
  • write_basic_package_version_file + configure_package_config_file for Config.cmake.
  • target_precompile_headers (CMake 3.16+) to speed builds.

8. Best practices

  • Modularize with libraries; minimize duplicate sources.
  • Mark PUBLIC vs PRIVATE deliberately.
  • Pin dependency versions (FetchContent GIT_TAG).
  • Platform-specific code behind if(WIN32) etc.
  • Options for tests, examples, sanitizers.
  • Define install rules for redistribution.

Build flow (sequence)

sequenceDiagram
    participant Dev as Developer
    participant CMake as CMake configure
    participant Fetch as FetchContent
    participant Build as Build system

    Dev->>CMake: cmake -B build
    CMake->>Fetch: Download external deps
    Fetch->>CMake: Targets created
    CMake->>CMake: Dependency graph
    CMake->>Build: Generate Ninja/Makefile

    Dev->>Build: cmake --build build
    Build->>Build: lib (mylib)
    Build->>Build: src (myapp)
    Build->>Build: tests (optional)

  • Package managers #17-2
  • CMake intro #4
  • Environment setup #1

Summary

TaskCommand
Libraryadd_library
Executableadd_executable
Linktarget_link_libraries
Includestarget_include_directories
Externalfind_package, FetchContent
Optionsoption
Testsenable_testing, add_test

Next: Package managers (#17-2)

Previous: Logging and assertions (#16-3)