Table of Contents

  1. GCC/Clang Detection
  2. System Package Dependencies
  3. RPATH Handling
  4. GNUInstallDirs Compliance
  5. Position Independent Code
  6. Sanitizer Integration
  7. Linux-Specific Libraries
  8. Distribution Packaging
  9. LSB Compliance
Back to CMake Mastery Series

Linux with GCC

June 4, 2026 Wasil Zafar 10 min read

A platform-specific guide for building C++ projects on Linux — GCC/Clang detection, pkg-config, RPATH, GNUInstallDirs, sanitizers, and creating DEB/RPM packages with CPack.

GCC/Clang Detection

On Linux, CMake automatically detects the system compiler — typically GCC from the gcc/g++ executables in PATH. You can select between GCC and Clang at configure time, and write conditional logic based on the detected compiler for platform-specific flags.

# Default — uses system GCC
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -S . -B build

# Explicitly select Clang
cmake -G Ninja \
    -DCMAKE_C_COMPILER=clang \
    -DCMAKE_CXX_COMPILER=clang++ \
    -DCMAKE_BUILD_TYPE=Release \
    -S . -B build-clang

# Use a specific GCC version
cmake -G Ninja \
    -DCMAKE_C_COMPILER=gcc-13 \
    -DCMAKE_CXX_COMPILER=g++-13 \
    -S . -B build-gcc13
cmake_minimum_required(VERSION 3.21)
project(LinuxDetection LANGUAGES CXX)

# Compiler detection
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}")

add_library(mylib src/core.cpp)

# GCC-specific flags
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    target_compile_options(mylib PRIVATE
        -Wall -Wextra -Wpedantic -Werror
        -Wconversion -Wsign-conversion
        -Wnon-virtual-dtor -Wold-style-cast
        -Wduplicated-cond -Wduplicated-branches
        -Wlogical-op -Wnull-dereference
        -Wuseless-cast -Wshadow
    )
    # GCC-specific optimization for release
    target_compile_options(mylib PRIVATE
        $<$<CONFIG:Release>:-march=native -flto>
    )
    target_link_options(mylib PRIVATE
        $<$<CONFIG:Release>:-flto>
    )
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    target_compile_options(mylib PRIVATE
        -Wall -Wextra -Wpedantic -Werror
        -Wconversion -Wsign-conversion
        -Wnon-virtual-dtor -Wold-style-cast
        -Wno-unknown-warning-option
    )
endif()

System Package Dependencies

Many Linux libraries ship .pc files for pkg-config rather than CMake config files. CMake's PkgConfig module wraps pkg-config queries into proper imported targets that integrate cleanly with target_link_libraries().

cmake_minimum_required(VERSION 3.21)
project(PkgConfigDemo LANGUAGES CXX)

# Find the PkgConfig module
find_package(PkgConfig REQUIRED)

# Search for libraries via pkg-config
pkg_check_modules(LIBSYSTEMD REQUIRED IMPORTED_TARGET libsystemd)
pkg_check_modules(DBUS REQUIRED IMPORTED_TARGET dbus-1)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0>=2.68)

# Use as imported targets (preferred)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE
    PkgConfig::LIBSYSTEMD
    PkgConfig::DBUS
    PkgConfig::GLIB
)

# Search for optional library
pkg_check_modules(LIBNOTIFY IMPORTED_TARGET libnotify)
if(LIBNOTIFY_FOUND)
    target_link_libraries(app PRIVATE PkgConfig::LIBNOTIFY)
    target_compile_definitions(app PRIVATE HAS_LIBNOTIFY)
endif()
IMPORTED_TARGET keyword: Always use IMPORTED_TARGET with pkg_check_modules(). Without it, you only get variables (LIBSYSTEMD_LIBRARIES, LIBSYSTEMD_INCLUDE_DIRS) that must be manually applied. With it, you get a clean PkgConfig::LIBSYSTEMD target that carries includes, link flags, and definitions automatically.

RPATH Handling

RPATH is the runtime library search path embedded in ELF binaries. On Linux, proper RPATH configuration ensures your application finds its shared libraries at runtime without requiring LD_LIBRARY_PATH hacks. CMake handles RPATH differently during build vs after installation.

cmake_minimum_required(VERSION 3.21)
project(RpathDemo LANGUAGES CXX)

# --- Build RPATH (automatic) ---
# During build, CMake sets RPATH to the build tree library locations
# so executables can find their libraries without installation.

# --- Install RPATH ---
# Use $ORIGIN for relocatable installations
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")

# Or use absolute paths (non-relocatable)
# set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")

# Append to existing RPATH (don't overwrite)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

# Don't strip RPATH during install
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)

add_library(mylib SHARED src/lib.cpp)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)

# Per-target RPATH override
set_target_properties(app PROPERTIES
    INSTALL_RPATH "$ORIGIN/../lib:$ORIGIN/../lib/plugins"
)

