# ============================================================================= # 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=... 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 # "--" string. It is declared by the toolchain # file in use (cmake/toolchains/.cmake -- what every # CMakePresets.json preset selects, and the recommended way to build), or # given explicitly with -DDIST_TARGET= (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 # -output `. # # 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/.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// 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/) agree on it; # without a preset the scripts use # out/build/default. Compiled # artifacts land in # / and # / # during normal day-to-day builds. # install//{bin,lib} what `cmake --install` / scripts/ # install.sh populates -- the same # name as build/, 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//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/.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=" ) 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=" ) 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=" ) 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=\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 " "-output ` 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=" ) 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=\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=" ) 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=\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/.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= 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/); " "${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= 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/ 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()