Files

862 lines
46 KiB
CMake

# =============================================================================
# Configuration.cmake
#
# The single source of truth for this project's identity, target
# selection, and output layout. Change a value here, or override with
# -D<NAME>=... at configure time, instead of hunting through
# CMakeLists.txt/cmake/*.cmake -- or a script under scripts/ -- for a
# hardcoded copy. If you find yourself typing a value this file already
# defines anywhere else, that's a bug: read it from here instead (see
# "How scripts read this file" below for the non-CMake side of that).
#
# -----------------------------------------------------------------------
# Project identity
# -----------------------------------------------------------------------
# PROJECT_CODE_NAME machine-friendly identifier baked into every
# target as PROJECT_NAME_CODE (BuildHelpers.cmake's
# _finalize_module_target) and used as the
# `cmake --install --component` name (CMakeLists.txt's
# install() calls, scripts/install.sh / install.bat).
# DEFAULT_BUILD_TYPE what CMAKE_BUILD_TYPE (single-config Ninja) and
# the scripts' own BUILD_TYPE argument default to
# when nothing else is specified.
#
# -----------------------------------------------------------------------
# Target selection (DIST_TARGET)
# -----------------------------------------------------------------------
# DIST_TARGET identifies what you're building for as a single
# "<platform>-<arch>-<compiler>" string. It is declared by the toolchain
# file in use (cmake/toolchains/<DIST_TARGET>.cmake -- what every
# CMakePresets.json preset selects, and the recommended way to build), or
# given explicitly with -DDIST_TARGET=<value> (if both are present they
# must agree), or, with neither, auto-detected from the compiler CMake
# found. Every value in PMN_VALID_DIST_TARGETS below is supported:
#
# linux-x64-gcc GCC on x86-64 Linux
# linux-x64-clang Clang on x86-64 Linux
# linux-arm64-gcc GCC on 64-bit ARM Linux (aarch64)
# linux-arm64-clang Clang on 64-bit ARM Linux (aarch64)
# linux-arm32-gcc GCC on 32-bit ARM Linux (armhf/armv7l and similar)
# linux-arm32-clang Clang on 32-bit ARM Linux
# macos-arm64-clang Clang on Apple Silicon macOS -- Apple Clang from
# the Xcode Command Line Tools, or LLVM's own
# Clang (e.g. Homebrew's); both are "clang" here
# macos-x64-clang Clang on Intel macOS (or, given Rosetta 2, an
# Apple Silicon Mac: the toolchain just asks the
# compiler for -arch x86_64)
# windows-x64-msvc cl.exe (MSVC) targeting x64
# windows-arm64-msvc cl.exe (MSVC) targeting ARM64
# windows-x64-clang-cl clang-cl targeting x64 (Clang targeting the
# MSVC ABI -- CMake's MSVC variable is TRUE for
# this too)
# windows-arm64-clang-cl clang-cl targeting ARM64
# windows-x64-clang Clang with its own GNU-style driver on Windows,
# targeting x64 (e.g. an MSYS2 CLANG64
# environment) -- distinct from clang-cl above,
# which mimics cl.exe instead of gcc
# windows-arm64-clang Clang with its own GNU-style driver on Windows,
# targeting ARM64 (e.g. an MSYS2 CLANGARM64
# environment)
# windows-x86-mingw32 32-bit x86 GCC via MinGW
# windows-x64-mingw64 64-bit x64 GCC via MinGW-w64
#
# No macOS GCC target and no universal one. Apple's own toolchain IS Clang
# (a Homebrew GCC would be a second, unrelated compiler ecosystem needing
# its own SDK and sysroot handling), and a universal binary is a
# CMAKE_OSX_ARCHITECTURES list with two entries -- which doesn't fit a
# DIST_TARGET that names ONE architecture, and is rejected below with the
# reason rather than mislabeled. To ship one universal file, build both
# macOS presets and combine the results: `lipo -create <arm64 file>
# <x64 file> -output <file>`.
#
# No windows-arm32-* and no ARM64 MinGW target: 32-bit ARM Windows has no
# real toolchain support left in any of the compilers above (MSVC dropped
# it; it only ever existed for Windows RT / Windows 10 IoT Core), and
# mainline MinGW-w64's GCC has no ARM64 Windows (PE/COFF) backend -- ARM64
# Windows toolchains in that ecosystem are LLVM-based (MSYS2's
# CLANGARM64, covered by windows-arm64-clang above), not GCC-based.
#
# Architecture (x64/arm64/arm32) is detected differently depending on
# compiler family, because CMAKE_SYSTEM_PROCESSOR turns out not to be
# trustworthy for all of them:
# - Linux (GCC/Clang) goes through cmake/HostArch.cmake's
# pmn_target_arch(), i.e. CMAKE_SYSTEM_PROCESSOR: `uname -m` for a
# native build, and whatever the toolchain file sets it to for a
# cross build (so an aarch64 cross build from an x64 machine is
# labeled arm64, not x64). Clang's GNU-style driver on Windows goes
# through pmn_host_arch() instead, an environment-variable check --
# see that file for why CMAKE_SYSTEM_PROCESSOR/
# CMAKE_HOST_SYSTEM_PROCESSOR can't be trusted there (a second,
# related trap: a 32-bit cmake.exe under WoW64, independent of the
# MSVC one below).
# - macOS goes through CMAKE_OSX_ARCHITECTURES, which the macOS toolchain
# files set to exactly one architecture, and only falls back to
# CMAKE_SYSTEM_PROCESSOR (`uname -m`) without one. Apple's clang is a
# single multi-architecture compiler, so an x86_64 build on an Apple
# Silicon Mac is just a different -arch -- not a cross compiler, and
# nothing that CMAKE_SYSTEM_PROCESSOR (which under Rosetta 2 reports
# the architecture of the cmake PROCESS, the same class of trap as
# WoW64 on Windows) could describe reliably.
# - MSVC and clang-cl use CMAKE_CXX_COMPILER_ARCHITECTURE_ID instead --
# CMAKE_SYSTEM_PROCESSOR is a documented, confirmed-in-the-wild trap
# here (tracked as CMake issue #25466): it reflects the architecture
# of the cmake.exe process itself, not what the selected cl.exe/
# clang-cl actually targets, and those two routinely differ (a
# same-machine cross build -- e.g. an x64 Visual Studio install's
# "HostX64/ARM64" tools -- is the normal way to target ARM64 from an
# x64 dev machine).
#
# Toolchain files and cross-compiling: cmake/toolchains/ has one toolchain
# file per DIST_TARGET, named after it, and every CMakePresets.json preset
# selects the one named after itself. A toolchain file settles the compiler
# (and, for clang-cl, the --target triple) before project() runs, declares
# the DIST_TARGET it is for, and -- where the target isn't the machine
# running CMake -- sets up a real cross build: the linux-arm64-* and
# linux-arm32-* targets cross-compile from any other Linux machine (an x64
# one, say), and the windows-*-mingw* targets from Linux via mingw-w64,
# while on matching hardware (ARM64 for linux-arm64-*) the very same
# preset just builds natively. On macOS the toolchain pins the
# architecture with CMAKE_OSX_ARCHITECTURES and a minimum macOS version
# with CMAKE_OSX_DEPLOYMENT_TARGET (11.0 unless you pass your own) -- both
# are only honored when set BEFORE project(), which is exactly when a
# toolchain file runs. cmake/toolchains/_common.cmake documents
# exactly what each one does and which packages it needs. The exception is
# MSVC: cl.exe takes its architecture from the Visual Studio developer
# environment it is started from (vcvarsall.bat), which no CMake file can
# switch, so the windows-*-msvc toolchains only pick cl and check it is
# there -- the architecture check below then verifies it is the right one.
#
# Without a toolchain file, pointing -DCMAKE_CXX_COMPILER at a cross
# compiler is not enough on its own: CMake is never told the target is a
# different architecture, so the auto-detected DIST_TARGET follows the
# HOST and you'll get a cross-check mismatch (see below) instead, same as
# any other unmet assumption this file's auto-detection makes.
#
# IMPORTANT: -DDIST_TARGET=... on its own only labels the build (which
# install subdirectory it lands in, which metadata gets printed); it does
# NOT select a compiler or an architecture. CMake still uses whatever
# CMAKE_CXX_COMPILER it would otherwise find -- your PATH, your
# environment, or an explicit -DCMAKE_CXX_COMPILER=... -- same as if
# DIST_TARGET had never been mentioned. Passing
# -DDIST_TARGET=windows-x64-mingw64 alone, with no MinGW compiler actually
# configured, does not make CMake go find one; it configures whatever
# compiler it would have anyway (often MSVC, since CMake looks for that
# first on Windows) and mislabels the result. Configuration.cmake catches
# exactly this: a DIST_TARGET that disagrees with what the real compiler
# (and architecture) auto-detects to is a FATAL_ERROR, not a silent
# mislabel -- see the "cross-check" step below. A toolchain file doesn't
# have the problem, because it sets the label and the compiler together:
# use the toolchain file for that target
# (-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<DIST_TARGET>.cmake), or simply
# the matching CMakePresets.json preset, which selects it.
#
# Auto-detection FATAL_ERRORs rather than guessing when the compiler
# doesn't match any of the above, instead of silently assuming MSVC --
# that silent assumption is exactly what used to misidentify a MinGW GCC
# build as MSVC (COMPILER_ID "msvc" for an actual GNU compiler), which in
# turn misfiled its `cmake --install` output under .../windows-x64-msvc/.
#
# PLATFORM, ARCHITECTURE, and COMPILER_ID below are metadata derived from
# DIST_TARGET; CMakeLists.txt and the cmake/*.cmake modules consume these
# rather than re-deriving them.
#
# -----------------------------------------------------------------------
# Output layout
# -----------------------------------------------------------------------
# Everything this project generates lives under one OUT_DIR:
#
# out/
# build/<preset>/ CMAKE_BINARY_DIR -- one build tree per
# preset (BUILD_OUTPUT_DIR is the
# root), so switching presets never
# reuses, invalidates or wipes another
# preset's cache. CMakePresets.json's
# binaryDir and scripts/build.sh
# (-B out/build/<preset>) agree on it;
# without a preset the scripts use
# out/build/default. Compiled
# artifacts land in
# <build>/<RUNTIME_OUTPUT_SUBDIR> and
# <build>/<ARCHIVE_OUTPUT_SUBDIR>
# during normal day-to-day builds.
# install/<DIST_TARGET>/{bin,lib} what `cmake --install` / scripts/
# install.sh populates -- the same
# name as build/<preset>, since a
# preset is named after its target.
# dist/ reserved for packaged/archived
# output (e.g. a future CPack
# integration) -- not populated by
# this project yet, but the path is
# configured and ready.
# docs/<module>/html, docs/index.html generated Doxygen output
# (cmake/Documentation.cmake).
#
# One thing lives outside OUT_DIR on purpose: .cache/tools/ (TOOLCACHE_DIR)
# caches downloaded, pinned build tools (currently Ninja and Doxygen), kept
# separate so a clean `rm -rf out/` doesn't force a re-download.
#
# .gitignore is kept in sync with the paths below automatically, every
# configure -- see the bottom of this file.
#
# -----------------------------------------------------------------------
# How scripts read this file
# -----------------------------------------------------------------------
# scripts/build.sh|build.bat and scripts/install.sh|install.bat need
# values from here too (the install component name, the default build
# type, where the tool cache lives) but are running before any CMake
# configure exists to ask. They get them from cmake/PrintConfig.cmake, a
# tiny `cmake -P` script that includes this file and prints one value --
# see that file for the exact mechanism, and any of the four scripts
# above for how each shell dialect calls it. Choosing WHICH build
# directory a script works on (a --preset, or the one built last) is the
# same kind of job and is done the same way, by cmake/BuildDir.cmake, so
# that logic exists once for both shell dialects. That's what makes this file
# a *real* source of truth end to end: nothing outside it -- not a
# .cmake module, not a shell script, not .gitignore -- hardcodes a second
# copy of a value defined here.
#
# One consequence: a plain `cmake -P` script never runs project() and so
# never gets real compiler detection -- WIN32, CMAKE_CXX_COMPILER_ID,
# CMAKE_SIZEOF_VOID_P etc. can't be trusted there. Everything in "Target
# selection" above is therefore skipped when PMN_CONFIG_QUERY_ONLY is set
# (cmake/PrintConfig.cmake sets it before including this file), along
# with this file's own filesystem side effects (creating out/dist/,
# rewriting .gitignore) -- a query has no business doing either, and it
# runs far more often than a real configure, which makes that doubly
# true. Keep that in mind if you add to this file: anything that needs a
# real compiler belongs inside the `if(NOT PMN_CONFIG_QUERY_ONLY)` guard
# below (where it's simply unavailable to a script query, on purpose);
# anything else must stay evaluable without one, and must not
# message(STATUS ...) outside that guard either, or it'll show up as
# extra noise in a script's query output (see PrintConfig.cmake).
#
# Included by CMakeLists.txt right after project(), before any other
# variable defined here is used, so every value is available project-wide
# from that point on.
# =============================================================================
include_guard(GLOBAL)
# -------------------------------------------------------
# Project identity
# -------------------------------------------------------
set(PROJECT_CODE_NAME "demo" CACHE STRING
"Machine-friendly identifier: baked into targets as PROJECT_NAME_CODE and used as the `cmake --install` component name")
set(DEFAULT_BUILD_TYPE "Release" CACHE STRING
"CMAKE_BUILD_TYPE / scripts BUILD_TYPE default when none is given")
# -------------------------------------------------------
# Target selection -- see the file header for the full model. Skipped in
# query mode (see "How scripts read this file" above): it needs real
# compiler detection that a bare `cmake -P` script never performs, and no
# script currently needs DIST_TARGET/PLATFORM/ARCHITECTURE/COMPILER_ID
# anyway -- cmake --install reads what a real configure already baked
# into the build directory, not these variables directly.
# -------------------------------------------------------
if(NOT PMN_CONFIG_QUERY_ONLY)
set(PMN_VALID_DIST_TARGETS
linux-x64-gcc
linux-x64-clang
linux-arm64-gcc
linux-arm64-clang
linux-arm32-gcc
linux-arm32-clang
macos-arm64-clang
macos-x64-clang
windows-x64-msvc
windows-arm64-msvc
windows-x64-clang-cl
windows-arm64-clang-cl
windows-x64-clang
windows-arm64-clang
windows-x86-mingw32
windows-x64-mingw64
)
list(JOIN PMN_VALID_DIST_TARGETS " " _pmn_valid_dist_targets_str)
# Where DIST_TARGET came from, for the messages below. A toolchain file
# (cmake/toolchains/<DIST_TARGET>.cmake -- every CMakePresets.json preset
# selects the one named after itself) declares the target it is for via
# PMN_TOOLCHAIN_DIST_TARGET (see toolchains/_common.cmake), so the label
# and the compiler the toolchain picked travel together. It is resolved
# here, ahead of the auto-detection below, because
# _pmn_dist_target_undetected() decides how hard to fail based on
# whether DIST_TARGET is already known. An explicit -DDIST_TARGET is
# still accepted alongside a toolchain, but it has to agree with it.
set(_pmn_dist_target_source "")
if(PMN_TOOLCHAIN_DIST_TARGET)
if(DIST_TARGET AND NOT DIST_TARGET STREQUAL PMN_TOOLCHAIN_DIST_TARGET)
message(FATAL_ERROR
"DIST_TARGET was explicitly set to '${DIST_TARGET}', but the toolchain file in use "
"(${CMAKE_TOOLCHAIN_FILE}) is for '${PMN_TOOLCHAIN_DIST_TARGET}'.\n"
"Drop -DDIST_TARGET (the toolchain already says which target this is), or "
"use the toolchain file for '${DIST_TARGET}':\n"
" ${CMAKE_SOURCE_DIR}/cmake/toolchains/${DIST_TARGET}.cmake")
endif()
set(DIST_TARGET "${PMN_TOOLCHAIN_DIST_TARGET}")
set(_pmn_dist_target_source "toolchain")
elseif(DIST_TARGET)
set(_pmn_dist_target_source "explicit")
endif()
# _pmn_dist_target_undetected(REASON...): an auto-detection branch
# below hit a case with no DIST_TARGET for it. REASON explains why --
# shown whether or not DIST_TARGET was explicitly given (e.g. via a
# CMakePresets.json preset), so a preset user gets the same specific
# diagnosis a plain, no-DIST_TARGET user already did.
#
# Previously REASON was only ever shown when DIST_TARGET was unset;
# setting DIST_TARGET (which every preset does -- the recommended way
# to build, per CMakePresets.json and the README) silently discarded
# it in favor of the generic "auto-detects to none of the values this
# project recognizes" cross-check message further down, with no
# explanation of WHY -- exactly the confusing, unhelpful failure this
# macro exists to prevent.
#
# No DIST_TARGET means there's nothing to fall back on, so REASON is
# the final word: FATAL_ERROR, with the full valid-values list. With
# DIST_TARGET set, REASON is advisory context for the unified
# cross-check FATAL_ERROR further down (which still runs, and has the
# copy-pasteable fix) -- so this is STATUS, and _pmn_detected_dist_target
# is left at "" for that check to pick up, same as before this macro
# existed.
macro(_pmn_dist_target_undetected)
if(DIST_TARGET)
set(_pmn_detected_dist_target "")
message(STATUS "Could not auto-detect a DIST_TARGET: " ${ARGN})
else()
message(FATAL_ERROR "Could not auto-detect a DIST_TARGET: " ${ARGN}
"\nValid values: ${_pmn_valid_dist_targets_str}")
endif()
endmacro()
include("${CMAKE_SOURCE_DIR}/cmake/HostArch.cmake")
# What DIST_TARGET the CXX compiler CMake actually found auto-detects
# to -- computed unconditionally, not only when DIST_TARGET is unset,
# so an explicitly-given DIST_TARGET can be cross-checked against it
# below rather than trusted as a label. See the file header for why
# that check exists.
if(WIN32)
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" OR
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"))
# MSVC, and clang-cl (Clang's cl.exe-compatible driver
# personality, selected either by invoking a binary literally
# named clang-cl(.exe) or via `clang++ --driver-mode=cl`;
# CMake surfaces this as CMAKE_CXX_COMPILER_FRONTEND_VARIANT
# rather than a distinct COMPILER_ID, since it's the same
# Clang either way -- just a different argument dialect and
# default target ABI). Both get their architecture from
# CMAKE_CXX_COMPILER_ARCHITECTURE_ID, deliberately NOT from
# CMAKE_SYSTEM_PROCESSOR/CMAKE_HOST_SYSTEM_PROCESSOR (see
# HostArch.cmake's header for why those can't be trusted for
# this specific case). CMAKE_CXX_COMPILER_ARCHITECTURE_ID is
# what CMake itself derives by probing the compiler -- it
# needs this internally for the right /machine: linker flag --
# so it reflects the real target regardless of which cl.exe/
# clang-cl variant ended up on PATH. Its values are
# mixed-case by long-standing CMake convention (X86, x64,
# ARM, ARM64); normalized to uppercase here so the
# comparisons below aren't tripped by that inconsistency.
string(TOUPPER "${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}" _pmn_msvc_arch_id)
if(_pmn_msvc_arch_id STREQUAL "X64")
set(_pmn_win_arch "x64")
elseif(_pmn_msvc_arch_id STREQUAL "ARM64")
set(_pmn_win_arch "arm64")
else()
# Includes X86 and ARM (32-bit) -- real MSVC targets, but
# not ones this project has a DIST_TARGET for (32-bit ARM
# Windows has no real toolchain support left in any
# compiler; see the file header). Treated as unrecognized
# rather than silently mislabeled as x64 or arm64.
set(_pmn_win_arch "")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set(_pmn_win_compiler_id "msvc")
else()
set(_pmn_win_compiler_id "clang-cl")
endif()
if(_pmn_win_arch)
set(_pmn_detected_dist_target "windows-${_pmn_win_arch}-${_pmn_win_compiler_id}")
else()
# X86 specifically gets its own hint rather than the
# generic message below: it's by far the most likely way
# to actually hit this branch in practice (ARM on Windows
# without ARM64 is rare) -- not a machine that's genuinely
# 32-bit-only (vanishingly rare today), almost always
# PATH/environment handing CMake the 32-bit-hosted,
# 32-bit-targeting cl.exe/clang-cl instead of the x64 one,
# e.g. from opening a plain shell instead of an "x64
# Native Tools Command Prompt for VS". Reported whether or
# not DIST_TARGET was explicitly given -- see
# _pmn_dist_target_undetected above.
if(_pmn_msvc_arch_id STREQUAL "X86")
_pmn_dist_target_undetected(
"${_pmn_win_compiler_id} is "
"targeting X86 (32-bit) -- almost always a sign that PATH/the "
"current shell is handing CMake the 32-bit-hosted, "
"32-bit-targeting ${_pmn_win_compiler_id}, not that this machine "
"is actually 32-bit-only.\n"
"Open an \"x64 Native Tools Command Prompt for VS\" (or a "
"\"Developer PowerShell for VS\" defaulted to x64) so an x64 "
"${_pmn_win_compiler_id} is what's first on PATH, delete this "
"build directory (a failed configure's CMakeCache.txt won't just "
"fix itself), and reconfigure.\n"
"If 32-bit really is intentional, this project doesn't have a "
"DIST_TARGET for it -- set one explicitly: -DDIST_TARGET=<value>"
)
else()
_pmn_dist_target_undetected(
"${_pmn_win_compiler_id} is "
"targeting an architecture ('${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}') "
"this project doesn't have a DIST_TARGET for -- set one "
"explicitly: -DDIST_TARGET=<value>"
)
endif()
endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# Clang with its own GNU-style driver on Windows (e.g. an
# MSYS2 CLANG64/CLANGARM64 environment) -- distinct from
# clang-cl above, which mimics cl.exe instead of gcc.
# CMAKE_CXX_COMPILER_ARCHITECTURE_ID is specific to the MSVC-
# style detection used above and isn't populated for this
# driver mode. Falls back to HostArch.cmake instead -- unlike
# the raw cl.exe/clang-cl case, a GNU-style environment like
# MSYS2 is used as a coherent whole (its own cmake, its own
# clang++, both built for and normally run on the same
# architecture), the same assumption the MinGW pointer-size
# check below already makes for mingw32 vs. mingw64.
pmn_host_arch(_pmn_win_clang_arch _pmn_win_clang_raw)
if(_pmn_win_clang_arch STREQUAL "arm64")
set(_pmn_detected_dist_target "windows-arm64-clang")
elseif(_pmn_win_clang_arch STREQUAL "x64")
set(_pmn_detected_dist_target "windows-x64-clang")
else()
_pmn_dist_target_undetected(
"unrecognized host "
"architecture '${_pmn_win_clang_raw}' for Clang's GNU-style driver "
"on Windows -- set one explicitly: -DDIST_TARGET=<value>"
)
endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# A real GCC on Windows is MinGW (mingw.org's original,
# 32-bit-only project) or MinGW-w64 (adds 64-bit; also
# provides a 32-bit target, conventionally called
# "mingw32" too since it's the direct successor). Tell
# them apart by pointer size rather than parsing the
# compiler's own executable name, which is a config
# choice, not something to depend on. No ARM MinGW target
# here on purpose, unlike the Clang branch above: mainline
# MinGW-w64's GCC has no ARM64 Windows (PE/COFF) backend --
# ARM64 Windows toolchains in that ecosystem are LLVM-based
# (MSYS2's CLANGARM64), not GCC-based.
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(_pmn_detected_dist_target "windows-x64-mingw64")
else()
set(_pmn_detected_dist_target "windows-x86-mingw32")
endif()
elseif(DIST_TARGET)
# Can't cross-check an explicit value against a compiler this
# doesn't recognize at all -- the mismatch message below
# handles an empty detected value.
set(_pmn_detected_dist_target "")
else()
message(FATAL_ERROR
"Could not auto-detect a DIST_TARGET for CXX compiler "
"'${CMAKE_CXX_COMPILER_ID}' on Windows -- set one "
"explicitly: -DDIST_TARGET=<value>\n"
"Valid values: ${_pmn_valid_dist_targets_str}")
endif()
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# macOS (CMAKE_SYSTEM_NAME is "Darwin" for macOS only; iOS and
# friends have their own names). The architecture is what
# CMAKE_OSX_ARCHITECTURES says when it's set -- the macOS toolchain
# files always set it, to one value -- else CMAKE_SYSTEM_PROCESSOR.
# See the file header for why not CMAKE_SYSTEM_PROCESSOR first.
# Mapped explicitly rather than through HostArch.cmake's spelling
# table, which files anything starting with "arm" -- arm64e, say --
# under 32-bit ARM.
set(_pmn_macos_arch "")
if(CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _pmn_macos_arch_count)
if(_pmn_macos_arch_count GREATER 1)
message(FATAL_ERROR
"CMAKE_OSX_ARCHITECTURES is '${CMAKE_OSX_ARCHITECTURES}' -- several "
"architectures, i.e. a universal binary. A DIST_TARGET names exactly one "
"architecture (macos-arm64-clang, macos-x64-clang): build each with its "
"preset and combine the results with `lipo -create <arm64 file> <x64 file> "
"-output <file>` if you need a single universal file.")
elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
set(_pmn_macos_arch "arm64")
elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64")
set(_pmn_macos_arch "x64")
endif()
set(_pmn_macos_arch_raw "CMAKE_OSX_ARCHITECTURES '${CMAKE_OSX_ARCHITECTURES}'")
else()
pmn_target_arch(_pmn_macos_arch _pmn_macos_arch_processor)
set(_pmn_macos_arch_raw "CMAKE_SYSTEM_PROCESSOR '${_pmn_macos_arch_processor}'")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
if(_pmn_macos_arch)
set(_pmn_detected_dist_target "macos-${_pmn_macos_arch}-clang")
else()
_pmn_dist_target_undetected(
"unrecognized macOS architecture (${_pmn_macos_arch_raw}) -- set one "
"explicitly: -DDIST_TARGET=<value>"
)
endif()
elseif(DIST_TARGET)
set(_pmn_detected_dist_target "")
else()
message(FATAL_ERROR
"Could not auto-detect a DIST_TARGET for CXX compiler "
"'${CMAKE_CXX_COMPILER_ID}' on macOS -- only Clang (Apple's, from the "
"Xcode Command Line Tools, or LLVM's own) is a supported macOS target. "
"Set one explicitly: -DDIST_TARGET=<value>\n"
"Valid values: ${_pmn_valid_dist_targets_str}")
endif()
else()
# Linux: architecture comes from CMAKE_SYSTEM_PROCESSOR via
# HostArch.cmake -- reliable here (unlike the raw cl.exe/clang-cl
# case above). For a native build that is simply `uname -m`; for a
# cross build the toolchain file (cmake/toolchains/) sets it to the
# TARGET's architecture, which is exactly what should be labeled --
# asking the host machine instead would call an aarch64 cross
# build "x64".
pmn_target_arch(_pmn_linux_arch)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# Not a blind TOLOWER of CMAKE_CXX_COMPILER_ID: CMake's own ID
# for GCC is "GNU" (a case-fold gives "gnu", which none of
# PMN_VALID_DIST_TARGETS above or CMakePresets.json spell --
# every one of them, and everyone outside CMake, calls it
# "gcc"). Same reasoning the MSVC/clang-cl branch above
# already applies to its own two compiler IDs.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(_pmn_linux_compiler_id "gcc")
else()
set(_pmn_linux_compiler_id "clang")
endif()
if(_pmn_linux_arch)
set(_pmn_detected_dist_target "linux-${_pmn_linux_arch}-${_pmn_linux_compiler_id}")
else()
_pmn_dist_target_undetected(
"unrecognized "
"CMAKE_SYSTEM_PROCESSOR '${CMAKE_SYSTEM_PROCESSOR}' -- set one "
"explicitly: -DDIST_TARGET=<value>"
)
endif()
elseif(DIST_TARGET)
set(_pmn_detected_dist_target "")
else()
message(FATAL_ERROR
"Could not auto-detect a DIST_TARGET for CXX compiler "
"'${CMAKE_CXX_COMPILER_ID}' -- set one explicitly: "
"-DDIST_TARGET=<value>\n"
"Valid values: ${_pmn_valid_dist_targets_str}")
endif()
endif()
if(NOT DIST_TARGET)
set(DIST_TARGET "${_pmn_detected_dist_target}")
message(STATUS "DIST_TARGET not set -- auto-detected: ${DIST_TARGET}")
message(STATUS " (use a CMakePresets.json preset -- or -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<target>.cmake -- to set it explicitly)")
else()
if(NOT DIST_TARGET IN_LIST PMN_VALID_DIST_TARGETS)
message(FATAL_ERROR
"Unsupported DIST_TARGET: '${DIST_TARGET}'.\n"
"Valid values: ${_pmn_valid_dist_targets_str}\n"
"Tip: open CMakePresets.json in your IDE to select a preset, "
"or pass -DDIST_TARGET=<value> on the command line.")
endif()
if(NOT DIST_TARGET STREQUAL _pmn_detected_dist_target)
# This is the check described in the file header: a DIST_TARGET
# that disagrees with the compiler CMake actually found.
if(_pmn_detected_dist_target)
set(_pmn_detected_desc "'${_pmn_detected_dist_target}'")
else()
set(_pmn_detected_desc "none of the values this project recognizes")
endif()
# The fix depends on where DIST_TARGET came from. From a
# toolchain file the compiler was already chosen to match, so
# something overrode it afterwards; from a bare -DDIST_TARGET
# nothing ever chose a compiler at all.
#
# (string(CONCAT) rather than a multi-value set(): a list would
# swallow every ";" in the text when expanded into message().)
if(_pmn_dist_target_source STREQUAL "toolchain")
string(CONCAT _pmn_fix
"The toolchain file picked its own compiler, so this means it was overridden "
"afterwards -- most likely an explicit -DCMAKE_CXX_COMPILER=... (on macOS "
"also -DCMAKE_OSX_ARCHITECTURES=...), or a CMakeCache.txt left in this "
"build directory by an earlier configure for a different target.\n"
"Remove that override, or configure into a fresh build directory. The default "
"layout already gives every preset its own (out/build/<preset>); "
"${CMAKE_BINARY_DIR} is being shared between targets.")
else()
string(CONCAT _pmn_fix
"-DDIST_TARGET=... only labels the build; it does not select a compiler. Use "
"the toolchain file made for that target, which sets both together:\n"
" -DCMAKE_TOOLCHAIN_FILE=${CMAKE_SOURCE_DIR}/cmake/toolchains/${DIST_TARGET}.cmake\n"
"or the matching CMakePresets.json preset, which selects it for you:\n"
" cmake --preset ${DIST_TARGET}")
endif()
# Some compilers can't be redirected from CMake at all; say so
# rather than only pointing at a toolchain file that cannot
# help.
set(_pmn_family_hint "")
if(DIST_TARGET MATCHES "-msvc$")
set(_pmn_family_hint
"\ncl.exe's architecture is decided by the Visual Studio developer environment \
it is run from, not by CMake: open the matching Native Tools / Cross Tools Command Prompt \
(vcvarsall.bat).")
elseif(DIST_TARGET MATCHES "^windows-(x64|arm64)-clang$")
set(_pmn_family_hint
"\nThis GNU-style Clang gets its target from the shell environment it runs in: \
use an MSYS2 CLANG64 (x64) or CLANGARM64 (ARM64) shell.")
endif()
message(FATAL_ERROR
"DIST_TARGET is '${DIST_TARGET}', but the CXX compiler CMake actually found -- "
"${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} -- auto-detects to "
"${_pmn_detected_desc}, not '${DIST_TARGET}'.\n"
"${_pmn_fix}\n"
"${_pmn_family_hint}")
endif()
message(STATUS "DIST_TARGET = ${DIST_TARGET}")
endif()
# Platform / compiler metadata from DIST_TARGET.
if(DIST_TARGET STREQUAL "linux-x64-gcc")
set(PLATFORM "linux")
set(ARCHITECTURE "x64")
set(COMPILER_ID "gcc")
elseif(DIST_TARGET STREQUAL "linux-x64-clang")
set(PLATFORM "linux")
set(ARCHITECTURE "x64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "linux-arm64-gcc")
set(PLATFORM "linux")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "gcc")
elseif(DIST_TARGET STREQUAL "linux-arm64-clang")
set(PLATFORM "linux")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "linux-arm32-gcc")
set(PLATFORM "linux")
set(ARCHITECTURE "arm32")
set(COMPILER_ID "gcc")
elseif(DIST_TARGET STREQUAL "linux-arm32-clang")
set(PLATFORM "linux")
set(ARCHITECTURE "arm32")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "macos-arm64-clang")
set(PLATFORM "macos")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "macos-x64-clang")
set(PLATFORM "macos")
set(ARCHITECTURE "x64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "windows-x64-msvc")
set(PLATFORM "windows")
set(ARCHITECTURE "x64")
set(COMPILER_ID "msvc")
elseif(DIST_TARGET STREQUAL "windows-arm64-msvc")
set(PLATFORM "windows")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "msvc")
elseif(DIST_TARGET STREQUAL "windows-x64-clang-cl")
set(PLATFORM "windows")
set(ARCHITECTURE "x64")
set(COMPILER_ID "clang-cl")
elseif(DIST_TARGET STREQUAL "windows-arm64-clang-cl")
set(PLATFORM "windows")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "clang-cl")
elseif(DIST_TARGET STREQUAL "windows-x64-clang")
set(PLATFORM "windows")
set(ARCHITECTURE "x64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "windows-arm64-clang")
set(PLATFORM "windows")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "windows-x86-mingw32")
set(PLATFORM "windows")
set(ARCHITECTURE "x86")
set(COMPILER_ID "mingw32")
elseif(DIST_TARGET STREQUAL "windows-x64-mingw64")
set(PLATFORM "windows")
set(ARCHITECTURE "x64")
set(COMPILER_ID "mingw64")
else()
message(FATAL_ERROR
"Unsupported DIST_TARGET: '${DIST_TARGET}'.\n"
"Valid values: ${_pmn_valid_dist_targets_str}\n"
"Tip: open CMakePresets.json in your IDE to select a preset, "
"or pass -DDIST_TARGET=<value> on the command line.")
endif()
message(STATUS "Platform = ${PLATFORM}")
message(STATUS "Architecture = ${ARCHITECTURE}")
message(STATUS "Compiler = ${COMPILER_ID} (${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION})")
endif()
# -------------------------------------------------------
# Output paths
# -------------------------------------------------------
set(OUT_DIR "${CMAKE_SOURCE_DIR}/out" CACHE PATH
"Root directory for everything this project generates: build artifacts, install trees, docs, and reserved dist packaging output")
set(DIST_OUTPUT_DIR "${OUT_DIR}/dist" CACHE PATH
"Reserved for packaged/archived distribution output (e.g. a future CPack integration). Not populated by this project yet.")
set(BUILD_OUTPUT_DIR "${OUT_DIR}/build" CACHE PATH
"Root of the build trees: one subdirectory (the CMAKE_BINARY_DIR) per preset, so switching presets never reuses or wipes another preset's build")
set(INSTALL_OUTPUT_DIR "${OUT_DIR}/install" CACHE PATH
"Root of the cmake --install destination tree, one subdirectory per DIST_TARGET")
set(DOCS_OUTPUT_DIR "${OUT_DIR}/docs" CACHE PATH
"Root of generated Doxygen documentation, one subdirectory per module")
set(COMPILE_COMMANDS_DESTINATION "${CMAKE_SOURCE_DIR}/compile_commands.json" CACHE FILEPATH
"Where compile_commands.json is copied to after every build, for clangd/clang-tidy tooling (see CMakeLists.txt)")
# Deliberately NOT under OUT_DIR: this caches downloaded, pinned build
# tools (Ninja and Doxygen -- see cmake/FetchNinja.cmake and
# cmake/FetchDoxygen.cmake), which are machine-local and expensive to
# re-fetch, not build output. Keeping it separate means a clean `rm -rf
# out/` (a normal "start fresh" gesture) doesn't force a re-download.
set(TOOLCACHE_DIR "${CMAKE_SOURCE_DIR}/.cache/tools" CACHE PATH
"Where pinned build tools (currently Ninja and Doxygen) are cached, kept separate from OUT_DIR")
if(NOT PMN_CONFIG_QUERY_ONLY)
# out/dist/ is reserved (see the header comment above) and nothing writes
# to it yet, so it's created eagerly here -- otherwise it simply wouldn't
# exist until something populates it, which could easily read as "this
# was never wired up" rather than "intentionally reserved for later".
# Skipped in query mode along with the .gitignore sync below -- a value
# lookup shouldn't have filesystem side effects, and it runs far more
# often than a real configure.
file(MAKE_DIRECTORY "${DIST_OUTPUT_DIR}")
endif()
# Subdirectory NAMES only (not full paths) for where compiled artifacts
# land inside whatever the current build directory is (CMAKE_BINARY_DIR) --
# these apply correctly no matter where -B points, including if you build
# outside out/build/<preset> entirely. LIBRARY defaults to the same subdirectory as
# RUNTIME because shared libraries need to sit next to the executables
# that load them via the project's $ORIGIN-relative RPATH strategy (see
# CMakeLists.txt).
set(RUNTIME_OUTPUT_SUBDIR "bin" CACHE STRING "Build-dir subdirectory for runtime artifacts (executables, .dll, .so)")
set(LIBRARY_OUTPUT_SUBDIR "bin" CACHE STRING "Build-dir subdirectory for shared-library artifacts")
set(ARCHIVE_OUTPUT_SUBDIR "lib" CACHE STRING "Build-dir subdirectory for static/import-library artifacts (.a, .lib)")
# =============================================================================
# .gitignore sync
#
# Keeps a marked block in .gitignore in sync with the actual, current
# values of the path variables above -- every configure, automatically.
# Nothing outside that block (e.g. the Visual Studio template this
# project's .gitignore starts from) is ever touched. This is what makes
# this file the real source of truth for these paths specifically: change
# OUT_DIR (or override it with -DOUT_DIR=...) and .gitignore updates on
# the next configure instead of silently drifting out of sync with what
# actually gets generated.
#
# If .gitignore doesn't exist yet, it's created with just this block. If
# it exists but has no markers yet, the block is appended. If the markers
# are already there, only the content between them is replaced. The file
# is only rewritten when the computed content actually differs, so a
# normal reconfigure doesn't spuriously touch it (or its mtime).
#
# A path is only added if it resolves inside the repo (CMAKE_SOURCE_DIR)
# -- e.g. an overridden TOOLCACHE_DIR pointing somewhere else entirely is
# silently skipped, since .gitignore has nothing meaningful to say about
# a location outside the repository it lives in.
#
# Skipped entirely in query mode -- see "How scripts read this file"
# above.
# =============================================================================
if(NOT PMN_CONFIG_QUERY_ONLY)
set(_pmn_gitignore_begin "# >>> Configuration.cmake generated paths -- BEGIN (do not edit by hand; edit Configuration.cmake and reconfigure) >>>")
set(_pmn_gitignore_end "# <<< Configuration.cmake generated paths -- END <<<")
function(_pmn_gitignore_relpath ABSOLUTE_VALUE OUT_VAR)
file(RELATIVE_PATH _pmn_rel "${CMAKE_SOURCE_DIR}" "${ABSOLUTE_VALUE}")
if(_pmn_rel MATCHES "^\\.\\.")
set(${OUT_VAR} "" PARENT_SCOPE)
else()
set(${OUT_VAR} "${_pmn_rel}" PARENT_SCOPE)
endif()
endfunction()
set(_pmn_gitignore_entries "")
foreach(_pmn_dir_var OUT_DIR BUILD_OUTPUT_DIR DIST_OUTPUT_DIR INSTALL_OUTPUT_DIR DOCS_OUTPUT_DIR TOOLCACHE_DIR)
_pmn_gitignore_relpath("${${_pmn_dir_var}}" _pmn_rel)
if(_pmn_rel)
list(APPEND _pmn_gitignore_entries "${_pmn_rel}/")
endif()
endforeach()
_pmn_gitignore_relpath("${COMPILE_COMMANDS_DESTINATION}" _pmn_cc_rel)
if(_pmn_cc_rel)
list(APPEND _pmn_gitignore_entries "${_pmn_cc_rel}")
endif()
list(REMOVE_DUPLICATES _pmn_gitignore_entries)
list(SORT _pmn_gitignore_entries)
set(_pmn_gitignore_block_content "")
foreach(_pmn_entry ${_pmn_gitignore_entries})
string(APPEND _pmn_gitignore_block_content "${_pmn_entry}\n")
endforeach()
set(_pmn_gitignore_path "${CMAKE_SOURCE_DIR}/.gitignore")
set(_pmn_gitignore_new_block "${_pmn_gitignore_begin}\n${_pmn_gitignore_block_content}${_pmn_gitignore_end}")
if(EXISTS "${_pmn_gitignore_path}")
file(READ "${_pmn_gitignore_path}" _pmn_gitignore_content)
else()
set(_pmn_gitignore_content "")
endif()
string(FIND "${_pmn_gitignore_content}" "${_pmn_gitignore_begin}" _pmn_gitignore_begin_pos)
string(FIND "${_pmn_gitignore_content}" "${_pmn_gitignore_end}" _pmn_gitignore_end_pos)
if(_pmn_gitignore_begin_pos GREATER -1 AND _pmn_gitignore_end_pos GREATER -1
AND _pmn_gitignore_end_pos GREATER _pmn_gitignore_begin_pos)
# Replace only the content between the existing markers.
string(SUBSTRING "${_pmn_gitignore_content}" 0 ${_pmn_gitignore_begin_pos} _pmn_gitignore_before)
string(LENGTH "${_pmn_gitignore_end}" _pmn_gitignore_end_marker_len)
math(EXPR _pmn_gitignore_after_start "${_pmn_gitignore_end_pos} + ${_pmn_gitignore_end_marker_len}")
string(SUBSTRING "${_pmn_gitignore_content}" ${_pmn_gitignore_after_start} -1 _pmn_gitignore_after)
set(_pmn_gitignore_final "${_pmn_gitignore_before}${_pmn_gitignore_new_block}${_pmn_gitignore_after}")
else()
# No markers yet -- append (trimming trailing blank lines first so we
# don't accumulate extra blank lines across repeated appends).
string(REGEX REPLACE "\n+$" "" _pmn_gitignore_trimmed "${_pmn_gitignore_content}")
if(_pmn_gitignore_trimmed STREQUAL "")
set(_pmn_gitignore_final "${_pmn_gitignore_new_block}\n")
else()
set(_pmn_gitignore_final "${_pmn_gitignore_trimmed}\n\n${_pmn_gitignore_new_block}\n")
endif()
endif()
if(NOT _pmn_gitignore_final STREQUAL _pmn_gitignore_content)
file(WRITE "${_pmn_gitignore_path}" "${_pmn_gitignore_final}")
endif()
endif()