install(TARGETS mylib LIBRARY DESTINATION lib)
install(TARGETS app RUNTIME DESTINATION bin)
Linux Gotcha — RPATH vs RUNPATH: Modern linkers default to RUNPATH (DT_RUNPATH) which is overridden by LD_LIBRARY_PATH. For libraries that must NOT be overridden, add -Wl,--disable-new-dtags to use the older RPATH (DT_RPATH) which takes precedence. Set this via target_link_options(app PRIVATE "-Wl,--disable-new-dtags").

GNUInstallDirs Compliance

The GNUInstallDirs module defines standard installation directories that comply with the Filesystem Hierarchy Standard (FHS). Using these variables ensures your project installs correctly on all Linux distributions.

cmake_minimum_required(VERSION 3.21)
project(InstallDemo VERSION 2.1.0 LANGUAGES CXX)

# Include GNUInstallDirs for standard paths
include(GNUInstallDirs)

# Standard variables provided:
# CMAKE_INSTALL_BINDIR     → bin
# CMAKE_INSTALL_LIBDIR     → lib or lib64 (arch-dependent)
# CMAKE_INSTALL_INCLUDEDIR → include
# CMAKE_INSTALL_DATADIR    → share
# CMAKE_INSTALL_MANDIR     → share/man
# CMAKE_INSTALL_DOCDIR     → share/doc/PROJECT_NAME
# CMAKE_INSTALL_SYSCONFDIR → etc

add_library(mylib SHARED src/core.cpp)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)

# Install using GNUInstallDirs variables
install(TARGETS mylib
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

install(TARGETS app
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

install(FILES "${CMAKE_SOURCE_DIR}/docs/mylib.1"
    DESTINATION ${CMAKE_INSTALL_MANDIR}/man1
)

# Configure pkg-config file with correct paths
configure_file(mylib.pc.in mylib.pc @ONLY)
install(FILES "${CMAKE_BINARY_DIR}/mylib.pc"
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
)

Position Independent Code

Shared libraries on Linux require position-independent code (PIC). CMake handles this automatically for SHARED libraries, but you may need to enable it for static libraries that will be linked into shared libraries.

cmake_minimum_required(VERSION 3.21)
project(PICDemo LANGUAGES CXX)

# Global: enable PIC for all targets
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Or per-target:
add_library(utils STATIC src/utils.cpp)
set_target_properties(utils PROPERTIES POSITION_INDEPENDENT_CODE ON)

# This static lib will be linked into a shared lib
add_library(mylib SHARED src/core.cpp)
target_link_libraries(mylib PRIVATE utils)  # utils must be PIC!

# PIE (Position Independent Executable) for ASLR security
add_executable(app src/main.cpp)
set_target_properties(app PROPERTIES
    POSITION_INDEPENDENT_CODE ON  # Enables -fPIE + -pie
)
Security Hardening: Modern Linux distributions require PIE (Position Independent Executables) for ASLR. CMake 3.14+ enables this by default when CMAKE_POSITION_INDEPENDENT_CODE is ON for executables. Verify with file app — it should say "ELF 64-bit LSB pie executable" not "ELF 64-bit LSB executable".

Sanitizer Integration

Address Sanitizer (ASan), Thread Sanitizer (TSan), and Undefined Behavior Sanitizer (UBSan) are invaluable for catching memory errors, data races, and undefined behavior. On Linux with GCC or Clang, these integrate cleanly via compile and link flags.

cmake_minimum_required(VERSION 3.21)
project(SanitizersDemo LANGUAGES CXX)

# Sanitizer option
set(SANITIZER "" CACHE STRING "Enable sanitizer (address, thread, undefined, memory)")

add_executable(app src/main.cpp)

if(SANITIZER STREQUAL "address")
    target_compile_options(app PRIVATE -fsanitize=address -fno-omit-frame-pointer)
    target_link_options(app PRIVATE -fsanitize=address)
elseif(SANITIZER STREQUAL "thread")
    target_compile_options(app PRIVATE -fsanitize=thread)
    target_link_options(app PRIVATE -fsanitize=thread)
elseif(SANITIZER STREQUAL "undefined")
    target_compile_options(app PRIVATE -fsanitize=undefined -fno-omit-frame-pointer)
    target_link_options(app PRIVATE -fsanitize=undefined)
elseif(SANITIZER STREQUAL "memory")
    # MSan requires Clang — GCC does not support it
    if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
        message(FATAL_ERROR "Memory sanitizer requires Clang")
    endif()
    target_compile_options(app PRIVATE -fsanitize=memory -fno-omit-frame-pointer)
    target_link_options(app PRIVATE -fsanitize=memory)
endif()
# Build with Address Sanitizer
cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DSANITIZER=address -S . -B build-asan
cmake --build build-asan

# Run — ASan will report errors to stderr
./build-asan/app

# Build with Thread Sanitizer
cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DSANITIZER=thread -S . -B build-tsan
cmake --build build-tsan

Linux-Specific Libraries

Linux programs frequently need system libraries like pthreads, libdl (dynamic loading), and librt (POSIX realtime). CMake provides find modules and modern imported targets for these.

cmake_minimum_required(VERSION 3.21)
project(LinuxLibs LANGUAGES CXX)

add_executable(app src/main.cpp)

# Threads (pthread) — the modern way
find_package(Threads REQUIRED)
target_link_libraries(app PRIVATE Threads::Threads)

# Dynamic loading (dlopen, dlsym)
target_link_libraries(app PRIVATE ${CMAKE_DL_LIBS})  # Resolves to "dl" on Linux

# POSIX Realtime (clock_gettime, shm_open, mq_open)
# Modern glibc includes these in libc, but older systems need -lrt
find_library(RT_LIBRARY rt)
if(RT_LIBRARY)
    target_link_libraries(app PRIVATE ${RT_LIBRARY})
endif()

# Math library (some platforms need explicit -lm)
target_link_libraries(app PRIVATE m)

# Filesystem (GCC < 9 needs explicit -lstdc++fs)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
   CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0)
    target_link_libraries(app PRIVATE stdc++fs)
endif()

Distribution Packaging

CPack can generate DEB (Debian/Ubuntu) and RPM (Fedora/RHEL) packages directly from your CMake project. This is the standard approach for distributing C++ applications on Linux.

cmake_minimum_required(VERSION 3.21)
project(PackagingDemo VERSION 2.1.0 LANGUAGES CXX)

include(GNUInstallDirs)

add_library(mylib SHARED src/core.cpp)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)

