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
- Project structure
- Targets and libraries
- Target properties
- External libraries
- Build options
- Common errors
- Practical patterns
- Best practices
Advanced topics: FetchContent, ExternalProject, add_custom_command, generator expressions, install + Config.cmake, protobuf integration.
1. Project structure
Recommended layout
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/.liblinked into the executable at link time. - SHARED:
.so/.dllloaded 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)
| Issue | Fix |
|---|---|
| Missing headers | PUBLIC target_include_directories + correct usage |
| Undefined reference | List all libs in target_link_libraries; fix subdirectory order |
| find_package fails | CMAKE_PREFIX_PATH, vcpkg toolchain, Conan |
| Duplicate symbols | inline / single definition in .cpp |
| DLL not found | Align RUNTIME_OUTPUT_DIRECTORY with DLL location |
| Generator expr typo | Use $<OR:$<...>,$<...>> not commas inside one $<CXX_COMPILER_ID:...> incorrectly |
| ExternalProject rebuilds | Set BUILD_BYPRODUCTS |
| Custom command not run | Attach 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_packageconsumers. - 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)
Related posts
- Package managers #17-2
- CMake intro #4
- Environment setup #1
Summary
| Task | Command |
|---|---|
| Library | add_library |
| Executable | add_executable |
| Link | target_link_libraries |
| Includes | target_include_directories |
| External | find_package, FetchContent |
| Options | option |
| Tests | enable_testing, add_test |
Next: Package managers (#17-2)
Previous: Logging and assertions (#16-3)