Table of Contents

  1. Static Libraries
  2. Shared Libraries
  3. Object Libraries
  4. Module Libraries
  5. Interface Libraries
  6. BUILD_SHARED_LIBS
  7. Linking Semantics
  8. Symbol Visibility
  9. RPATH and Install Names
  10. Library Versioning
  11. Exercises
  12. Conclusion & Next Steps
Back to CMake Mastery Series

Part 4: Building and Linking Libraries

June 4, 2026 Wasil Zafar 40 min read

Create static, shared, object, module, and interface libraries. Master linking semantics with PUBLIC/PRIVATE/INTERFACE, control symbol visibility, and configure RPATH for portable deployments.

Static Libraries

A static library is an archive of compiled object files that gets linked directly into the final executable at build time. The linker copies the needed code from the archive into the binary, producing a self-contained executable with no runtime library dependencies.

Key Insight: Static libraries produce larger executables but eliminate runtime dependency issues. Every executable gets its own copy of the library code. On Linux/macOS they use the .a extension; on Windows they use .lib.

Create a static library with add_library() using the STATIC keyword:

# CMakeLists.txt — Creating a static library
cmake_minimum_required(VERSION 3.20)
project(MathLib VERSION 1.0 LANGUAGES CXX)

# Create a static library from source files
add_library(mathlib STATIC
    src/algebra.cpp
    src/calculus.cpp
    src/statistics.cpp
)

