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 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)
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
)
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
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 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.