Skip to main content

Updated Jun 4, 2026

Linking Libraries

Understanding Library Linking​

Linking is the process of combining compiled object files and libraries into a final executable or library. In CMake, linking determines which libraries your target depends on and how those dependencies are propagated through your build system.

The distinction between compile-time requirements (headers, definitions) and link-time requirements (actual library binaries) is crucial for proper dependency management. CMake's modern approach uses target-based linking, which automatically handles both aspects through the target_link_libraries() command.

This is the primary command for establishing dependencies between targets. It does more than just linking - it also propagates include directories, compile definitions, and compiler flags based on the visibility specifiers you choose.

Basic Syntax​

target_link_libraries(target
<PRIVATE|PUBLIC|INTERFACE> library1 library2 ...
)

The target must already exist (created with add_executable() or add_library()). Libraries can be CMake targets or system libraries.

Simple Example​

# Create a library
add_library(math_operations STATIC
src/add.cpp
src/multiply.cpp
)

# Create an executable
add_executable(calculator main.cpp)

# Link the library to the executable
target_link_libraries(calculator PRIVATE math_operations)

In this example, calculator needs math_operations to build and run, but since it's an executable, nothing else will depend on it, so we use PRIVATE.

Visibility Keywords: The Heart of Modern CMake​

The visibility keywords (PRIVATE, PUBLIC, INTERFACE) control how dependencies and their properties propagate through your build system. Understanding these is essential for maintainable CMake projects.

PRIVATE​

Use PRIVATE when a library is an implementation detail that consumers don't need to know about. The dependency is used internally but doesn't appear in your public API.

add_library(database STATIC
src/database.cpp
src/connection.cpp
)

# SQLite is used internally but not exposed in headers
find_package(SQLite3 REQUIRED)
target_link_libraries(database PRIVATE SQLite3::SQLite3)

When to use PRIVATE:

  • The library is used only in .cpp files, not headers
  • You want to hide implementation details
  • The dependency won't be needed by code that uses your library
  • You're linking to an executable (executables are always endpoints)

What gets propagated: Nothing to dependents. Only the current target gets the library, includes, and compile options.

PUBLIC​

Use PUBLIC when a library appears in your public interface - when users of your library also need to know about and link to this dependency.

add_library(graphics_engine STATIC
src/renderer.cpp
src/shader.cpp
)

# OpenGL is used and exposed in our public headers
find_package(OpenGL REQUIRED)
target_link_libraries(graphics_engine PUBLIC OpenGL::GL)

When to use PUBLIC:

  • The dependency appears in your public header files
  • Users of your library need to link to the same dependency
  • You're creating a wrapper or facade around another library
  • Your API directly exposes types from the dependency

What gets propagated: Everything - include directories, compile definitions, compile options, and the link dependency itself.

INTERFACE​

Use INTERFACE when a library is needed by consumers but not by the library itself. This is primarily used for header-only libraries or when you're propagating requirements without using them yourself.

# Header-only library
add_library(math_utilities INTERFACE)

# Only consumers need the include directory
target_include_directories(math_utilities INTERFACE
include/
)

# Only consumers need C++17
target_compile_features(math_utilities INTERFACE
cxx_std_17
)

When to use INTERFACE:

  • You're creating a header-only library
  • You need to pass requirements to consumers without using them
  • You're creating an "umbrella" target that just groups dependencies

What gets propagated: Everything to dependents, but the library itself doesn't use these dependencies.

Transitive Dependencies​

One of CMake's most powerful features is automatic handling of transitive dependencies. When you link with PUBLIC, the dependencies automatically propagate down the dependency chain.

How Transitivity Works​

# Low-level library
add_library(logging STATIC logging.cpp)
target_include_directories(logging PUBLIC include/logging)

# Mid-level library uses and exposes logging
add_library(database STATIC database.cpp)
target_link_libraries(database PUBLIC logging)
target_include_directories(database PUBLIC include/database)

