Phase 5 · Professional C++ EngineeringModule 27~58 min read

CMake, Dependencies, Libraries & Packaging

Build, consume, install, version, test, and package C++ targets reproducibly with modern CMake and continuous integration.

What you'll learn

A professional C++ build describes targets and their requirements—not a pile of global flags. You will organize translation units, create modern CMake targets, consume dependencies, install packages, manage ABI boundaries, and automate builds across platforms.

By the end, you'll be able to:

  • Explain translation units, linking, and library kinds
  • Create target-based CMake with transitive usage requirements
  • Integrate versioned dependencies without hidden global state
  • Install, export, package, test, and release a reusable target

Translation units and linking

Each source file after preprocessing is a translation unit compiled independently into an object file. The linker resolves definitions across objects and libraries. Headers should contain declarations and templates that consumers need; non-inline definitions generally belong in one source file to satisfy the One Definition Rule.

ArtifactRole
Object fileCompiled code and data from one translation unit
Static libraryArchive whose needed objects are copied into consumers
Shared libraryDynamically loaded code with a runtime ABI boundary
ExecutableLinked program entry point and dependencies
Header-only libraryDefinitions instantiated or inlined in consumer translation units

Key idea

Compilation errors belong to one translation unit; unresolved symbols and duplicate definitions are usually link-stage problems. Identify the stage before changing code.

Modern CMake targets

A target describes an executable or library plus the requirements needed to build and use it.PRIVATE requirements affect only the target, INTERFACE requirements affect consumers, and PUBLIC requirements affect both. Avoid directory-wide flags that silently leak into unrelated targets.

CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(tasker VERSION 1.0.0 LANGUAGES CXX)

add_library(tasker src/task.cpp)
add_library(tasker::tasker ALIAS tasker)
target_compile_features(tasker PUBLIC cxx_std_20)
target_include_directories(tasker
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
target_compile_options(tasker PRIVATE
  $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall;-Wextra;-Wpedantic>
  $<$<CXX_COMPILER_ID:MSVC>:/W4;/permissive->
)

add_executable(tasker_cli app/main.cpp)
target_link_libraries(tasker_cli PRIVATE tasker::tasker)

Tip

Express the required language level with target_compile_features. Do not manually paste compiler-specific -std= flags into every target.

Dependencies and usage requirements

Prefer dependencies that export CMake package targets, such asSomeLibrary::Core. Link that target and inherit its include paths, definitions, and transitive libraries. Pin compatible versions through a package manager, vendored source, or reviewed FetchContent declaration according to the project's reproducibility policy.

dependency.cmake
find_package(fmt 10 CONFIG REQUIRED)
target_link_libraries(tasker_cli PRIVATE fmt::fmt)

# Or, when project policy permits fetching reviewed source:
include(FetchContent)
FetchContent_Declare(example
  GIT_REPOSITORY https://example.invalid/example.git
  GIT_TAG        0123456789abcdef0123456789abcdef01234567
)
Pin immutable revisions and use a real reviewed source; the example URL is intentionally inert.

Watch out

A branch name such as main is not a reproducible dependency version. Record exact versions or immutable revisions and verify source integrity in the chosen dependency workflow.

Build and analysis configurations

Debug, release, sanitizer, coverage, and profiling builds have different goals. Prefer CMake presets to capture generators, toolchains, cache variables, and build directories without asking developers to remember long command lines. Keep output directories separate so configurations never contaminate one another.

CMakePresets.json
{
  "version": 6,
  "configurePresets": [
    {
      "name": "debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
    },
    {
      "name": "release",
      "inherits": "debug",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
    }
  ]
}
  • Compile tests against the same public target consumers use
  • Make sanitizer options a target or preset concern
  • Generate compile_commands.json for analysis tools when supported
  • Test both optimized and diagnostic configurations

Install, export, and package

Installation copies binaries, libraries, headers, and package metadata into a relocatable prefix. Exported targets let downstream CMake projects call find_package and link the same names used in-tree. Packages also need version compatibility policy, licenses, runtime dependencies, and platform-appropriate archive or installer formats.

install.cmake
include(GNUInstallDirs)
install(TARGETS tasker
  EXPORT taskerTargets
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
  INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT taskerTargets
  FILE taskerTargets.cmake
  NAMESPACE tasker::
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/tasker
)

ABI, CI, and releases

A shared-library ABI includes symbol names, calling conventions, layouts, compiler runtime expectations, and exception boundaries. Source compatibility does not guarantee binary compatibility. Keep unstable implementation behind a Pimpl or C boundary when ABI stability matters, control symbol visibility, and version breaking releases deliberately.

  • Build on every supported compiler, operating system, and architecture tier
  • Run unit, integration, sanitizer, and packaging tests
  • Install into a clean prefix and build a separate consumer against it
  • Cache dependencies carefully without caching stale build outputs
  • Create signed or checksummed release artifacts from tagged source
  • Publish compiler, standard-library, runtime, and platform compatibility

Note

A successful in-tree build does not prove the package works. A clean downstream consumer test catches missing headers, wrong install paths, and leaked private dependencies.

Recap & quick check

Key takeaways

  • Translation units compile independently; the linker resolves cross-unit definitions.
  • Modern CMake expresses build and usage requirements on targets with scoped visibility.
  • Dependencies should be versioned, immutable, and consumed through exported targets.
  • Presets make diagnostic and optimized configurations repeatable.
  • A professional release validates installation, downstream consumption, ABI policy, and platform matrices.

Quick check

1. What does PUBLIC mean in target_link_libraries?

2. Which stage reports an unresolved external symbol?

3. Why pin an immutable dependency revision?

4. What validates an installed CMake package best?

Next: Module 28 — Architecture, Design Patterns & API Design, where target boundaries become maintainable software boundaries.