Table of Contents

  1. Operating System Detection
  2. Processor Architecture
  3. Compiler Identification
  4. Check Modules
  5. CPU Feature Detection
  6. Endianness and Sizes
  7. Platform-Dependent Sources
  8. configure_file for Feature Detection
  9. System Information
  10. Cross-Compilation Awareness
Back to CMake Mastery Series

Part 8: Detecting the Environment

June 4, 2026 Wasil Zafar 35 min read

Detect operating systems, CPU architectures, compiler capabilities, and system features to write portable CMake configurations that adapt gracefully to any build environment.

Operating System Detection

CMake provides several variables and convenience booleans for identifying the target operating system. The CMAKE_SYSTEM_NAME variable is the canonical way to identify the target platform.

Platform Variables

cmake_minimum_required(VERSION 3.21)
project(PlatformDetect LANGUAGES CXX)

# CMAKE_SYSTEM_NAME is the authoritative variable
message(STATUS "Target system: ${CMAKE_SYSTEM_NAME}")
message(STATUS "Target processor: ${CMAKE_SYSTEM_PROCESSOR}")

# Convenience boolean variables (set automatically)
if(WIN32)
    message(STATUS "Building for Windows (includes Cygwin)")
endif()
if(APPLE)
    message(STATUS "Building for Apple (macOS, iOS, tvOS, watchOS)")
endif()
if(UNIX)
    message(STATUS "Building for Unix-like (Linux, macOS, BSD, etc.)")
endif()
if(LINUX)  # Available since CMake 3.25
    message(STATUS "Building for Linux specifically")
endif()

# More specific checks using CMAKE_SYSTEM_NAME
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    message(STATUS "Windows target")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    message(STATUS "macOS target")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(STATUS "Linux target")
elseif(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
    message(STATUS "FreeBSD target")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Android")
    message(STATUS "Android target")
elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS")
    message(STATUS "iOS target")
endif()
Important: UNIX is true on both Linux and macOS. APPLE is true for macOS, iOS, tvOS, and watchOS. If you need Linux specifically (not macOS), use CMAKE_SYSTEM_NAME STREQUAL "Linux" or the LINUX variable (CMake 3.25+). Never assume UNIX AND NOT APPLE means Linux — it could be FreeBSD or other Unix variants.
Platform Variable Hierarchy
        flowchart TD
            A[CMAKE_SYSTEM_NAME] --> B{Value}
            B -->|Windows| C["WIN32 = TRUE
UNIX = FALSE
APPLE = FALSE"] B -->|Darwin| D["WIN32 = FALSE
UNIX = TRUE
APPLE = TRUE"] B -->|Linux| E["WIN32 = FALSE
UNIX = TRUE
APPLE = FALSE
LINUX = TRUE"] B -->|FreeBSD| F["WIN32 = FALSE
UNIX = TRUE
APPLE = FALSE"] B -->|Android| G["WIN32 = FALSE
UNIX = TRUE
APPLE = FALSE"] style C fill:#16476A,color:#fff style D fill:#132440,color:#fff style E fill:#3B9797,color:#fff

Processor Architecture

The CMAKE_SYSTEM_PROCESSOR variable identifies the target CPU architecture. For native builds, this matches the host; for cross-compilation, it reflects the target.

cmake_minimum_required(VERSION 3.21)
project(ArchDetect LANGUAGES CXX)

message(STATUS "Host processor: ${CMAKE_HOST_SYSTEM_PROCESSOR}")
message(STATUS "Target processor: ${CMAKE_SYSTEM_PROCESSOR}")

add_executable(app main.cpp)

# Architecture-specific flags
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
    message(STATUS "64-bit x86 architecture")
    target_compile_definitions(app PRIVATE ARCH_X86_64=1)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "i[3-6]86|x86")
    message(STATUS "32-bit x86 architecture")
    target_compile_definitions(app PRIVATE ARCH_X86=1)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64|arm64")
    message(STATUS "64-bit ARM architecture")
    target_compile_definitions(app PRIVATE ARCH_ARM64=1)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
    message(STATUS "32-bit ARM architecture")
    target_compile_definitions(app PRIVATE ARCH_ARM32=1)
endif()

Determining 32-bit vs 64-bit

cmake_minimum_required(VERSION 3.21)
project(PointerSize LANGUAGES CXX)

# CMAKE_SIZEOF_VOID_P is the most reliable way to detect pointer size
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
    message(STATUS "64-bit build (pointer size = 8 bytes)")
    set(ARCH_BITS 64)
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
    message(STATUS "32-bit build (pointer size = 4 bytes)")
    set(ARCH_BITS 32)
endif()

add_executable(app main.cpp)
target_compile_definitions(app PRIVATE ARCH_BITS=${ARCH_BITS})

Compiler Identification

Beyond the CMAKE_CXX_COMPILER_ID covered in Part 6, you can also check compiler versions to enable features conditionally:

cmake_minimum_required(VERSION 3.21)
project(CompilerVersion LANGUAGES CXX)

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

add_executable(app main.cpp)

# Version-dependent features
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0)
        message(STATUS "GCC 12+ detected — full C++20 support")
        target_compile_options(app PRIVATE -Wno-interference-size)
    endif()
    if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 13.0)
        message(STATUS "GCC 13+ detected — std::print support")
    endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0)
        message(STATUS "Clang 16+ detected — C++20 modules support")
    endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.35)
        message(STATUS "MSVC 17.5+ detected — improved C++23 support")
    endif()