install(TARGETS mylib LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(TARGETS app RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

# --- CPack Configuration ---
set(CPACK_PACKAGE_NAME "myproject")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "My awesome C++ project")
set(CPACK_PACKAGE_CONTACT "dev@example.com")
set(CPACK_PACKAGE_VENDOR "MyCompany")

# DEB-specific settings
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.31), libstdc++6 (>= 10)")
set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)  # Auto-detect shared lib deps

# RPM-specific settings
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
set(CPACK_RPM_PACKAGE_REQUIRES "glibc >= 2.31")
set(CPACK_RPM_PACKAGE_AUTOREQ ON)  # Auto-detect requirements

# Generate both DEB and RPM
set(CPACK_GENERATOR "DEB;RPM;TGZ")

include(CPack)
# Build and package
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -S . -B build
cmake --build build
cd build

# Generate all configured package types
cpack

# Or generate specific type
cpack -G DEB
cpack -G RPM

# Install DEB package
sudo dpkg -i myproject-2.1.0-Linux.deb

# Install RPM package
sudo rpm -i myproject-2.1.0-Linux.rpm
Linux Production Pattern
Multi-Package Split (lib + dev + app)

Professional Linux packages split into runtime (libmylib), development (libmylib-dev), and application (myapp) packages. CPack component-based installation achieves this:

# Component-based installation
install(TARGETS mylib
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    COMPONENT runtime
)
install(TARGETS app
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    COMPONENT application
)
install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    COMPONENT development
)

# Component-based DEB packaging
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_DEBIAN_RUNTIME_PACKAGE_NAME "libmylib")
set(CPACK_DEBIAN_DEVELOPMENT_PACKAGE_NAME "libmylib-dev")
set(CPACK_DEBIAN_DEVELOPMENT_PACKAGE_DEPENDS "libmylib (= ${PROJECT_VERSION})")
set(CPACK_DEBIAN_APPLICATION_PACKAGE_NAME "myapp")
set(CPACK_DEBIAN_APPLICATION_PACKAGE_DEPENDS "libmylib (= ${PROJECT_VERSION})")

LSB Compliance

The Linux Standard Base (LSB) defines a standard set of libraries and paths that applications can depend on. While LSB certification is less common today, following its conventions ensures maximum compatibility across distributions.

cmake_minimum_required(VERSION 3.21)
project(LsbDemo LANGUAGES CXX)

include(GNUInstallDirs)

# LSB-compliant installation paths
# /opt/company/product/ for third-party software
set(CMAKE_INSTALL_PREFIX "/opt/mycompany/myproduct"
    CACHE PATH "Installation prefix")

# Or follow FHS for system packages
# /usr/local/ for manually compiled software
# /usr/ for distribution packages

add_executable(app src/main.cpp)
add_library(mylib SHARED src/lib.cpp)

# Versioned shared library (SOVERSION for ABI compatibility)
set_target_properties(mylib PROPERTIES
    VERSION ${PROJECT_VERSION}          # libmylib.so.2.1.0
    SOVERSION ${PROJECT_VERSION_MAJOR}  # libmylib.so.2 → symlink
)

install(TARGETS mylib LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(TARGETS app RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

# Post-install: update shared library cache
# (Handled by package managers, but useful for manual installs)
# install(CODE "execute_process(COMMAND ldconfig)")
SOVERSION Best Practice: Always set SOVERSION on shared libraries. This creates the symlink chain (libfoo.so → libfoo.so.2 → libfoo.so.2.1.0) that enables ABI-compatible upgrades without relinking consumers. Bump SOVERSION only when the ABI breaks.