# High-level library uses database
add_library(application_logic STATIC app.cpp)
target_link_libraries(application_logic PUBLIC database)

# Executable uses application_logic
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE application_logic)

What myapp automatically gets:

  • Direct link to application_logic
  • Transitive link to database (via application_logic)
  • Transitive link to logging (via database)
  • Include directories from all three libraries
  • Any compile definitions from all three libraries

This happens automatically because of the PUBLIC links. No manual tracking needed.

Breaking the Chain​

You can break transitive propagation using PRIVATE:

add_library(A STATIC a.cpp)
add_library(B STATIC b.cpp)
add_library(C STATIC c.cpp)

# B uses A but doesn't expose it
target_link_libraries(B PRIVATE A)

# C uses B
target_link_libraries(C PUBLIC B)

# An executable using C
add_executable(app main.cpp)
target_link_libraries(app PRIVATE C)

What app gets:

  • C (direct)
  • B (transitive via C)
  • NOT A (B's PRIVATE dependency)

Linking Static Libraries​

Static libraries (.a on Unix, .lib on Windows) are archives of compiled object files that get incorporated directly into the final binary at link time.

Characteristics​

When you link a static library, the linker extracts only the object files you actually use and copies them into your executable. This creates a self-contained binary with no runtime dependencies on those libraries.

add_library(mylib STATIC
src/core.cpp
src/utils.cpp
src/helpers.cpp
)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)

Advantages:

  • No runtime dependencies - easier distribution
  • Potential for better optimization (linker can see more code)
  • Slightly faster startup (no dynamic linking overhead)

Disadvantages:

  • Larger executable size
  • Memory duplication if multiple processes use the same code
  • Must relink executables to update the library
  • Code bloat if multiple executables link the same static library

Position Independent Code​

When a static library will be linked into a shared library, it must be compiled with position-independent code (PIC):

add_library(my_static_lib STATIC lib.cpp)

# Required if this static lib will be linked into a shared lib
set_target_properties(my_static_lib PROPERTIES
POSITION_INDEPENDENT_CODE ON
)

# Now we can use it in a shared library
add_library(my_shared_lib SHARED shared.cpp)
target_link_libraries(my_shared_lib PRIVATE my_static_lib)

This is necessary because shared libraries must be loadable at any memory address, which requires PIC.

Linking Shared Libraries​

Shared libraries (.so on Unix, .dll on Windows, .dylib on macOS) are loaded at runtime, allowing multiple processes to share the same library code in memory.

Runtime Dependencies​

Unlike static libraries, shared libraries remain separate files that must be present at runtime:

add_library(mylib SHARED
src/api.cpp
src/implementation.cpp
)

# Set version information
set_target_properties(mylib PROPERTIES
VERSION 2.1.4
SOVERSION 2
)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)

On Linux, this creates:

  • libmylib.so.2.1.4 - the actual library file
  • libmylib.so.2 - symlink (API compatibility version)
  • libmylib.so - symlink (for linking at build time)

Runtime behavior: When you run app, the system's dynamic linker searches for libmylib.so.2 in standard locations (/usr/lib, /usr/local/lib, or paths in LD_LIBRARY_PATH).

RPATH Configuration​

RPATH (Run-time search path) tells the executable where to find shared libraries. By default, CMake sets up reasonable RPATH behavior:

# CMake's default RPATH settings (usually don't need to change)
set(CMAKE_SKIP_BUILD_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

For custom library locations:

add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)

# Add custom RPATH
set_target_properties(app PROPERTIES
INSTALL_RPATH "/opt/myapp/lib:$ORIGIN/../lib"
)

The $ORIGIN variable expands to the executable's directory, allowing relative paths.

Linking System Libraries​

System libraries are external libraries installed on the system, typically found through find_package() or find_library().

Using find_package()​

The modern approach uses imported targets, which encapsulate all necessary information:

# Find the package
find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)

add_executable(myapp main.cpp)