endif()

Check Modules

CMake's Check modules compile small test programs to determine if the compiler supports specific features or if specific symbols/headers exist. These are your most powerful tools for fine-grained feature detection. See the CheckCXXSourceCompiles documentation.

CheckCXXSourceCompiles

cmake_minimum_required(VERSION 3.21)
project(SourceChecks LANGUAGES CXX)

include(CheckCXXSourceCompiles)

# Check if std::filesystem is available
check_cxx_source_compiles("
    #include <filesystem>
    int main() {
        std::filesystem::path p(\"/tmp\");
        return p.empty() ? 1 : 0;
    }
" HAS_STD_FILESYSTEM)

# Check if std::format is available
check_cxx_source_compiles("
    #include <format>
    int main() {
        auto s = std::format(\"Hello, {}!\", \"world\");
        return 0;
    }
" HAS_STD_FORMAT)

# Check for a POSIX function
check_cxx_source_compiles("
    #include <unistd.h>
    int main() {
        return getpid();
    }
" HAS_POSIX_GETPID)

add_executable(app main.cpp)
if(HAS_STD_FILESYSTEM)
    target_compile_definitions(app PRIVATE HAS_FILESYSTEM=1)
endif()
if(HAS_STD_FORMAT)
    target_compile_definitions(app PRIVATE HAS_FORMAT=1)
endif()

CheckSymbolExists and CheckIncludeFileCXX

cmake_minimum_required(VERSION 3.21)
project(SymbolChecks LANGUAGES CXX)

include(CheckSymbolExists)
include(CheckIncludeFileCXX)

# Check if a header exists
check_include_file_cxx("optional" HAS_OPTIONAL_HEADER)
check_include_file_cxx("span" HAS_SPAN_HEADER)
check_include_file_cxx("sys/mman.h" HAS_MMAN_H)

# Check if a symbol (function/variable) exists
check_symbol_exists(aligned_alloc "stdlib.h" HAS_ALIGNED_ALLOC)
check_symbol_exists(posix_memalign "stdlib.h" HAS_POSIX_MEMALIGN)
check_symbol_exists(mmap "sys/mman.h" HAS_MMAP)

add_executable(app main.cpp)
target_compile_definitions(app PRIVATE
    $<$<BOOL:${HAS_ALIGNED_ALLOC}>:USE_ALIGNED_ALLOC=1>
    $<$<BOOL:${HAS_POSIX_MEMALIGN}>:USE_POSIX_MEMALIGN=1>
    $<$<BOOL:${HAS_MMAP}>:USE_MMAP=1>
)
Hands-On Feature Detection Matrix

Create a CMakeLists.txt that checks for: std::filesystem, std::format, std::jthread, std::ranges, and aligned_alloc. Print a table showing which features are available on your system. Use the results to define preprocessor macros.

CheckCXXSourceCompiles CheckSymbolExists Feature Matrix

CPU Feature Detection

For performance-critical code, you may need to detect SIMD instruction set support (SSE, AVX, NEON). The cmake_host_system_information() command provides some CPU information, but for specific instruction sets, compile checks are more reliable:

cmake_minimum_required(VERSION 3.21)
project(CPUFeatures LANGUAGES CXX)

include(CheckCXXSourceCompiles)
include(CheckCXXCompilerFlag)

# Check for SSE4.2 support
check_cxx_compiler_flag("-msse4.2" HAS_SSE42_FLAG)
if(HAS_SSE42_FLAG)
    check_cxx_source_compiles("
        #include <nmmintrin.h>
        int main() {
            __m128i a = _mm_set1_epi32(1);
            __m128i b = _mm_set1_epi32(2);
            __m128i c = _mm_add_epi32(a, b);
            return _mm_extract_epi32(c, 0);
        }
    " HAS_SSE42)
endif()

# Check for AVX2 support
check_cxx_compiler_flag("-mavx2" HAS_AVX2_FLAG)
if(HAS_AVX2_FLAG)
    set(CMAKE_REQUIRED_FLAGS "-mavx2")
    check_cxx_source_compiles("
        #include <immintrin.h>
        int main() {
            __m256i a = _mm256_set1_epi32(1);
            return _mm256_extract_epi32(a, 0);
        }
    " HAS_AVX2)
    unset(CMAKE_REQUIRED_FLAGS)
endif()

# Check for ARM NEON
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64|arm64")
    check_cxx_source_compiles("
        #include <arm_neon.h>
        int main() {
            float32x4_t a = vdupq_n_f32(1.0f);
            float32x4_t b = vdupq_n_f32(2.0f);
            float32x4_t c = vaddq_f32(a, b);
            return 0;
        }
    " HAS_NEON)
endif()

add_executable(app main.cpp)
if(HAS_SSE42)
    target_compile_definitions(app PRIVATE USE_SSE42=1)
    target_compile_options(app PRIVATE -msse4.2)
endif()
if(HAS_AVX2)
    target_compile_definitions(app PRIVATE USE_AVX2=1)
    target_compile_options(app PRIVATE -mavx2)
endif()

Endianness and Sizes

For code that handles binary data, serialization, or network protocols, knowing the byte order is essential:

Byte Order Detection

cmake_minimum_required(VERSION 3.21)
project(Endianness LANGUAGES CXX)

include(TestBigEndian)
test_big_endian(IS_BIG_ENDIAN)

add_executable(app main.cpp)

if(IS_BIG_ENDIAN)
    target_compile_definitions(app PRIVATE PLATFORM_BIG_ENDIAN=1)
    message(STATUS "Big-endian platform detected")
else()
    target_compile_definitions(app PRIVATE PLATFORM_LITTLE_ENDIAN=1)
    message(STATUS "Little-endian platform detected")
endif()

# Also useful: CMAKE_SIZEOF_VOID_P for pointer size
target_compile_definitions(app PRIVATE
    SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}
)

Platform-Dependent Source Selection

A common pattern is selecting different implementation files based on the target platform:

cmake_minimum_required(VERSION 3.21)
project(PlatformSources LANGUAGES CXX)

# Common sources for all platforms
set(COMMON_SOURCES
    src/app.cpp
    src/config.cpp
    src/logger.cpp
)

# Platform-specific implementations
if(WIN32)
    set(PLATFORM_SOURCES
        src/platform/windows/filesystem_impl.cpp
        src/platform/windows/threading_impl.cpp
        src/platform/windows/network_impl.cpp
    )
elseif(APPLE)
    set(PLATFORM_SOURCES
        src/platform/macos/filesystem_impl.cpp
        src/platform/macos/threading_impl.cpp
        src/platform/macos/network_impl.cpp
    )
elseif(UNIX)
    set(PLATFORM_SOURCES
        src/platform/linux/filesystem_impl.cpp
        src/platform/linux/threading_impl.cpp
        src/platform/linux/network_impl.cpp
    )
endif()

add_executable(app ${COMMON_SOURCES} ${PLATFORM_SOURCES})

# Platform-specific libraries
if(WIN32)
    target_link_libraries(app PRIVATE ws2_32 userenv)
elseif(UNIX AND NOT APPLE)
    target_link_libraries(app PRIVATE pthread dl)
elseif(APPLE)
    target_link_libraries(app PRIVATE "-framework CoreFoundation" "-framework Security")
endif()

configure_file for Feature Detection

The configure_file() command generates C/C++ headers from templates, substituting CMake variables and #cmakedefine directives. This is the standard way to communicate detected features to your C++ code.

The config.h.in Pattern

First, create a template file config.h.in:

// config.h.in — processed by CMake's configure_file()
#pragma once

// Project metadata
#define PROJECT_NAME "@PROJECT_NAME@"
#define PROJECT_VERSION "@PROJECT_VERSION@"
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@

// Platform detection results
#cmakedefine HAS_STD_FILESYSTEM
#cmakedefine HAS_STD_FORMAT
#cmakedefine HAS_ALIGNED_ALLOC
#cmakedefine HAS_POSIX_MEMALIGN
#cmakedefine HAS_MMAP

// CPU features
#cmakedefine USE_SSE42
#cmakedefine USE_AVX2
#cmakedefine HAS_NEON

// Sizes
#define SIZEOF_VOID_P @CMAKE_SIZEOF_VOID_P@

// Endianness
#cmakedefine PLATFORM_BIG_ENDIAN
#cmakedefine PLATFORM_LITTLE_ENDIAN

Then in your CMakeLists.txt:

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

# ... (run all checks from previous sections) ...

# Generate config.h from template
configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/generated/config.h
    @ONLY  # Only substitute @VAR@ patterns, not ${VAR}
)

add_executable(app src/main.cpp)
target_include_directories(app PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated)
Key Insight: #cmakedefine VAR becomes #define VAR if the CMake variable is "truthy" (non-empty, not 0/FALSE/OFF), or /* #undef VAR */ otherwise. Use #cmakedefine01 VAR to always define it as #define VAR 0 or #define VAR 1. Use @ONLY to prevent accidental substitution of ${} patterns in your template.
Hands-On Generated config.h Project

Create a project that detects std::filesystem, aligned_alloc, and SSE4.2 support. Generate a config.h using configure_file(). Write main.cpp that #includes this header and prints which features are available using #ifdef checks.

configure_file #cmakedefine Feature Detection

System Information

The cmake_host_system_information() command queries detailed information about the host machine (where CMake is running):

cmake_minimum_required(VERSION 3.21)
project(SystemInfo LANGUAGES CXX)

# Query host system information
cmake_host_system_information(RESULT HOSTNAME QUERY HOSTNAME)
cmake_host_system_information(RESULT NUM_LOGICAL_CORES QUERY NUMBER_OF_LOGICAL_CORES)
cmake_host_system_information(RESULT NUM_PHYSICAL_CORES QUERY NUMBER_OF_PHYSICAL_CORES)
cmake_host_system_information(RESULT TOTAL_RAM_MB QUERY TOTAL_PHYSICAL_MEMORY)
cmake_host_system_information(RESULT AVAIL_RAM_MB QUERY AVAILABLE_PHYSICAL_MEMORY)
cmake_host_system_information(RESULT OS_NAME QUERY OS_NAME)
cmake_host_system_information(RESULT OS_RELEASE QUERY OS_RELEASE)
cmake_host_system_information(RESULT OS_VERSION QUERY OS_VERSION)
cmake_host_system_information(RESULT OS_PLATFORM QUERY OS_PLATFORM)

message(STATUS "=== Host System Information ===")
message(STATUS "Hostname: ${HOSTNAME}")
message(STATUS "OS: ${OS_NAME} ${OS_RELEASE} (${OS_PLATFORM})")
message(STATUS "Logical cores: ${NUM_LOGICAL_CORES}")
message(STATUS "Physical cores: ${NUM_PHYSICAL_CORES}")
message(STATUS "Total RAM: ${TOTAL_RAM_MB} MB")
message(STATUS "Available RAM: ${AVAIL_RAM_MB} MB")

# Use core count for parallel build hints
add_executable(app main.cpp)
target_compile_definitions(app PRIVATE
    HOST_NUM_CORES=${NUM_LOGICAL_CORES}
)

Cross-Compilation Awareness

When cross-compiling, the host (where CMake runs) differs from the target (where binaries will execute). CMake provides separate variables for each:

Host vs Target in Cross-Compilation
        flowchart LR
            A["Host Machine
(x86_64 Linux)"] -->|"CMake runs here
CMAKE_HOST_*"| B[CMake] B -->|"Generates build for"| C["Target Machine
(aarch64 Android)"] B -->|"CMAKE_SYSTEM_*
CMAKE_CROSSCOMPILING=TRUE"| C style A fill:#3B9797,color:#fff style C fill:#BF092F,color:#fff
cmake_minimum_required(VERSION 3.21)
project(CrossAware LANGUAGES CXX)

# CMAKE_CROSSCOMPILING is TRUE when using a toolchain file
if(CMAKE_CROSSCOMPILING)
    message(STATUS "Cross-compiling!")
    message(STATUS "  Host: ${CMAKE_HOST_SYSTEM_NAME} ${CMAKE_HOST_SYSTEM_PROCESSOR}")
    message(STATUS "  Target: ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_PROCESSOR}")
else()
    message(STATUS "Native build for ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_PROCESSOR}")
endif()

add_executable(app main.cpp)

# Don't run target executables during cross-compilation!
if(NOT CMAKE_CROSSCOMPILING)
    # Safe to run compiled programs (e.g., for code generation)
    add_custom_command(
        OUTPUT ${CMAKE_BINARY_DIR}/generated_code.cpp
        COMMAND app --generate-code > ${CMAKE_BINARY_DIR}/generated_code.cpp
        DEPENDS app
    )
endif()

# Use try_run carefully — it can't execute on cross targets
include(CheckCXXSourceRuns)
if(NOT CMAKE_CROSSCOMPILING)
    check_cxx_source_runs("
        int main() { return sizeof(void*) == 8 ? 0 : 1; }
    " CONFIRMED_64BIT)
endif()
Warning: When cross-compiling, check_cxx_source_runs() and try_run() cannot execute the compiled binary on the host. They will either fail or use cached results. Use check_cxx_source_compiles() (which only compiles, doesn't run) whenever possible, and guard try_run() calls with if(NOT CMAKE_CROSSCOMPILING).
Hands-On Toolchain File for Cross-Compilation

Create a toolchain file (aarch64-linux.cmake) that sets CMAKE_SYSTEM_NAME, CMAKE_SYSTEM_PROCESSOR, and compiler paths. Then create a CMakeLists.txt that behaves differently for native vs cross builds, guarding try_run() with CMAKE_CROSSCOMPILING.

Toolchain File CMAKE_CROSSCOMPILING ARM64