# Specify include directories for consumers
target_include_directories(mathlib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# Link the library to an executable
add_executable(calculator src/main.cpp)
target_link_libraries(calculator PRIVATE mathlib)

When CMake builds this project, it compiles each .cpp into an object file, then archives them into libmathlib.a (Unix) or mathlib.lib (Windows). The executable calculator links against this archive at build time.

When to Use Static Libraries

  • Self-contained deployment — No shared library dependencies to manage at runtime
  • Performance-critical paths — Enables link-time optimization (LTO) across library boundaries
  • Embedded systems — Single binary with no dynamic loader available
  • Small libraries — Overhead of shared library machinery not justified

The linker is smart about static libraries: it only pulls in object files that resolve undefined symbols. If your executable uses algebra.cpp functions but not statistics.cpp, the statistics code is excluded from the final binary.

Shared Libraries

A shared library (also called dynamic library) is loaded at runtime by the operating system's dynamic linker. Multiple programs can share a single copy of the library in memory, reducing total system resource usage.

# CMakeLists.txt — Creating a shared library
cmake_minimum_required(VERSION 3.20)
project(NetworkLib VERSION 2.3.1 LANGUAGES CXX)

# Create a shared library
add_library(netlib SHARED
    src/socket.cpp
    src/http_client.cpp
    src/dns_resolver.cpp
)

# Set public include directories
target_include_directories(netlib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# Set version properties
set_target_properties(netlib PROPERTIES
    VERSION ${PROJECT_VERSION}        # libnetlib.so.2.3.1
    SOVERSION ${PROJECT_VERSION_MAJOR} # libnetlib.so.2 symlink
)

# Link executable against the shared library
add_executable(web_client src/main.cpp)
target_link_libraries(web_client PRIVATE netlib)

On Linux, this produces three filesystem entries:

# Shared library files on Linux after build
ls -la lib/
# libnetlib.so -> libnetlib.so.2          (development symlink)
# libnetlib.so.2 -> libnetlib.so.2.3.1    (SOVERSION symlink)
# libnetlib.so.2.3.1                       (actual library file)

Library Versioning with SOVERSION

The VERSION and SOVERSION properties control the symlink structure on Unix systems. The SOVERSION represents the ABI version — increment it when you make binary-incompatible changes:

# CMakeLists.txt — SOVERSION for ABI compatibility
cmake_minimum_required(VERSION 3.20)
project(Codec VERSION 3.2.0 LANGUAGES CXX)

add_library(codec SHARED src/encoder.cpp src/decoder.cpp)

set_target_properties(codec PROPERTIES
    # Full version: libcodec.so.3.2.0
    VERSION 3.2.0
    # ABI version: libcodec.so.3 (symlink)
    # Increment SOVERSION only on ABI-breaking changes
    SOVERSION 3
)
ABI Compatibility Rule: When you change a function signature, remove a public symbol, or modify the size/layout of a public class, you must increment the SOVERSION. Adding new functions without removing existing ones is backward-compatible and only requires incrementing the minor VERSION.

Platform-Specific Shared Library Files

PlatformExtensionExample
Linux.solibnetlib.so.2.3.1
macOS.dyliblibnetlib.2.3.1.dylib
Windows.dll + .libnetlib.dll + netlib.lib (import lib)

Object Libraries

An object library compiles source files into object files but does not archive or link them. It acts as a logical grouping of compiled objects that can be incorporated into other targets. Object libraries avoid the overhead of creating an actual archive file.

# CMakeLists.txt — Object library for code reuse
cmake_minimum_required(VERSION 3.20)
project(GameEngine VERSION 1.0 LANGUAGES CXX)

# Create object library — compiles sources but doesn't create .a or .so
add_library(engine_core OBJECT
    src/renderer.cpp
    src/physics.cpp
    src/audio.cpp
)

target_include_directories(engine_core PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)

# Use objects in a shared library
add_library(engine SHARED $<TARGET_OBJECTS:engine_core> src/engine_api.cpp)

# Use same objects in a static library (no recompilation!)
add_library(engine_static STATIC $<TARGET_OBJECTS:engine_core> src/engine_api.cpp)

# Or link directly (CMake 3.12+)
add_executable(game src/main.cpp)
target_link_libraries(game PRIVATE engine_core)

Object Library Use Cases

  • Compile once, use in multiple targets — Build both static and shared variants from the same objects
  • Position-independent code control — Compile with -fPIC once, use in both shared libraries and executables
  • Internal modularization — Split large targets into logical groups without creating actual library files
Note: Since CMake 3.12, you can use target_link_libraries() with object libraries directly. Before 3.12, you had to use the $<TARGET_OBJECTS:name> generator expression exclusively.

Module Libraries

A module library is a shared library that is loaded at runtime via dlopen() (Unix) or LoadLibrary() (Windows) rather than linked at build time. It's the mechanism behind plugin architectures.

# CMakeLists.txt — Plugin system with MODULE libraries
cmake_minimum_required(VERSION 3.20)
project(PluginHost VERSION 1.0 LANGUAGES CXX)

# The host application that loads plugins
add_executable(host src/host.cpp src/plugin_loader.cpp)
target_include_directories(host PRIVATE include)

# A plugin — loaded at runtime, NOT linked at build time
add_library(plugin_reverb MODULE plugins/reverb.cpp)
target_include_directories(plugin_reverb PRIVATE include)

# Plugins don't have the "lib" prefix on all platforms
set_target_properties(plugin_reverb PROPERTIES
    PREFIX ""                    # Output: plugin_reverb.so (not libplugin_reverb.so)
    SUFFIX ".plugin"            # Custom extension: plugin_reverb.plugin
)

# Another plugin
add_library(plugin_echo MODULE plugins/echo.cpp)
set_target_properties(plugin_echo PROPERTIES PREFIX "" SUFFIX ".plugin")
MODULE vs SHARED: A SHARED library can be linked to at build time with target_link_libraries(). A MODULE library cannot — it's designed exclusively for runtime loading. On most platforms they produce the same file format, but the semantic distinction matters for CMake's dependency tracking.

Interface Libraries

An interface library has no compiled source files. It exists purely to propagate usage requirements — include directories, compile definitions, link dependencies — to consuming targets. This is the canonical pattern for header-only libraries.

# CMakeLists.txt — Header-only library using INTERFACE
cmake_minimum_required(VERSION 3.20)
project(JsonParser VERSION 1.0 LANGUAGES CXX)

# No source files! Header-only library
add_library(json_parser INTERFACE)

# All properties use INTERFACE scope (propagated to consumers)
target_include_directories(json_parser INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# Require C++17 from consumers
target_compile_features(json_parser INTERFACE cxx_std_17)

# Propagate a dependency
target_compile_definitions(json_parser INTERFACE JSON_PARSER_VERSION=1)

# Consumer automatically gets include paths and C++17 requirement
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE json_parser)

Interface libraries are also useful for creating "meta targets" that bundle multiple dependencies:

# CMakeLists.txt — Meta-target bundling dependencies
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0 LANGUAGES CXX)

# Bundle multiple dependencies into one logical target
add_library(platform_deps INTERFACE)
target_link_libraries(platform_deps INTERFACE
    Threads::Threads
    ${CMAKE_DL_LIBS}
    $<$<PLATFORM_ID:Linux>:rt>
)

add_executable(server src/server.cpp)
target_link_libraries(server PRIVATE platform_deps)
CMake Library Types Comparison
        flowchart LR
            subgraph "Library Types"
                STATIC["STATIC
.a / .lib
Linked at build time"] SHARED["SHARED
.so / .dylib / .dll
Linked at load time"] OBJECT["OBJECT
No archive file
Object files only"] MODULE["MODULE
.so / .dll
dlopen() at runtime"] INTERFACE["INTERFACE
No output file
Headers + properties only"] end subgraph "Usage" EXE[Executable] end STATIC -->|"code copied into"| EXE SHARED -->|"referenced at runtime"| EXE OBJECT -->|"objects merged into"| EXE MODULE -.->|"loaded dynamically"| EXE INTERFACE -->|"properties propagated"| EXE

BUILD_SHARED_LIBS

When add_library() is called without an explicit type (STATIC/SHARED), CMake checks the BUILD_SHARED_LIBS variable to decide. This lets users choose at configure time whether they want static or shared libraries:

# CMakeLists.txt — Letting users choose library type
cmake_minimum_required(VERSION 3.20)
project(FlexLib VERSION 1.0 LANGUAGES CXX)

# Provide an option (defaults to OFF = static)
option(BUILD_SHARED_LIBS "Build shared libraries instead of static" OFF)

# No explicit STATIC or SHARED — respects BUILD_SHARED_LIBS
add_library(flexlib
    src/core.cpp
    src/utils.cpp
)

target_include_directories(flexlib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE flexlib)

Users choose at configure time:

# Build with static libraries (default)
cmake -B build -DBUILD_SHARED_LIBS=OFF

# Build with shared libraries
cmake -B build -DBUILD_SHARED_LIBS=ON
Best Practice: Omit the library type in add_library() to give consumers flexibility. Only hard-code STATIC or SHARED when the library fundamentally requires one type (e.g., a plugin must be MODULE, a header-only library must be INTERFACE).

Linking Semantics

The target_link_libraries() command accepts three visibility keywords that control how dependencies propagate through the dependency graph:

KeywordCompiles with?Links with?Propagates to consumers?
PRIVATEYesYesNo
PUBLICYesYesYes
INTERFACENoNoYes
# CMakeLists.txt — Linking visibility in practice
cmake_minimum_required(VERSION 3.20)
project(Networking VERSION 1.0 LANGUAGES CXX)

# Low-level crypto library
add_library(crypto STATIC src/crypto/aes.cpp src/crypto/sha256.cpp)
target_include_directories(crypto PUBLIC include/crypto)

# TLS library uses crypto internally but doesn't expose it
add_library(tls STATIC src/tls/handshake.cpp src/tls/record.cpp)
target_include_directories(tls PUBLIC include/tls)
target_link_libraries(tls
    PUBLIC Threads::Threads   # Consumers also need threads
    PRIVATE crypto            # Crypto is an implementation detail
)

# HTTP library builds on TLS (exposes TLS in its API)
add_library(http STATIC src/http/client.cpp src/http/server.cpp)
target_include_directories(http PUBLIC include/http)
target_link_libraries(http
    PUBLIC tls                # HTTP API exposes TLS types
    PRIVATE ZLIB::ZLIB       # Compression is internal
)

# Application links HTTP — automatically gets Threads transitively
add_executable(web_app src/main.cpp)
target_link_libraries(web_app PRIVATE http)
# web_app links: http, tls, Threads::Threads
# web_app does NOT link: crypto, ZLIB (they're PRIVATE)

Transitive Dependencies Explained

When target A links PUBLIC to B, and target C links to A, then C automatically inherits B's include directories and link libraries. This is transitive dependency propagation — CMake's most powerful feature for large projects:

Transitive Dependency Propagation
        flowchart TD
            APP["web_app
(executable)"] HTTP["http
(static lib)"] TLS["tls
(static lib)"] THREADS["Threads::Threads"] CRYPTO["crypto
(static lib)"] ZLIB["ZLIB::ZLIB"] APP -->|"PRIVATE"| HTTP HTTP -->|"PUBLIC"| TLS HTTP -->|"PRIVATE"| ZLIB TLS -->|"PUBLIC"| THREADS TLS -->|"PRIVATE"| CRYPTO style APP fill:#3B9797,color:#fff style HTTP fill:#16476A,color:#fff style TLS fill:#16476A,color:#fff style THREADS fill:#132440,color:#fff style CRYPTO fill:#666,color:#fff style ZLIB fill:#666,color:#fff

In the diagram above, web_app only directly links http — but it transitively receives tls and Threads::Threads through PUBLIC links. The PRIVATE dependencies (crypto, ZLIB) do not propagate.

Decision Rule: If a dependency appears in your library's public headers (the headers consumers #include), use PUBLIC. If it's only used in your .cpp implementation files, use PRIVATE. If your library doesn't use it at all but wants to pass it on, use INTERFACE.

Symbol Visibility

By default, shared libraries on Linux/macOS export all symbols, while Windows exports none. This inconsistency leads to portability issues. Modern CMake provides tools to enforce consistent visibility across platforms.

CXX_VISIBILITY_PRESET

Set the default visibility to hidden, then explicitly export only the public API:

# CMakeLists.txt — Controlling symbol visibility
cmake_minimum_required(VERSION 3.20)
project(ImageLib VERSION 2.0 LANGUAGES CXX)

add_library(imagelib SHARED
    src/loader.cpp
    src/transform.cpp
    src/codec.cpp
)

# Hide all symbols by default (consistent with Windows behavior)
set_target_properties(imagelib PROPERTIES
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN YES
)

target_include_directories(imagelib PUBLIC include)

GenerateExportHeader

CMake's GenerateExportHeader module creates a portable export macro header that works on all platforms:

# CMakeLists.txt — Generate export macros automatically
cmake_minimum_required(VERSION 3.20)
project(ImageLib VERSION 2.0 LANGUAGES CXX)

include(GenerateExportHeader)

add_library(imagelib SHARED
    src/loader.cpp
    src/transform.cpp
)

set_target_properties(imagelib PROPERTIES
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN YES
)

# Generates imagelib_export.h with IMAGELIB_EXPORT macro
generate_export_header(imagelib
    EXPORT_MACRO_NAME IMAGELIB_EXPORT
    EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/include/imagelib_export.h
)

target_include_directories(imagelib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Then use the generated macro in your public headers:

// include/imagelib/loader.h — Using the export macro
#pragma once
#include "imagelib_export.h"  // Generated by CMake

// IMAGELIB_EXPORT resolves to:
//   __declspec(dllexport) on Windows when building the DLL
//   __declspec(dllimport) on Windows when consuming the DLL
//   __attribute__((visibility("default"))) on GCC/Clang
class IMAGELIB_EXPORT ImageLoader {
public:
    ImageLoader();
    ~ImageLoader();
    bool load(const char* path);
    int width() const;
    int height() const;

private:
    struct Impl;  // Private implementation (not exported)
    Impl* pImpl;
};

// Non-exported helper (stays hidden)
namespace detail {
    void internal_decode(const unsigned char* data, int size);
}
Windows Gotcha: On Windows, without __declspec(dllexport), no symbols are exported from a DLL at all — the resulting .lib import library will be empty. Always use GenerateExportHeader for cross-platform shared libraries.

RPATH and Install Names

When you build an executable that links a shared library, the OS dynamic linker needs to find that library at runtime. RPATH embeds a search path directly into the binary so it knows where to look.

# CMakeLists.txt — RPATH configuration for portable installs
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0 LANGUAGES CXX)

add_library(applib SHARED src/applib.cpp)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE applib)

# --- RPATH settings for installed binaries ---

# Use full RPATH during build (for running from build tree)
set(CMAKE_BUILD_RPATH_USE_ORIGIN TRUE)

# Set RPATH for installed binaries
set(CMAKE_INSTALL_RPATH
    "$ORIGIN/../lib"           # Linux: relative to executable
)

# On macOS, use @rpath
if(APPLE)
    set(CMAKE_INSTALL_RPATH "@executable_path/../lib")
    set(CMAKE_MACOSX_RPATH TRUE)
endif()

# Don't strip RPATH from installed binaries
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

# Install rules
install(TARGETS myapp RUNTIME DESTINATION bin)
install(TARGETS applib LIBRARY DESTINATION lib)

RPATH strategies vary by platform:

PlatformMechanismRelative Syntax
LinuxELF RPATH/RUNPATH$ORIGIN/../lib
macOSLC_RPATH load command@executable_path/../lib, @loader_path/../lib, @rpath
WindowsDLL search orderSame directory as .exe, then PATH
Best Practice: Use $ORIGIN (Linux) or @executable_path (macOS) for relocatable installations. This allows users to install your application anywhere without breaking library resolution. Windows searches the executable's directory first, so just co-locate DLLs with the .exe.

Library Versioning

Proper library versioning communicates ABI compatibility to users and package managers. CMake supports this through target properties:

# CMakeLists.txt — Complete library versioning setup
cmake_minimum_required(VERSION 3.20)
project(DataLib VERSION 4.2.1 LANGUAGES CXX)

add_library(datalib SHARED
    src/parser.cpp
    src/serializer.cpp
    src/validator.cpp
)

set_target_properties(datalib PROPERTIES
    # Full version: libdatalib.so.4.2.1
    VERSION ${PROJECT_VERSION}

    # ABI version: libdatalib.so.4 (major version = ABI contract)
    SOVERSION ${PROJECT_VERSION_MAJOR}

    # macOS: set compatibility and current version
    MACOSX_RPATH TRUE
)

# On the filesystem (Linux):
#   libdatalib.so       -> libdatalib.so.4       (dev symlink)
#   libdatalib.so.4     -> libdatalib.so.4.2.1   (SOVERSION symlink)
#   libdatalib.so.4.2.1                           (real file)
#
# Programs link against libdatalib.so.4 — any 4.x.y is compatible

The versioning scheme follows semantic versioning at the ABI level:

  • Major (SOVERSION) — Increment when binary interface changes (removed functions, changed signatures, changed class layout)
  • Minor — Increment when new functions/classes added without breaking existing ones
  • Patch — Increment for bug fixes with no API/ABI changes
# Inspect library version information on Linux
readelf -d libdatalib.so.4.2.1 | grep -i "soname"
# 0x000000000000000e (SONAME)    Library soname: [libdatalib.so.4]

# On macOS, use otool
otool -L libdatalib.4.2.1.dylib
# libdatalib.4.dylib (compatibility version 4.0.0, current version 4.2.1)

Exercises

Exercise 1 Static vs Shared Size Comparison

Create a project with a math library containing 5+ functions. Build it as both STATIC and SHARED. Link an executable to each variant and compare:

  1. The size of the executable when linked statically vs dynamically
  2. Run ldd (Linux) or otool -L (macOS) on each to see dependencies
  3. Use nm -D to inspect exported symbols from the shared library
static shared nm ldd
Exercise 2 Transitive Dependency Chain

Create a three-layer project:

  1. libbase — provides a Base class
  2. libmiddle — depends PUBLIC on libbase, adds Middle class
  3. app — links only to libmiddle

Verify that app can use Base without directly linking libbase. Then change the link to PRIVATE and observe the compilation error. Understand why PUBLIC propagation is essential when types appear in your public headers.

target_link_libraries PUBLIC PRIVATE transitive
Exercise 3 Cross-Platform Export Header

Build a shared library that works on both Linux and Windows:

  1. Use GenerateExportHeader to create an export macro
  2. Set CXX_VISIBILITY_PRESET to hidden
  3. Export only 2-3 public classes/functions
  4. Verify with nm -D (Linux) that only exported symbols are visible
  5. Keep internal helper functions hidden
GenerateExportHeader visibility dllexport cross-platform
Exercise 4 Plugin Architecture with MODULE

Create a plugin host application:

  1. Define a plugin interface header with a create_plugin() factory function
  2. Build 2-3 plugins as MODULE libraries
  3. Write a host that scans a directory and dlopen()s each plugin
  4. Verify that plugins cannot be linked at build time (only loaded at runtime)
MODULE dlopen plugin LoadLibrary

Conclusion & Next Steps

Libraries are the fundamental building blocks of modular C++ software. In this part you've learned:

  • STATIC — Archive of objects linked at build time, producing self-contained executables
  • SHARED — Dynamically linked at load time, shared across processes, versioned with SOVERSION
  • OBJECT — Compile-once objects without creating an archive file
  • MODULE — Shared libraries for runtime loading (plugins)
  • INTERFACE — Header-only or meta targets propagating usage requirements
  • Linking semantics — PUBLIC/PRIVATE/INTERFACE control transitive propagation
  • Symbol visibilityGenerateExportHeader creates portable export macros
  • RPATH — Embed relative library search paths for relocatable installs
Official Reference: See add_library(), target_link_libraries(), and GenerateExportHeader in the official CMake documentation.