# Link using imported targets (preferred)
target_link_libraries(myapp PRIVATE
Threads::Threads # Thread library (pthread on Unix)
ZLIB::ZLIB # Compression library
OpenSSL::SSL # SSL library
OpenSSL::Crypto # Crypto library
)

Why imported targets are better:

  • Automatically include the necessary include directories
  • Handle compile definitions
  • Work correctly for both Debug and Release builds
  • Provide better error messages
  • More portable across platforms

Legacy Variable-Based Linking​

Older CMake code uses variables. While this still works, it's less maintainable:

find_package(ZLIB REQUIRED)

add_executable(myapp main.cpp)

# Old style (avoid if possible)
target_include_directories(myapp PRIVATE ${ZLIB_INCLUDE_DIRS})
target_link_libraries(myapp PRIVATE ${ZLIB_LIBRARIES})

# Modern style (preferred)
target_link_libraries(myapp PRIVATE ZLIB::ZLIB)

Platform-Specific System Libraries​

Different platforms have different system libraries. Handle them conditionally:

add_executable(myapp main.cpp)

if(WIN32)
# Windows-specific libraries
target_link_libraries(myapp PRIVATE
ws2_32 # Winsock
user32 # Windows API
gdi32 # Graphics
)
elseif(APPLE)
# macOS frameworks
find_library(COCOA_LIBRARY Cocoa REQUIRED)
find_library(OPENGL_LIBRARY OpenGL REQUIRED)
target_link_libraries(myapp PRIVATE
${COCOA_LIBRARY}
${OPENGL_LIBRARY}
)
elseif(UNIX)
# Linux libraries
find_package(Threads REQUIRED)
target_link_libraries(myapp PRIVATE
Threads::Threads
dl # Dynamic loading
m # Math library
)
endif()

Linking Order and Circular Dependencies​

The order in which you link libraries can matter, especially with static libraries on Unix-like systems. The linker processes libraries left to right, resolving symbols as it goes.

# If libB depends on symbols in libA
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE
libB # Should come first
libA # Should come second
)

With modern CMake and target-based linking, CMake usually handles this correctly based on target_link_libraries() relationships. However, sometimes you need to explicitly control order.

Handling Circular Dependencies​

Sometimes two static libraries depend on each other (though this is a design smell):

# If libA and libB have circular dependencies
target_link_libraries(myapp PRIVATE
libA
libB
libA # Link libA again
)

Better solution: redesign to eliminate the circular dependency or combine into one library.

For stubborn circular dependencies with static libraries:

target_link_libraries(myapp PRIVATE
-Wl,--start-group
libA
libB
-Wl,--end-group
)

This tells the linker to resolve symbols within the group iteratively. Note this is GNU ld specific.

Advanced Linking Techniques​

Whole Archive Linking​

Sometimes you need to force the linker to include all symbols from a static library, not just those referenced:

if(MSVC)
target_link_libraries(myapp PRIVATE
-WHOLEARCHIVE:mylib
)
else()
target_link_libraries(myapp PRIVATE
-Wl,--whole-archive mylib -Wl,--no-whole-archive
)
endif()

This is useful for libraries with static initializers or plugin systems where symbols register themselves.

Add linker-specific flags:

target_link_options(myapp PRIVATE
-Wl,--as-needed # Only link libraries actually used
-Wl,--no-undefined # Error on undefined symbols
)

# Platform-specific
if(APPLE)
target_link_options(myapp PRIVATE
-Wl,-dead_strip # Remove unused code
)
endif()

Interface Libraries for Convenience​

Group common dependencies into interface libraries:

# Create a convenience interface library
add_library(common_deps INTERFACE)

target_link_libraries(common_deps INTERFACE
Threads::Threads
ZLIB::ZLIB
fmt::fmt
)

# Now multiple targets can easily use common dependencies
add_executable(app1 app1.cpp)
target_link_libraries(app1 PRIVATE common_deps)

add_executable(app2 app2.cpp)
target_link_libraries(app2 PRIVATE common_deps)

Complete Real-World Example​

Here's a comprehensive example showing different linking scenarios:

cmake_minimum_required(VERSION 3.15)
project(ComplexProject VERSION 1.0.0)

# Find system dependencies
find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)

# Low-level utility library (header-only)
add_library(utilities INTERFACE)
target_include_directories(utilities INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include/utilities
)
target_compile_features(utilities INTERFACE cxx_std_17)

# Core library (static, uses utilities, ZLIB internally)
add_library(core STATIC
src/core/engine.cpp
src/core/config.cpp
)

target_include_directories(core
PRIVATE src/core
PUBLIC include/core
)

target_link_libraries(core
PUBLIC utilities # Exposed in headers
PRIVATE ZLIB::ZLIB # Used internally only
PRIVATE Threads::Threads # Used internally only
)

# Network library (static, uses core and OpenSSL, exposes both)
add_library(network STATIC
src/network/client.cpp
src/network/server.cpp
)

target_include_directories(network
PRIVATE src/network
PUBLIC include/network
)

target_link_libraries(network
PUBLIC core # Types in our headers
PUBLIC OpenSSL::SSL # Types in our headers
PRIVATE OpenSSL::Crypto # Used internally
)

# Application (uses network, gets everything transitively)
add_executable(myapp
src/main.cpp
src/application.cpp
)

target_link_libraries(myapp PRIVATE network)
# myapp automatically gets: network, core, utilities, OpenSSL::SSL
# myapp does NOT get: ZLIB, Threads, OpenSSL::Crypto (all PRIVATE)

# Installation
install(TARGETS myapp core network utilities
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
INCLUDES DESTINATION include
)

Best Practices​

Link Visibility Guidelines

Use PRIVATE when:

  • Linking to an executable (always)
  • The dependency is used only in .cpp files
  • You want to hide implementation details

Use PUBLIC when:

  • The dependency appears in your public headers
  • Users of your library need the same dependency
  • You're creating a wrapper library

Use INTERFACE when:

  • Creating header-only libraries
  • Creating umbrella/convenience targets
  • Propagating requirements without using them
General Best Practices
  1. Always use visibility keywords - never omit PRIVATE/PUBLIC/INTERFACE
  2. Prefer imported targets over variable-based linking
  3. Link to the minimum required - don't over-link
  4. Keep implementation private - minimize PUBLIC dependencies
  5. Use interface libraries to group common dependencies
  6. Set POSITION_INDEPENDENT_CODE for static libs used in shared libs
  7. Let CMake handle link order through target dependencies

Common Issues and Solutions​

Undefined Reference Errors​

When you see "undefined reference to..." errors:

# Check that you've linked all required libraries
target_link_libraries(myapp PRIVATE
all_required_libs
)

# For static libraries, check link order
# Dependent libraries should come before dependencies

Multiple Definition Errors​

When the same symbol is defined multiple times:

# Ensure libraries aren't linked multiple times
# Use PRIVATE where possible
# Check for duplicate object files in link command

Missing Shared Libraries at Runtime​

When executable can't find .so/.dll files:

# Linux: Check LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/path/to/libs:$LD_LIBRARY_PATH

# Or set RPATH in CMake
set_target_properties(myapp PROPERTIES
INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib"
)

Circular Dependencies​

When libraries depend on each other:

# Best: Redesign to remove circular dependency
# Workaround: Use link groups or link twice
target_link_libraries(myapp PRIVATE A B A)

Quick Reference​

# Basic linking
target_link_libraries(target PRIVATE library)

# Multiple libraries with visibility
target_link_libraries(target
PRIVATE private_libs
PUBLIC public_libs
INTERFACE interface_libs
)

# System libraries (modern)
find_package(PackageName REQUIRED)
target_link_libraries(target PRIVATE PackageName::Component)

# System libraries (legacy)
target_link_libraries(target PRIVATE ${LIBRARY_VARIABLE})

# Link options
target_link_options(target PRIVATE -Wl,--flag)

# RPATH
set_target_properties(target PROPERTIES
INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib"
)