commit b695b60153d56da4e1a2ffc4ba3828254641db05 Author: TEC-GO Date: Sat Sep 26 14:45:07 2026 -0300 Movimiento de "Cuadrado" veloz en mapa blanco diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..541defa --- /dev/null +++ b/.clang-format @@ -0,0 +1,117 @@ +--- +# .clang-format +# Enforces the formatting rules from the C/C++ Coding Style Guide (Part I: C++ Style). +# Applies equally to Part II C API files per §15 (Indentation and Braces) — same +# tabs/Allman/line-length rules, just different naming (naming can't be enforced +# by clang-format; see .clang-tidy). +Language: Cpp +Standard: Latest + +# --- Indentation (style guide §2) --- +UseTab: Always +TabWidth: 4 +IndentWidth: 4 +ContinuationIndentWidth: 4 +IndentCaseLabels: true +IndentPPDirectives: None +IndentExternBlock: Indent + +# Access specifiers (public:/private:) sit unindented, flush with the class +# keyword — only members are indented one level, matching every example in §1/§6. +AccessModifierOffset: -4 + +# Namespace bodies are indented like any other block (style guide §Namespaces). +NamespaceIndentation: All + +# --- Braces: Allman style everywhere (style guide §3) --- +BreakBeforeBraces: Allman +Cpp11BracedListStyle: true +# InlineOnly (not Empty or None): out-of-class function definitions always +# get full Allman expansion regardless of length, same as before -- this +# only additionally allows a function *defined inside a class body* to +# stay a compact one-liner if it's short enough to fit. That's specifically +# for the trivial-one-liner exception to "avoid inline member definitions" +# in Class Member Organization (style guide §6) -- a plain getter like +# `int getValue() const { return m_value; }` stays inline-and-compact; +# anything defined out-of-class still always gets the full brace expansion. +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortEnumsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false + +# No trailing "// namespace X" / "// class X" comments on closing braces +# (style guide §Namespaces / §6). +FixNamespaceComments: false + +# --- Pointers and references attach to the type (style guide §4) --- +PointerAlignment: Left +ReferenceAlignment: Pointer +DerivePointerAlignment: false + +# --- Line length (style guide §8) --- +ColumnLimit: 100 + +# --- Spacing (style guide §5) --- +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpacesInAngles: false + +# --- Include ordering (style guide §7: main header, C headers, C++ std, +# third-party, project headers) --- +SortIncludes: true +SortUsingDeclarations: true +IncludeBlocks: Regroup +IncludeIsMainRegex: '(Test)?$' +IncludeCategories: + # 1. Corresponding header is auto-detected and always sorted first. + # 2. C system headers wrapped for C++ (, , ...). + - Regex: '^<(cassert|cctype|cerrno|cfenv|cfloat|cinttypes|climits|clocale|cmath|csetjmp|csignal|cstdarg|cstddef|cstdint|cstdio|cstdlib|cstring|ctime|cuchar|cwchar|cwctype)>$' + Priority: 1 + CaseSensitive: true + # 3. C++ standard library headers (no dot, no slash — , , ...). + - Regex: '^<[a-z_]+>$' + Priority: 2 + CaseSensitive: true + # 4. Third-party / external library headers (, , ...). + - Regex: '^<.*>$' + Priority: 3 + CaseSensitive: true + # 5. Project headers, quoted ("NetworkManager.hpp", ...). + - Regex: '^".*"$' + Priority: 4 + CaseSensitive: true + +# --- Misc --- +AlignTrailingComments: true +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false + +# Parameters/arguments: never one-per-line (style guide §8). When a +# signature/call doesn't fit on one line, keep everything together on a +# single continuation line if it fits there (AllowAll...OnNextLine), and +# fall back to packing multiple per line (BinPack...) rather than ever +# breaking to one per line. This can't force a genuinely long signature +# to fit within ColumnLimit without wrapping at all -- doing that would +# require ColumnLimit: 0 project-wide, which would also stop comments and +# ordinary expressions from ever wrapping. See §8 for the full rule. +# +# COMPATIBILITY NOTE: clang-format 20 changed BinPackParameters (and the +# equivalent for call arguments) from a boolean to an enum, and later +# versions deprecated it again in favor of a nested PackParameters/ +# PackArguments option. The boolean form below is correct for clang-format +# through 19 (still the most common version in practice, e.g. Ubuntu +# 24.04 LTS ships 18). On clang-format 20 or newer, replace `true` with +# `BinPack` on both lines if you find these are being silently ignored. +BinPackArguments: true +BinPackParameters: true +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true + +BreakConstructorInitializers: BeforeColon +ConstructorInitializerIndentWidth: 4 +PenaltyReturnTypeOnItsOwnLine: 1000 diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..87e69b8 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,161 @@ +--- +# .clang-tidy +# Enforces naming conventions (style guide §1) and a subset of the Language +# Feature Do's and Don'ts (style guide §10) for C++ code. Place at the repo +# root for Part I (C++) files. For Part II (C API) files, use the +# directory-scoped override in c-api/.clang-tidy instead — clang-tidy picks +# the closest .clang-tidy file up the directory tree, so nesting one there +# gives that subtree different naming rules without affecting the rest of +# the project. +# +# readability-redundant-inline-specifier is a narrow, complementary aid for +# the "avoid inline member definitions" rule in §6 (Class Member +# Organization): it only catches an explicit `inline` keyword that's +# already redundant (e.g. on a function defined in the class body, which +# is implicitly inline regardless). It does not catch — and nothing in +# this file catches — an inline definition that omits the keyword +# entirely, which is the actual, common case the style guide rule is +# about; that one stays a code-review judgment call. +Checks: > + -*, + readability-identifier-naming, + readability-redundant-inline-specifier, + modernize-use-nullptr, + modernize-use-override, + modernize-use-equals-default, + modernize-use-equals-delete, + modernize-loop-convert, + modernize-concat-nested-namespaces, + modernize-use-default-member-init, + modernize-avoid-c-arrays, + modernize-use-nodiscard, + cppcoreguidelines-special-member-functions, + cppcoreguidelines-owning-memory, + cppcoreguidelines-no-malloc, + cppcoreguidelines-pro-type-member-init, + cppcoreguidelines-pro-type-cstyle-cast, + cppcoreguidelines-macro-usage, + performance-unnecessary-value-param, + performance-unnecessary-copy-initialization, + bugprone-exception-escape, + bugprone-use-after-move + +WarningsAsErrors: '' +HeaderFilterRegex: '.*\.hpp$' +FormatStyle: file + +CheckOptions: + # --- Naming Conventions — style guide §1 --- + - key: readability-identifier-naming.NamespaceCase + value: lower_case + + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.MethodCase + value: camelBack + + - key: readability-identifier-naming.VariableCase + value: camelBack + - key: readability-identifier-naming.LocalVariableCase + value: camelBack + - key: readability-identifier-naming.ParameterCase + value: camelBack + + # A plain local `const` (not `static`, not `constexpr`) stays under the + # Local Variables rule (camelCase) per the style guide's own note that + # only *global/static* const/constexpr values are "constants" for this + # rule's purposes -- without this, it silently inherits the general + # ConstantCase (PascalCase) fallback below instead, which would flag + # totally ordinary code like `const std::string name = ...;` inside a + # function. StaticConstantCase/GlobalConstantCase/ConstexprVariableCase + # below are unaffected and still require PascalCase, matching the guide. + - key: readability-identifier-naming.LocalConstantCase + value: camelBack + + # Private/protected member variables: camelCase with an m_ prefix. + - key: readability-identifier-naming.PrivateMemberCase + value: camelBack + - key: readability-identifier-naming.PrivateMemberPrefix + value: m_ + - key: readability-identifier-naming.ProtectedMemberCase + value: camelBack + - key: readability-identifier-naming.ProtectedMemberPrefix + value: m_ + # Public members (e.g. plain data structs) — camelCase, no prefix. + - key: readability-identifier-naming.PublicMemberCase + value: camelBack + + # Static class data members — camelCase with an s_ prefix (distinct from + # instance members above). "ClassMember" is clang-tidy's category for + # static (non-const) data members specifically. + - key: readability-identifier-naming.ClassMemberCase + value: camelBack + - key: readability-identifier-naming.ClassMemberPrefix + value: s_ + + # Global (namespace/file-scope, mutable) variables — camelCase with a g_ + # prefix, to flag genuinely global mutable state at every use site. + - key: readability-identifier-naming.GlobalVariableCase + value: camelBack + - key: readability-identifier-naming.GlobalVariablePrefix + value: g_ + + # Static variables — function-local `static` or file-scope `static` — + # camelCase with an s_ prefix, for the same reason as globals above. + - key: readability-identifier-naming.StaticVariableCase + value: camelBack + - key: readability-identifier-naming.StaticVariablePrefix + value: s_ + + # Constants and enum values — PascalCase (style guide §1 table). + - key: readability-identifier-naming.ConstantCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantCase + value: CamelCase + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + - key: readability-identifier-naming.ClassConstantCase + value: CamelCase + - key: readability-identifier-naming.ConstexprVariableCase + value: CamelCase + + # Template parameters — PascalCase (e.g. ValueType). + - key: readability-identifier-naming.TemplateParameterCase + value: CamelCase + - key: readability-identifier-naming.TypeTemplateParameterCase + value: CamelCase + + # Type aliases / typedefs — PascalCase, matching class naming. + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.TypedefCase + value: CamelCase + + # Macros — ALL_CAPS_SNAKE_CASE (the one deliberate exception in §1). + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + + # --- cppcoreguidelines-owning-memory — style guide §10, Memory and Ownership --- + # Flags raw new/delete and legacy C allocators used for ownership instead of + # std::unique_ptr / std::shared_ptr. + - key: cppcoreguidelines-owning-memory.LegacyResourceProducers + value: 'malloc;calloc;realloc;strdup;strndup' + - key: cppcoreguidelines-owning-memory.LegacyResourceConsumers + value: 'free;realloc' + + # --- cppcoreguidelines-macro-usage — style guide §10, prefer constexpr over #define --- + # Allow include-guard-style and project-prefixed macros; flag everything else + # that could instead be a constexpr constant. + - key: cppcoreguidelines-macro-usage.AllowedRegexp + value: '^[A-Z0-9_]+_H(PP)?$' + - key: cppcoreguidelines-macro-usage.CheckCapsOnly + value: false diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a36a39a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +# .editorconfig +# Baseline whitespace/indentation rules for editors that don't run +# clang-format live (or before it runs). See .clang-format for the +# authoritative, full formatting rules from the style guide. +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# Windows scripts are CRLF (see .gitattributes). Without this, the global +# end_of_line = lf above would have editors rewrite them as LF. +[*.{bat,cmd,ps1}] +end_of_line = crlf + +# Style guide §2 / §15 — tabs everywhere, C++ and C API alike. +[*.{c,h,cpp,hpp,cc,cxx,tpp}] +indent_style = tab +indent_size = 4 +tab_width = 4 +max_line_length = 100 + +# Config/build files typically use spaces by convention — not covered by the +# C/C++ style guide, kept here only so this file has a sane fallback for +# CMakeLists.txt, YAML, etc. in the same repo. +[*.{yml,yaml,cmake,json}] +indent_style = space +indent_size = 2 + +[CMakeLists.txt] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f53f6af --- /dev/null +++ b/.gitattributes @@ -0,0 +1,29 @@ +# .gitattributes +# +# Forces LF line endings for source files regardless of OS or each +# contributor's core.autocrlf setting. Without this, the same file can have +# CRLF on a Windows checkout and LF on Linux/Mac, which makes clang-format +# and the pre-commit hook's diff check report false formatting differences +# that have nothing to do with actual style. +* text=auto eol=lf + +# Be explicit for the style guide's own file types, on top of the general +# rule above. +*.c text eol=lf +*.h text eol=lf +*.cpp text eol=lf +*.hpp text eol=lf +*.cc text eol=lf +*.cxx text eol=lf +*.tpp text eol=lf + +# Windows-specific scripts keep CRLF (cmd.exe is fussy about LF-only batch +# files; PowerShell scripts follow the same convention). +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + +# Never touch binaries. +*.png binary +*.jpg binary +*.ico binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7214bf8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,439 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ + +[Dd]ebug/x64/ +[Dd]ebugPublic/x64/ +[Rr]elease/x64/ +[Rr]eleases/x64/ +bin/x64/ +obj/x64/ + +[Dd]ebug/x86/ +[Dd]ebugPublic/x86/ +[Rr]elease/x86/ +[Rr]eleases/x86/ +bin/x86/ +obj/x86/ + +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +.artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +**/.paket/paket.exe +paket-files/ + +# FAKE - F# Make +**/.fake/ + +# CodeRush personal settings +**/.cr/personal + +# Python Tools for Visual Studio (PTVS) +**/__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +#tools/** +#!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# >>> Configuration.cmake generated paths -- BEGIN (do not edit by hand; edit Configuration.cmake and reconfigure) >>> +.cache/tools/ +compile_commands.json +out/ +out/build/ +out/dist/ +out/docs/ +out/install/ +# <<< Configuration.cmake generated paths -- END <<< diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..09c342e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,397 @@ +### Project +cmake_minimum_required(VERSION 3.25) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +project( + DemoApp + VERSION 0.1.0 + DESCRIPTION "Demo app exercising the generator-friendly CMake module system" + LANGUAGES CXX +) + +# This project's identity (PROJECT_CODE_NAME, DEFAULT_BUILD_TYPE), target +# selection (DIST_TARGET/PLATFORM/ARCHITECTURE/COMPILER_ID), and output-path +# configuration (out/, install/, dist/, docs/, and the bin/lib subdirectory +# names) all live in Configuration.cmake -- included immediately after +# project() since it needs real compiler detection (for DIST_TARGET +# auto-detection) and since everything below depends on it. See that file +# for the full model; edit it, or pass -D=... at configure time, +# rather than hardcoding a value it already owns anywhere else in this +# project, scripts included. +include("${CMAKE_CURRENT_SOURCE_DIR}/Configuration.cmake") + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# This project uses C++20 but not C++20 modules. CMake >= 3.28 nevertheless +# assumes C++20 sources might `import` modules and scans every one of them +# before compiling (policy CMP0155) -- in any directory whose own +# cmake_minimum_required is >= 3.28, which includes fetched dependencies +# like fmt even though this project's own minimum is lower. The scan needs +# a scanner tool (clang-scan-deps for Clang, which e.g. Ubuntu's plain +# `clang` package doesn't ship, so the build dies with +# "CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS-NOTFOUND") and adds a step to every +# compile. This variable is CMake's documented opt-out; it is inherited by +# every target created below, third-party ones included. If you start using +# modules, remove it (or set the CXX_SCAN_FOR_MODULES property on just the +# targets that need it). +set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + +# Every static library in the project gets built with -fPIC by default -- +# including vendored/fetched ones, whose own CMakeLists.txt we don't +# control and can't rely on to opt in themselves. Without this, a static +# dependency (ours or third-party) fails to link the moment it ends up +# pulled into a SHARED module: "recompile with -fPIC". Cheap on every +# platform that matters here; add_lib_module's STATIC branch also sets +# this per-target as a belt-and-braces fallback. +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# ------------------------------------------------------- +# Generator guard -- require Ninja (single or multi-config) +# ------------------------------------------------------- +if(NOT CMAKE_GENERATOR MATCHES "Ninja") + message(WARNING + "This project targets Ninja. " + "Current generator: '${CMAKE_GENERATOR}'. " + "Reconfigure with: cmake -G Ninja or cmake -G \"Ninja Multi-Config\"") +endif() + +# Single-config Ninja needs CMAKE_BUILD_TYPE at configure time. +# Ninja Multi-Config uses CMAKE_CONFIGURATION_TYPES instead (no BUILD_TYPE needed). +if(CMAKE_GENERATOR STREQUAL "Ninja") + if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING + "Build type (Debug|Release|RelWithDebInfo|MinSizeRel)" FORCE) + message(STATUS "CMAKE_BUILD_TYPE not set -- defaulting to ${DEFAULT_BUILD_TYPE}") + endif() + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + Debug Release RelWithDebInfo MinSizeRel) +endif() + +# ------------------------------------------------------- +# Flat output layout +# Multi-config generators (Ninja Multi-Config) append / by default. +# Override all four configs so both Ninja variants produce the same layout. +# Subdirectory names come from Configuration.cmake (RUNTIME_OUTPUT_SUBDIR +# etc.); the base directory is always CMAKE_BINARY_DIR, wherever -B points +# (scripts/build.sh uses out/build/ -- see Configuration.cmake). +# ------------------------------------------------------- +foreach(_cfg Debug Release RelWithDebInfo MinSizeRel) + string(TOUPPER "${_cfg}" _CFG) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${_CFG} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${_CFG} "${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}") + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${_CFG} "${CMAKE_BINARY_DIR}/${ARCHIVE_OUTPUT_SUBDIR}") +endforeach() + +# ------------------------------------------------------- +# RPATH strategy +# By default CMake builds with a temporary RPATH and relinks at install time +# to embed the final one. Ninja on Linux with Clang does not support that +# relink step and will error out. Setting this ON tells CMake to embed the +# install RPATH at build time so no relink is ever needed. +# This is safe here because all our RPATHs are relative to the binary that +# carries them ($ORIGIN on Linux, @loader_path on macOS -- see +# _apply_rpath in cmake/BuildHelpers.cmake), so the same value is correct +# for both the build tree and the installed layout. Guarded to non-Windows +# since RPATH is an ELF/Mach-O concept only. (On macOS the same setting also +# spares CMake from rewriting load commands with install_name_tool at +# install time.) +# ------------------------------------------------------- +if(NOT WIN32) + set(CMAKE_BUILD_WITH_INSTALL_RPATH ON) +endif() + +# ------------------------------------------------------- +# options +# ------------------------------------------------------- +option(BUILD_TESTS "Build test binaries" OFF) +option(BUILD_EXAMPLES "Build example binaries" OFF) +option(INSTALL_TESTS "Install test binaries" OFF) +option(INSTALL_EXAMPLES "Install example binaries" OFF) + +# Project-wide default for add_lib_module() when a module doesn't pass +# TYPE explicitly. This is CMake's own BUILD_SHARED_LIBS variable, so it +# also affects any third-party add_library() calls (FetchContent, etc.) +# that respect it. Override per-module with add_lib_module(... TYPE +# STATIC|SHARED ...), or globally with -DBUILD_SHARED_LIBS=ON. +option(BUILD_SHARED_LIBS "Build libraries as SHARED by default" OFF) + +# INSTALL_* implies BUILD_* so targets exist when cmake --install runs +if(INSTALL_TESTS) + set(BUILD_TESTS ON CACHE BOOL "" FORCE) +endif() +if(INSTALL_EXAMPLES) + set(BUILD_EXAMPLES ON CACHE BOOL "" FORCE) +endif() + +message(STATUS "BUILD_TESTS = ${BUILD_TESTS}") +message(STATUS "BUILD_EXAMPLES = ${BUILD_EXAMPLES}") +message(STATUS "BUILD_SHARED_LIBS (default lib TYPE) = ${BUILD_SHARED_LIBS}") + +# ------------------------------------------------------- +# Global compiler settings +# +# MSVC is TRUE for both cl.exe and clang-cl (windows-x64-msvc and +# windows-x64-clang-cl -- Clang targeting the MSVC ABI), so the outer +# block covers both with the flags they share. Two flags are cl.exe-only, +# though: clang-cl doesn't implement them and reports "argument unused +# during compilation" for every translation unit, so they're gated on the +# compiler ID (MSVC == cl.exe; clang-cl reports Clang) rather than on +# the MSVC variable. +# +# Shared by cl.exe and clang-cl: +# /EHsc -- standard C++ exception handling +# /utf-8 -- UTF-8 source + execution charset +# cl.exe only: +# /MP -- parallel file compilation within one compiler +# invocation (Ninja also parallelizes across +# invocations; clang-cl ignores this) +# /Zc:preprocessor -- conformant preprocessor (fixes __VA_ARGS__ etc.); +# clang's preprocessor is always conformant +# ------------------------------------------------------- +if(MSVC) + add_compile_options(/EHsc /utf-8) + add_compile_definitions(_CRT_SECURE_NO_WARNINGS) + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_compile_options(/MP /Zc:preprocessor) + endif() +endif() + +# ------------------------------------------------------- +# MinGW runtime linking (windows-x86-mingw32 / windows-x64-mingw64) +# +# A MinGW build dynamically links libgcc/libstdc++/(on some distributions) +# libwinpthread by default, which then have to ship alongside the binary +# or sit on PATH -- easy to forget, and the exact DLL names differ between +# mingw32 and mingw64. Statically linking them instead trades a slightly +# larger binary for one that runs standalone, which is what you want for +# anything actually distributed. Verified directly (objdump -p on the +# result): this combination drops every non-Windows-system DLL from the +# import table, whether the toolchain's thread model is win32 +# (libwinpthread isn't linked in the first place; -static is then a no-op +# for it) or posix (it is, and -static covers it). MINGW is CMake's +# built-in equivalent of this project's own COMPILER_ID being "mingw32" +# or "mingw64". +# +# EXECUTABLE targets only, deliberately, via a generator expression +# rather than a plain add_link_options() -- add_lib_module's SHARED +# targets (e.g. core_lib -- see source/Core/CMakeLists.txt) build with +# WINDOWS_EXPORT_ALL_SYMBOLS ON (BuildHelpers.cmake), which re-exports +# every symbol a statically-linked libgcc/libstdc++ would pull in, +# unwinder included. A dependent executable that *also* statically links +# the unwinder then collides with the copy the DLL already re-exports: +# "multiple definition of `_Unwind_Resume'". Confirmed directly (this +# project's demo_core + hello/farewell fail to link with that error if +# this is widened to every target) -- shared library modules keep the +# default dynamic runtime; only the final executables that link against +# them go fully static. +# +# That leaves every SHARED target still dynamically depending on exactly +# the DLLs the executables no longer need -- confirmed directly too: an +# executable built this way starts fine standalone, but loading a sibling +# SHARED module next to it fails (module not found) without them. +# PMN_MINGW_RUNTIME_DLLS resolves the actual files so BuildHelpers.cmake's +# add_lib_module can copy them next to every SHARED target (both in the +# build tree and at install time -- see the "install" section below), +# rather than making SHARED targets static too, which would hit the exact +# same symbol collision as above, just from the other direction. The +# filename varies by exception model (SEH on x86_64; DWARF-2 or SJLJ on +# i686) and by whether this toolchain uses posix threads, so each +# candidate is resolved by asking the compiler rather than hardcoding +# one -- -print-file-name=NAME echoes NAME back unchanged if it can't +# find it, which is how "not applicable to this toolchain" is detected +# below. Computed once, here, rather than per SHARED module, since it's +# the same toolchain-wide answer every time. +# ------------------------------------------------------- +if(MINGW) + add_link_options( + "$<$,EXECUTABLE>:-static-libgcc;-static-libstdc++;-static;-lwinpthread>" + ) + + set(PMN_MINGW_RUNTIME_DLLS "") + foreach(_pmn_dll_candidate + libgcc_s_seh-1.dll + libgcc_s_dw2-1.dll + libgcc_s_sjlj-1.dll + libstdc++-6.dll + libwinpthread-1.dll + ) + execute_process( + COMMAND "${CMAKE_CXX_COMPILER}" -print-file-name=${_pmn_dll_candidate} + OUTPUT_VARIABLE _pmn_resolved_dll + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(EXISTS "${_pmn_resolved_dll}") + list(APPEND PMN_MINGW_RUNTIME_DLLS "${_pmn_resolved_dll}") + endif() + endforeach() + if(PMN_MINGW_RUNTIME_DLLS) + message(STATUS "MinGW runtime DLLs (bundled next to SHARED targets): ${PMN_MINGW_RUNTIME_DLLS}") + endif() +endif() + +# ------------------------------------------------------- +# Build machinery (macros/functions) -- see cmake/BuildHelpers.cmake. +# This file should change rarely; it holds nothing project-specific. +# ------------------------------------------------------- +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/BuildHelpers.cmake") + +# ------------------------------------------------------- +# Third-party dependencies -- see cmake/Dependencies.cmake for +# add_vendor_header_only / add_vendor_subdirectory / add_dependency. +# A module declares its own dependencies in its own CMakeLists.txt (see +# source/Core/CMakeLists.txt for an example); nothing to add here. +# ------------------------------------------------------- +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/Dependencies.cmake") + +# ------------------------------------------------------- +# Documentation -- see cmake/Documentation.cmake. Every module +# automatically gets its own Doxygen target the moment Doxygen is +# installed; nothing to add here or in a module's own CMakeLists.txt +# unless you want to opt a module out (NO_DOCS) or override a Doxyfile +# setting (-DDOXYGEN_=...). +# ------------------------------------------------------- +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/Documentation.cmake") + +# ------------------------------------------------------- +# Module discovery +# +# Every module lives in its own directory with its own CMakeLists.txt -- +# usually a single add_lib_module/add_exe_module/add_test_module/ +# add_example_module(...) call (see cmake/BuildHelpers.cmake for what each +# accepts). This file never needs editing to add, remove, or reconfigure +# a module: just add or remove the module's directory + CMakeLists.txt. +# +# source//CMakeLists.txt -- library or app/executable module +# source/tests//CMakeLists.txt -- test module +# source/examples//CMakeLists.txt -- example module +# +# (source/tests and source/examples aren't picked up by the first call +# below -- they have no CMakeLists.txt directly inside them, only their +# own children do, so they're only ever visited by their dedicated calls.) +# +# _discover_module_subdirectories() add_subdirectory()s every matching +# directory automatically; CONFIGURE_DEPENDS means the next build +# reconfigures on its own once a module directory is added or removed. +# ------------------------------------------------------- +_discover_module_subdirectories("${CMAKE_SOURCE_DIR}/source") +_discover_module_subdirectories("${CMAKE_SOURCE_DIR}/source/tests") +_discover_module_subdirectories("${CMAKE_SOURCE_DIR}/source/examples") + +# Emits /module_manifest-.txt ("target_name=path_to_binary" +# per line) for every executable module discovered above. scripts/run.sh / run.bat +# read this to resolve a CMake target name to its binary -- must run after +# discovery. +_write_module_manifest("${CMAKE_BINARY_DIR}") + +# Writes out/docs/index.html (DOCS_OUTPUT_DIR, see Configuration.cmake) +# linking to every module's generated documentation (a no-op if +# ENABLE_DOXYGEN is off) -- must run after discovery, same reasoning as +# the manifest above. +_write_docs_index("${DOCS_OUTPUT_DIR}") + +# ------------------------------------------------------- +# Copy compile_commands.json to the project root once it's fully +# populated, so clangd/clang-tidy/IWYU find it without extra config (most +# editors look for it at the repo root or search upward from the source +# file; CMake only ever writes it into the build directory). Runs at +# build time, by which point CMake's own generate step has already +# written build/compile_commands.json, so this works even on the very +# first build with no prior build directory. Vendored and fetched +# dependencies are excluded from it (see cmake/Dependencies.cmake) so it +# only reflects code this project owns. +# ------------------------------------------------------- +if(CMAKE_EXPORT_COMPILE_COMMANDS) + add_custom_target(copy_compile_commands ALL + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_BINARY_DIR}/compile_commands.json" + "${COMPILE_COMMANDS_DESTINATION}" + COMMENT "Copying compile_commands.json to project root" + VERBATIM + ) +endif() + +# ------------------------------------------------------- +# install +# ------------------------------------------------------- +set(_pmn_install_target_dir ${INSTALL_OUTPUT_DIR}/${DIST_TARGET}) + +# primary +set(RUNTIME_OUT_DIR ${_pmn_install_target_dir}/${RUNTIME_OUTPUT_SUBDIR}) +set(LIBRARY_OUT_DIR ${_pmn_install_target_dir}/${LIBRARY_OUTPUT_SUBDIR}) +set(ARCHIVE_OUT_DIR ${_pmn_install_target_dir}/${ARCHIVE_OUTPUT_SUBDIR}) + +# tests -- runtime/library -> bin/tests/ archives -> lib/tests/ +set(TEST_RUNTIME_OUT_DIR ${_pmn_install_target_dir}/${RUNTIME_OUTPUT_SUBDIR}/tests) +set(TEST_LIBRARY_OUT_DIR ${_pmn_install_target_dir}/${LIBRARY_OUTPUT_SUBDIR}/tests) +set(TEST_ARCHIVE_OUT_DIR ${_pmn_install_target_dir}/${ARCHIVE_OUTPUT_SUBDIR}/tests) + +# examples -- runtime/library -> bin/examples/ archives -> lib/examples/ +set(EXAMPLE_RUNTIME_OUT_DIR ${_pmn_install_target_dir}/${RUNTIME_OUTPUT_SUBDIR}/examples) +set(EXAMPLE_LIBRARY_OUT_DIR ${_pmn_install_target_dir}/${LIBRARY_OUTPUT_SUBDIR}/examples) +set(EXAMPLE_ARCHIVE_OUT_DIR ${_pmn_install_target_dir}/${ARCHIVE_OUTPUT_SUBDIR}/examples) + +# Every install(TARGETS ...) below is tagged COMPONENT ${PROJECT_CODE_NAME}. +# A vendored/fetched dependency's own CMakeLists.txt (see +# cmake/Dependencies.cmake) usually has install() rules of its own, with +# no COMPONENT tag -- installing with `cmake --install build` (no +# --component filter) would pull those in too, typically under +# CMAKE_INSTALL_PREFIX (e.g. /usr/local). scripts/install.sh / install.bat always +# pass --component ${PROJECT_CODE_NAME} so only this project's own +# artifacts are installed; use the same flag if you invoke cmake --install +# directly. +message(STATUS "Will install primary modules.") +get_property(_pmn_primary_targets GLOBAL PROPERTY PMN_MODULE_TARGETS_PRIMARY) +if(_pmn_primary_targets) + install(TARGETS ${_pmn_primary_targets} + COMPONENT ${PROJECT_CODE_NAME} + ARCHIVE DESTINATION ${ARCHIVE_OUT_DIR} + LIBRARY DESTINATION ${LIBRARY_OUT_DIR} + RUNTIME DESTINATION ${RUNTIME_OUT_DIR} + ) +endif() + +# MinGW SHARED targets (see the "MinGW runtime linking" comment above) -- +# installed as plain files, alongside the DLLs above, rather than as +# TARGETS, since they're not CMake targets this project defines. RUNTIME, +# not LIBRARY: a .dll is a runtime artifact on Windows no matter which +# install()-TARGETS category built it. +if(PMN_MINGW_RUNTIME_DLLS) + install(FILES ${PMN_MINGW_RUNTIME_DLLS} + COMPONENT ${PROJECT_CODE_NAME} + DESTINATION ${RUNTIME_OUT_DIR} + ) +endif() + +# Both blocks below read whatever test/example modules discovery actually +# found -- no target names to keep in sync here when a module is added, +# removed, or renamed. +if(INSTALL_TESTS) + get_property(_pmn_test_targets GLOBAL PROPERTY PMN_MODULE_TARGETS_TEST) + if(_pmn_test_targets) + message(STATUS "Will install test modules: ${_pmn_test_targets}") + install(TARGETS ${_pmn_test_targets} + COMPONENT ${PROJECT_CODE_NAME} + ARCHIVE DESTINATION ${TEST_ARCHIVE_OUT_DIR} + LIBRARY DESTINATION ${TEST_LIBRARY_OUT_DIR} + RUNTIME DESTINATION ${TEST_RUNTIME_OUT_DIR} + ) + else() + message(WARNING "INSTALL_TESTS is ON but no test modules were found under source/tests/.") + endif() +endif() + +if(INSTALL_EXAMPLES) + get_property(_pmn_example_targets GLOBAL PROPERTY PMN_MODULE_TARGETS_EXAMPLE) + if(_pmn_example_targets) + message(STATUS "Will install example modules: ${_pmn_example_targets}") + install(TARGETS ${_pmn_example_targets} + COMPONENT ${PROJECT_CODE_NAME} + ARCHIVE DESTINATION ${EXAMPLE_ARCHIVE_OUT_DIR} + LIBRARY DESTINATION ${EXAMPLE_LIBRARY_OUT_DIR} + RUNTIME DESTINATION ${EXAMPLE_RUNTIME_OUT_DIR} + ) + else() + message(WARNING "INSTALL_EXAMPLES is ON but no example modules were found under source/examples/.") + endif() +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..59c499e --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,166 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}" + }, + { + "name": "base-linux", + "hidden": true, + "inherits": "base", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" } + }, + { + "name": "base-macos", + "hidden": true, + "inherits": "base", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" } + }, + { + "name": "base-windows", + "hidden": true, + "inherits": "base", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" } + }, + { + "name": "base-mingw", + "hidden": true, + "inherits": "base", + "condition": { "type": "inList", "string": "${hostSystemName}", "list": ["Windows", "Linux"] } + }, + { + "name": "linux-x64-gcc", + "inherits": "base-linux", + "displayName": "Linux x64 (GCC)", + "description": "GCC for x64 Linux: native on x64 hardware; on an ARM64 machine a cross build with x86_64-linux-gnu-g++", + "toolchainFile": "${sourceDir}/cmake/toolchains/linux-x64-gcc.cmake" + }, + { + "name": "linux-x64-clang", + "inherits": "base-linux", + "displayName": "Linux x64 (Clang)", + "description": "Clang for x64 Linux: native on x64 hardware; on an ARM64 machine a cross build (clang++ --target=x86_64-linux-gnu)", + "toolchainFile": "${sourceDir}/cmake/toolchains/linux-x64-clang.cmake" + }, + { + "name": "linux-arm64-gcc", + "inherits": "base-linux", + "displayName": "Linux ARM64 (GCC)", + "description": "GCC for ARM64 (aarch64) Linux: native on ARM64 hardware, otherwise a cross build with aarch64-linux-gnu-g++ (Debian/Ubuntu: apt install g++-aarch64-linux-gnu; qemu-user, if installed, lets ctest run the result)", + "toolchainFile": "${sourceDir}/cmake/toolchains/linux-arm64-gcc.cmake" + }, + { + "name": "linux-arm64-clang", + "inherits": "base-linux", + "displayName": "Linux ARM64 (Clang)", + "description": "Clang for ARM64 (aarch64) Linux: native on ARM64 hardware, otherwise a cross build (clang++ --target=aarch64-linux-gnu; needs the aarch64 GCC cross package for its libraries, or -DCMAKE_SYSROOT)", + "toolchainFile": "${sourceDir}/cmake/toolchains/linux-arm64-clang.cmake" + }, + { + "name": "linux-arm32-gcc", + "inherits": "base-linux", + "displayName": "Linux ARM32 (GCC)", + "description": "GCC for 32-bit ARM (armhf) Linux: native on 32-bit ARM hardware, otherwise a cross build with arm-linux-gnueabihf-g++ (Debian/Ubuntu: apt install g++-arm-linux-gnueabihf). A 32-bit ARM userland on 64-bit ARM hardware should pass -DCMAKE_CXX_COMPILER=g++ -- see cmake/toolchains/_common.cmake", + "toolchainFile": "${sourceDir}/cmake/toolchains/linux-arm32-gcc.cmake" + }, + { + "name": "linux-arm32-clang", + "inherits": "base-linux", + "displayName": "Linux ARM32 (Clang)", + "description": "Clang for 32-bit ARM (armhf) Linux: native on 32-bit ARM hardware, otherwise a cross build (clang++ --target=arm-linux-gnueabihf) -- see linux-arm32-gcc", + "toolchainFile": "${sourceDir}/cmake/toolchains/linux-arm32-clang.cmake" + }, + { + "name": "macos-arm64-clang", + "inherits": "base-macos", + "displayName": "macOS ARM64 (Clang)", + "description": "Clang for Apple Silicon macOS -- Apple Clang from the Xcode Command Line Tools (xcode-select --install), or LLVM's own with -DCMAKE_CXX_COMPILER. The architecture is pinned with CMAKE_OSX_ARCHITECTURES, so this means arm64 even under a Rosetta-translated cmake; minimum macOS 11.0", + "toolchainFile": "${sourceDir}/cmake/toolchains/macos-arm64-clang.cmake" + }, + { + "name": "macos-x64-clang", + "inherits": "base-macos", + "displayName": "macOS x64 (Clang)", + "description": "Clang for Intel macOS -- Apple Clang from the Xcode Command Line Tools, or LLVM's own with -DCMAKE_CXX_COMPILER. Pinned to x86_64 with CMAKE_OSX_ARCHITECTURES, so on an Apple Silicon Mac it builds x64 code that runs through Rosetta 2; minimum macOS 11.0", + "toolchainFile": "${sourceDir}/cmake/toolchains/macos-x64-clang.cmake" + }, + { + "name": "windows-x64-msvc", + "inherits": "base-windows", + "displayName": "Windows x64 (MSVC)", + "description": "cl.exe for x64: run from an x64 Native Tools Command Prompt for VS -- cl.exe's architecture comes from that developer environment, not from CMake", + "toolchainFile": "${sourceDir}/cmake/toolchains/windows-x64-msvc.cmake" + }, + { + "name": "windows-arm64-msvc", + "inherits": "base-windows", + "displayName": "Windows ARM64 (MSVC)", + "description": "cl.exe for ARM64: run from a Visual Studio Developer Prompt configured for ARM64 (an ARM64 Native Tools prompt, or x64_arm64 via vcvarsall.bat to cross-build from an x64 host) -- cl.exe's architecture comes from that environment, not from CMake", + "toolchainFile": "${sourceDir}/cmake/toolchains/windows-arm64-msvc.cmake" + }, + { + "name": "windows-x64-clang-cl", + "inherits": "base-windows", + "displayName": "Windows x64 (clang-cl)", + "description": "clang-cl (Clang with the MSVC ABI), pinned to --target=x86_64-pc-windows-msvc so it means x64 whatever the machine's own architecture is", + "toolchainFile": "${sourceDir}/cmake/toolchains/windows-x64-clang-cl.cmake" + }, + { + "name": "windows-arm64-clang-cl", + "inherits": "base-windows", + "displayName": "Windows ARM64 (clang-cl)", + "description": "clang-cl (Clang with the MSVC ABI), pinned to --target=aarch64-pc-windows-msvc so it means ARM64 whatever the machine's own architecture is", + "toolchainFile": "${sourceDir}/cmake/toolchains/windows-arm64-clang-cl.cmake" + }, + { + "name": "windows-x64-clang", + "inherits": "base-windows", + "displayName": "Windows x64 (Clang, GNU-style)", + "description": "Clang with its own GNU-style driver, e.g. from an MSYS2 CLANG64 shell -- assumes that environment's clang++ is what's on PATH, distinct from clang-cl above", + "toolchainFile": "${sourceDir}/cmake/toolchains/windows-x64-clang.cmake" + }, + { + "name": "windows-arm64-clang", + "inherits": "base-windows", + "displayName": "Windows ARM64 (Clang, GNU-style)", + "description": "Clang with its own GNU-style driver, e.g. from an MSYS2 CLANGARM64 shell -- assumes that environment's clang++ is what's on PATH", + "toolchainFile": "${sourceDir}/cmake/toolchains/windows-arm64-clang.cmake" + }, + { + "name": "windows-x86-mingw32", + "inherits": "base-mingw", + "displayName": "Windows x86 (MinGW32)", + "description": "32-bit MinGW g++: i686-w64-mingw32-g++ from an MSYS2 MINGW32 shell on Windows, or a cross build from Linux (apt install g++-mingw-w64-i686; wine, if installed, lets ctest run the result)", + "toolchainFile": "${sourceDir}/cmake/toolchains/windows-x86-mingw32.cmake" + }, + { + "name": "windows-x64-mingw64", + "inherits": "base-mingw", + "displayName": "Windows x64 (MinGW-w64)", + "description": "64-bit MinGW-w64 g++: x86_64-w64-mingw32-g++ from an MSYS2 MINGW64 shell on Windows, or a cross build from Linux (apt install g++-mingw-w64-x86-64; wine, if installed, lets ctest run the result)", + "toolchainFile": "${sourceDir}/cmake/toolchains/windows-x64-mingw64.cmake" + } + ], + "buildPresets": [ + { "name": "linux-x64-gcc", "configurePreset": "linux-x64-gcc" }, + { "name": "linux-x64-clang", "configurePreset": "linux-x64-clang" }, + { "name": "linux-arm64-gcc", "configurePreset": "linux-arm64-gcc" }, + { "name": "linux-arm64-clang", "configurePreset": "linux-arm64-clang" }, + { "name": "linux-arm32-gcc", "configurePreset": "linux-arm32-gcc" }, + { "name": "linux-arm32-clang", "configurePreset": "linux-arm32-clang" }, + { "name": "macos-arm64-clang", "configurePreset": "macos-arm64-clang" }, + { "name": "macos-x64-clang", "configurePreset": "macos-x64-clang" }, + { "name": "windows-x64-msvc", "configurePreset": "windows-x64-msvc" }, + { "name": "windows-arm64-msvc", "configurePreset": "windows-arm64-msvc" }, + { "name": "windows-x64-clang-cl", "configurePreset": "windows-x64-clang-cl" }, + { "name": "windows-arm64-clang-cl", "configurePreset": "windows-arm64-clang-cl" }, + { "name": "windows-x64-clang", "configurePreset": "windows-x64-clang" }, + { "name": "windows-arm64-clang", "configurePreset": "windows-arm64-clang" }, + { "name": "windows-x86-mingw32", "configurePreset": "windows-x86-mingw32" }, + { "name": "windows-x64-mingw64", "configurePreset": "windows-x64-mingw64" } + ] +} diff --git a/Configuration.cmake b/Configuration.cmake new file mode 100644 index 0000000..7d77d66 --- /dev/null +++ b/Configuration.cmake @@ -0,0 +1,861 @@ +# ============================================================================= +# 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() diff --git a/README.md b/README.md new file mode 100644 index 0000000..984bc1b --- /dev/null +++ b/README.md @@ -0,0 +1,825 @@ +# CMakeTemplate — a generator-friendly CMake module system + +A small template project (one shared library, two executables, one +vendored dependency, one fetched dependency) that demonstrates a CMake +setup where **adding, removing, or renaming a module never requires +editing the root `CMakeLists.txt`**, output paths and project identity +come from exactly one file, and Linux, macOS, Windows/MSVC, clang-cl, +and MinGW are all first-class targets -- each with its own toolchain +file and preset, built in its own directory -- across x64, ARM64, and +(on Linux) 32-bit ARM. + +Copy the structure, delete the demo code, keep the machinery. + +## Quick start + +```bash +scripts/setup.sh # once: offer to install missing tools, fetch pinned Ninja + Doxygen, install the git hook +scripts/build.sh # configure (if needed) + build, Release by default +scripts/run.sh hello_exe World +``` + +```bat +scripts\setup.bat +scripts\build.bat +scripts\run.bat hello_exe World +``` + +`setup` starts by checking for the tools a build needs -- Git, CMake 3.25+ +and a C++ compiler, plus optional extras like Clang and clang-format -- +and asks, **one tool at a time**, whether to install each missing one, +showing the exact command first. Nothing is installed without your yes; +see [scripts/setup.sh / setup.bat](#scriptssetupsh--setupbat). + +``` +Hello, World! (from demo_core) +``` + +Pick a target explicitly with a preset, and switch freely -- every +preset builds in its own directory, so none of this ever invalidates +another build: + +```bash +scripts/build.sh --list-presets # what this OS offers +scripts/build.sh --preset linux-x64-gcc +scripts/build.sh --preset linux-x64-clang # leaves the GCC build alone +scripts/run.sh hello_exe World # runs whichever you built last +scripts/run.sh --preset linux-x64-gcc hello_exe World +``` + +## What you get + +- **One source of truth.** `Configuration.cmake` owns this project's + identity, target selection, and every output path. Nothing else — + not a `.cmake` module, not a shell script, not `.gitignore` — + hardcodes a second copy of a value it defines. See + [Configuration.cmake: the source of truth](#configurationcmake-the-source-of-truth). +- **Sixteen targets, one toolchain file each.** GCC and Clang on Linux, + each on x64, ARM64, and 32-bit ARM; Clang on macOS, on Apple Silicon + and Intel; MSVC, clang-cl, and GNU-style Clang on Windows, each on x64 + and ARM64, plus both MinGW flavors (x86/x64) — see + [Supported targets](#supported-targets-dist_target). + Each has a toolchain file in `cmake/toolchains/` that picks the + compiler and declares the target, and where the target isn't the + machine you're on it sets up a real cross build: the ARM Linux targets + and both MinGW ones cross-compile from a Linux machine — see + [Toolchain files and cross-compiling](#toolchain-files-and-cross-compiling). +- **Presets that never step on each other.** Every preset builds in its + own `out/build//`, so switching between GCC and Clang, or + between two architectures, never invalidates — or deletes — the + other's build. `scripts/run`, `install` and `docs` follow whichever + you built last, or take `--preset`. +- **Zero-edit module discovery.** Drop a directory with a one-line + `CMakeLists.txt` under `source/`, `source/tests/`, or + `source/examples/`; it's built, linked, installed, tested, and + documented without touching anything else. See + [Adding a module](#adding-a-module). +- **Self-managing tooling.** `scripts/setup.sh`/`setup.bat` fetch a + pinned Ninja and Doxygen (so every machine builds and documents + identically), wire up a pre-commit hook, and offer — one tool at a + time, with your say-so for each — to install whatever else is missing + (CMake, Git, a compiler, Clang, MinGW, ...) through your package + manager: apt, dnf, pacman, Homebrew or winget. +- **A build script that takes real arguments.** Pick a preset (which + selects the right toolchain), a configuration, and forward + arbitrary `-D...` flags to CMake — safely, on both bash and + `cmd.exe` — see [scripts/build.sh / build.bat](#scriptsbuildsh--buildbat). + +## Project layout + +``` +Configuration.cmake Source of truth: identity, DIST_TARGET, output paths +CMakeLists.txt Root build: options, compiler settings, module + discovery, install rules -- rarely needs editing +CMakePresets.json One preset per supported DIST_TARGET, each + pointing at its toolchain file and its own + build directory -- for IDEs and + scripts/build.sh|.bat --preset +StyleGuide.md The C++ style guide .clang-format/.clang-tidy enforce + +cmake/ + BuildHelpers.cmake add_lib_module / add_exe_module / add_test_module / + add_example_module, and module discovery + Dependencies.cmake add_vendor_header_only / add_vendor_subdirectory / + add_dependency + Documentation.cmake Per-module Doxygen wiring + PrintConfig.cmake Lets scripts/*.sh|*.bat read Configuration.cmake + BuildDir.cmake Tells scripts/*.sh|*.bat which build directory + (preset) to work on -- see "Scripts" + HostArch.cmake x64/arm64/arm32 labels from a processor string + toolchains/ One toolchain file per DIST_TARGET, plus + _common.cmake with what they share + FetchNinja.cmake, NinjaPin.cmake Pinned Ninja download + FetchDoxygen.cmake, DoxygenPin.cmake Pinned Doxygen download + +scripts/ setup, build, run, install, docs, refactor + -- .sh and .bat, one-to-one. _common.sh holds + what the .sh ones share; _tools.sh (Linux/macOS) + and _tools.ps1 (Windows) are the interactive tool + installers setup runs first + +source/ + Core/ core_lib -- a SHARED library module + HelloApp/, FarewellApp/ Executable modules depending on core_lib + tests/, examples/ Empty by default -- see Adding a module + +vendor/ + tiny_case/ A vendored dependency with its own CMakeLists.txt + tiny_ansi/ A vendored header-only dependency + +c-api/.clang-tidy Stricter naming rules for a C-API subtree, if you add one +.githooks/pre-commit clang-format/clang-tidy check, installed by scripts/setup +``` + +## Configuration.cmake: the source of truth + +Every value that used to (or could) drift into a second, hand-kept-in-sync +copy lives in `Configuration.cmake`: + +- **Identity** — `PROJECT_CODE_NAME` (the `cmake --install --component` + name) and `DEFAULT_BUILD_TYPE`. +- **Target selection** — `DIST_TARGET`, and the `PLATFORM` / + `ARCHITECTURE` / `COMPILER_ID` metadata derived from it. +- **Output paths** — `OUT_DIR`, `BUILD_OUTPUT_DIR`, `INSTALL_OUTPUT_DIR`, + `DOCS_OUTPUT_DIR`, `TOOLCACHE_DIR`, and the `bin`/`lib` subdirectory + names. + +Retargeting this template for your own project, or changing where +things land, means editing this one file — or passing `-D=...` at +configure time — never hunting through `CMakeLists.txt`, a `cmake/*.cmake` +module, or a script for a hardcoded duplicate. + +That includes the shell scripts, which run *before* any CMake configure +exists to ask a question of. `cmake/PrintConfig.cmake` is a tiny +`cmake -P` script that loads `Configuration.cmake` and prints one value; +every script that needs one calls it instead of hardcoding a default. +(`cmake/BuildDir.cmake` is its sibling for the one question that isn't a +single value: *which* build directory a script should work on.) +This is exactly the bug it replaces: `install.sh`/`install.bat` used to +each hardcode their own literal `"demo"`, with a comment warning it +*"must match `PROJECT_CODE_NAME` in `CMakeLists.txt`"* — the kind of +comment that's really begging for the two to eventually disagree. They +don't hardcode it anymore; they ask `Configuration.cmake`. + +The one thing this deliberately can't do: a bare `cmake -P` script never +runs `project()`, so it never gets real compiler detection. Anything +in `Configuration.cmake` that depends on the compiler — `DIST_TARGET` +and everything derived from it — is skipped in this "query mode" and +fails loudly if a script asks for it anyway, rather than guessing. No +script currently needs to; `cmake --install` reads what a real configure +already baked into the build directory, not these variables directly. + +## Supported targets (`DIST_TARGET`) + +`DIST_TARGET` is a single `--` string that +picks the toolchain metadata (`PLATFORM`, `ARCHITECTURE`, `COMPILER_ID`) +used throughout the build and as the `cmake --install` destination +subdirectory (`out/install//...`). Each value below has a +[toolchain file](#toolchain-files-and-cross-compiling) of the same name, +`cmake/toolchains/.cmake`, which declares it — and which +every preset selects. Without one (a bare `cmake -S . -B build`) it's +auto-detected from whatever compiler CMake finds: + +| `DIST_TARGET` | Compiler | +|--------------------------|---------------------------------------------------------| +| `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 or LLVM's) | +| `macos-x64-clang` | Clang on Intel macOS (or Apple Silicon, via Rosetta 2) | +| `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) | +| `windows-arm64-clang-cl` | clang-cl targeting ARM64 | +| `windows-x64-clang` | Clang, own GNU-style driver, x64 (e.g. MSYS2 CLANG64) | +| `windows-arm64-clang` | Clang, own GNU-style driver, ARM64 (e.g. MSYS2 CLANGARM64) | +| `windows-x86-mingw32` | 32-bit GCC via MinGW | +| `windows-x64-mingw64` | 64-bit GCC via MinGW-w64 | + +No macOS GCC target and no universal one: Apple's own toolchain *is* +Clang, and a universal binary is two architectures at once, which a +`DIST_TARGET` (one architecture per name) can't express — see +[macOS](#macos) for how to get a universal file anyway. + +No `windows-arm32-*` and no ARM64 MinGW target: 32-bit ARM Windows has +no real toolchain support left in any of the compilers above, 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. + +### Toolchain files and cross-compiling + +`cmake/toolchains/` has one toolchain file per target above, named after +it, and every preset in `CMakePresets.json` selects the one with its own +name. CMake reads a toolchain file *before* it detects anything, so the +compiler and target are settled first instead of being discovered and +then cross-checked after the fact. Each file: + +1. **declares its `DIST_TARGET`**, so the label and the compiler travel + together and can't be mismatched; +2. **picks the compiler** — and, for clang-cl, pins `--target=` (so + `windows-x64-clang-cl` means x64 on an ARM64 machine too); +3. **where the target isn't the machine running CMake, sets up a real + cross build**: `CMAKE_SYSTEM_NAME`/`CMAKE_SYSTEM_PROCESSOR`, package + and library lookup restricted to the target's sysroot (so a host + library is never linked into a foreign-architecture binary — fmt is + fetched and built *for the target* instead), and, if one is + installed, an emulator (`qemu-user` or `wine`) registered as + `CMAKE_CROSSCOMPILING_EMULATOR`, which is what lets `ctest` and + `scripts/run.sh` run what was just built. + +| Target family | Host | What the toolchain does | +|---|---|---| +| `linux-*` | matching hardware (ARM64 for `linux-arm64-*`, …) | native build with the ordinary `g++` / `clang++` | +| `linux-*` | any other Linux machine | cross build: `-g++` (`apt install g++-aarch64-linux-gnu`, `g++-arm-linux-gnueabihf`), or `clang++ --target=` plus that same package for the target's libraries (or `-DCMAKE_SYSROOT=...`); `qemu-user` runs the result | +| `macos-*-clang` | any Mac | not a cross build in the Linux sense: Apple's clang is one multi-architecture compiler, so the architecture is just an `-arch` flag — pinned with `CMAKE_OSX_ARCHITECTURES`, along with a minimum macOS of 11.0. See [macOS](#macos) | +| `windows-*-mingw*` | Windows (MSYS2, MinGW-w64) | native build with `-g++` | +| `windows-*-mingw*` | Linux | cross build with mingw-w64 (`apt install g++-mingw-w64-x86-64`, `g++-mingw-w64-i686`); `wine` runs the result | +| `windows-*-msvc` | Windows | picks `cl` and fails early, with the fix, if it isn't on `PATH`. **cl.exe's architecture comes from the Visual Studio developer prompt, not from CMake** — no toolchain file can switch it — so the architecture check below verifies you opened the right one | +| `windows-*-clang-cl` | Windows | picks `clang-cl` and pins its `--target` | +| `windows-*-clang` | Windows | picks `clang++`; the MSYS2 CLANG64 / CLANGARM64 shell is what sets its target | + +Windows executables are only built for Windows hosts, except MinGW, +which also cross-builds from Linux; there is no cross-compiling *to* +MSVC or clang-cl from Linux, and no cross-compiling to or from macOS +beyond the two architectures the Mac's own compiler can already emit. + +Two rules about overriding a toolchain, both deliberate: an explicit +`-DCMAKE_CXX_COMPILER=...` beats the toolchain's compiler (the +architecture and label checks below then catch an override that no longer +matches the target), while a stray `CXX` environment variable does *not* +— a target called `linux-x64-gcc` shouldn't be built with whatever +`CXX=clang++` happens to be exported. And a 32-bit ARM userland on 64-bit +ARM hardware reports `aarch64` from `uname -m`, so `linux-arm32-*` would +try to cross-compile there; pass `-DCMAKE_CXX_COMPILER=g++` in that one +case (the build is labeled `arm32`, correctly). +[`cmake/toolchains/_common.cmake`](cmake/toolchains/_common.cmake) +documents everything each helper does. + +### macOS + +`macos-arm64-clang` and `macos-x64-clang` build with Clang: Apple Clang +from the Xcode Command Line Tools (`xcode-select --install`, which +`scripts/setup.sh` offers to run), or LLVM's own Clang if you point +`-DCMAKE_CXX_COMPILER` at it (Homebrew's: `$(brew --prefix llvm)/bin/clang++`). +Both count as "clang" here. What is specific to the Mac: + +- **The architecture is pinned, not inherited.** Apple's clang is a single + multi-architecture compiler, so building x86_64 on an Apple Silicon Mac + (or the reverse) is an `-arch` flag, not a cross compiler — no sysroot, + no emulator, `CMAKE_CROSSCOMPILING` stays off. The toolchain sets + `CMAKE_OSX_ARCHITECTURES` explicitly even when it equals the machine's, + because an x86_64 `cmake` running under Rosetta 2 on an Apple Silicon Mac + would otherwise quietly build x86_64 for a target named `arm64`. The + x64 build then runs on an Apple Silicon Mac through Rosetta 2, if + installed; the reverse (arm64 on Intel) can't run. +- **Minimum macOS is 11.0** (the first release for Apple Silicon), set with + `CMAKE_OSX_DEPLOYMENT_TARGET`. Unset, it would default to the SDK's own + version and everything you build would refuse to start on any older + macOS than yours. Pass `-DCMAKE_OSX_DEPLOYMENT_TARGET=...` to change it. + Bear in mind Apple's libc++ marks some newer C++ library features + (`std::format`, floating-point `std::to_chars`, `std::pmr`, ...) as + unavailable below the macOS that introduced them; this project's fmt + dependency doesn't use any. +- **Libraries load through `@rpath`.** Every RPATH in the build is written + `$ORIGIN` (the Linux spelling) and translated to `@loader_path` for + Mach-O in one place, `_apply_rpath` in `cmake/BuildHelpers.cmake`; + shared libraries get `@rpath/...` install names (CMake's default here). + The layout is the same as everywhere else: everything flat in `bin/`. +- **Universal binaries** aren't a target. Build both presets and combine + the results: `lipo -create out/install/macos-arm64-clang/bin/hello + out/install/macos-x64-clang/bin/hello -output hello`. +- **The shell is bash 3.2.** macOS still ships it as `/bin/bash`, so every + `.sh` script here is written for it (no `mapfile`, no bash-4 expansions, + empty arrays guarded for `set -u`) and BSD `sed`/`mktemp`. +- **Pinned Ninja and Doxygen work on macOS**: Ninja's universal binary + (one asset for both chips) and Doxygen's `mac-arm` / `mac-intel` zips, + chosen by the Mac's real architecture, Rosetta included. +- **Homebrew's llvm is keg-only** (installed but not on `PATH`), and it is + where `clang-tidy` comes from; `scripts/refactor.sh` and the pre-commit + hook look there after the normal `PATH`. + +How this was verified, so you know what to trust: everything above was +run for real except the Apple toolchain itself. The macOS toolchain, +`Configuration.cmake`'s detection and the RPATH translation were exercised +by cross-compiling to Mach-O from Linux (with zig's clang as the compiler, +`CMAKE_SYSTEM_NAME=Darwin`) and reading the resulting load commands +(`LC_RPATH @loader_path`, `@rpath/libdemo_core.dylib`, arm64 and x86_64); the +scripts were run under a real bash 3.2 build; the pinned downloads were +fetched and hash-verified; the installer flows ran against a fake Mac. What +no Mac has run yet is Apple's own clang and SDK — if something is off +there, that is the place to look first. + +### Architecture detection + +Architecture is detected differently depending on compiler family, +because `CMAKE_SYSTEM_PROCESSOR` isn't trustworthy for all of them: +Linux (GCC/Clang) reads it through `cmake/HostArch.cmake` — `uname -m` +for a native build, and whatever the toolchain file set it to for a +cross build (so an aarch64 cross build from an x64 machine is labeled +`arm64`, not `x64`) — while macOS reads `CMAKE_OSX_ARCHITECTURES`, which +its toolchain files always set (see [macOS](#macos)), and Clang's +GNU-style driver on Windows uses a +direct environment-variable check instead of `CMAKE_SYSTEM_PROCESSOR`/ +`CMAKE_HOST_SYSTEM_PROCESSOR`: those report the architecture of the +`cmake.exe` *process*, so a 32-bit `cmake.exe` running under WoW64 on an +ordinary 64-bit machine reports `x86` there, machine be damned. MSVC and +clang-cl have the equivalent problem one level down (the compiler, not +cmake.exe, is what can be cross-targeting) and use +`CMAKE_CXX_COMPILER_ARCHITECTURE_ID` instead — those two routinely +differ for a same-machine cross build (an x64 Visual Studio install's +"HostX64/ARM64" tools are the normal way to target ARM64 from an x64 dev +machine), and CMake tracks the resulting mismatch as a known issue +([#25466](https://gitlab.kitware.com/cmake/cmake/-/issues/25466)). + +Auto-detection tells clang-cl apart from GNU-style Clang by +`CMAKE_CXX_COMPILER_FRONTEND_VARIANT` (`MSVC` vs. `GNU` — both report +`Clang` as the compiler ID), and mingw32 apart from mingw64 by pointer +size, not by parsing the compiler's own executable name. An +unrecognized compiler *or* architecture is a configure-time +`FATAL_ERROR` naming every valid value above, not a silent guess — +auto-detection guessing "must be MSVC" for anything it doesn't +recognize is exactly what used to misidentify a MinGW build as MSVC and +install it to the wrong directory. + +**Two Windows gotchas worth knowing before you hit them:** +- *"targeting X86 (32-bit)" on a 64-bit machine.* `cl.exe`/`clang-cl` + found via `PATH` is the 32-bit-hosted, 32-bit-targeting one — usually + from opening a plain shell instead of an "x64 Native Tools Command + Prompt for VS" (or a Developer PowerShell defaulted to x64). The error + says this; open the right prompt, delete that preset's build directory + (`out/build/`), and reconfigure. +- *"No pinned ninja/Doxygen binary is configured for this host platform" + on an ordinary 64-bit machine.* Almost always a 32-bit `cmake.exe` + (an entirely normal, common download) running under WoW64 — see + `cmake/HostArch.cmake`'s header for why that fools a naive host- + architecture check. Either install a 64-bit CMake, or just install + ninja yourself and put it on `PATH` — `scripts/build.sh`/`build.bat` + fall back to a system ninja either way (see below). + +**A bare `-DDIST_TARGET=` labels the build; it doesn't select a +compiler.** CMake still uses whatever `CMAKE_CXX_COMPILER` it would +otherwise find — your `PATH`, your environment, or an explicit +`-DCMAKE_CXX_COMPILER=...`. 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 would silently mislabel the result as +MinGW. It doesn't, though: an explicit `DIST_TARGET` that disagrees with +what the real compiler auto-detects to is a configure-time +`FATAL_ERROR`, naming both what you asked for and what CMake actually +found, plus the toolchain file and preset that fix it. The same check +also catches a toolchain whose compiler was overridden afterwards (an +explicit `-DCMAKE_CXX_COMPILER=...`, or a stale cache), and a +`-DDIST_TARGET` that contradicts the toolchain's own target. + +The simplest way to avoid hitting that in the first place is a +`CMakePresets.json` preset, which selects the target's toolchain file — +compiler and `DIST_TARGET` together — for exactly this reason. +[`scripts/build.sh`/`build.bat`](#scriptsbuildsh--buildbat) take a +preset name directly: + +```bash +scripts/build.sh --list-presets # see every preset for your OS +scripts/build.sh --preset windows-x64-mingw64 +``` + +or use `cmake` directly — open the project in an IDE with CMake preset +support (CLion, VS Code, Visual Studio) and pick one from the target +dropdown, or: + +```bash +cmake --list-presets +cmake --preset windows-x64-mingw64 # configure +cmake --build --preset windows-x64-mingw64 # build +``` + +Selecting a target without a preset still works — hand CMake the same +toolchain file a preset would, e.g. +`-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/windows-x64-mingw64.cmake` — or +run from an environment (an MSYS2 MINGW64 shell, a Visual Studio +Developer Prompt) where the right compiler is already the default and +auto-detection alone is enough. Either way, the compiler itself has to +already be installed and on `PATH` — nothing here installs MinGW, +clang-cl, or MSVC for you (a toolchain file that can't find its compiler +says which package or prompt provides it). What doesn't work: giving +*both* `--preset ` and an explicit `-DDIST_TARGET=...` to +`scripts/build.sh`/`build.bat` at the same time — a preset's toolchain +already declares `DIST_TARGET`, so a second value alongside it is always +either redundant or contradictory, and both scripts reject the +combination up front with a one-line fix, rather than leaving it to +whichever of the two CMake happens to prefer. + +### Used as a dependency of another project + +Short answer: the root (outer) project always decides, automatically — +nothing extra to configure. `CMakePresets.json` presets are a +command-line concept tied to whichever directory you point `cmake -S`/ +`--preset` at; they're never implicitly consulted for a directory pulled +in as a dependency (`add_subdirectory()`, `FetchContent`) unless the +outer project's own `CMakePresets.json` explicitly `"include"`s this +one, which is the outer project's choice to make, not this template's. +So: + +- If the root project sets `DIST_TARGET` itself (its own preset, a + plain `-DDIST_TARGET=...`, or one of this template's toolchain files, + which declare it), that is visible everywhere in the build — including inside this template + once it's a subdirectory — so this file's `if(NOT DIST_TARGET)` check + simply sees it already set and skips straight to cross-checking it + against the real compiler, exactly like a top-level build. +- If the root project does nothing, `DIST_TARGET` here still + auto-detects normally, from whatever compiler the whole build is + using (there's only one `CMAKE_CXX_COMPILER` per build, shared across + every subdirectory) — it just does so as a plain, directory-scoped + variable rather than a cache one, so it never leaks back up into the + root project's own variables. + +One related thing this template does *not* handle when nested, since +it's out of scope for a project meant to be built standalone: `OUT_DIR` +and everything under it are defined from `CMAKE_SOURCE_DIR`, which is +always the *outermost* project's source directory, not this one's — so +if you vendor this template into a larger project, its `out/`, +`.gitignore` sync, etc. would target the parent's root, not its own +subdirectory. Retarget `OUT_DIR` (and friends) explicitly if you need +this template's output layout to stay self-contained in that scenario. + +**MinGW runtime note:** executables built for `windows-x86-mingw32` / +`windows-x64-mingw64` statically link `libgcc`/`libstdc++`/`libwinpthread` +so they run standalone. `core_lib` (a `SHARED` target) can't do the +same — it would collide with the executable's own statically-linked +copy of the C++ unwinder (`multiple definition of _Unwind_Resume`) — so +it keeps the default dynamic runtime instead, and the build copies the +three runtime DLLs next to it automatically (both in the build tree and +at install time). You won't notice this unless you go looking for those +DLLs next to `libdemo_core.dll`; it's mentioned here in case you do. + +## Scripts + +Every script has a `.sh` and a `.bat` twin with identical behavior, +prints its own usage in a header comment, and supports `-h`/`--help` +for that same usage at the command line. All of them assume the +project root as the working directory but `cd` there themselves, so run +them from anywhere inside the repo. + +### `scripts/setup.sh` / `setup.bat` + +Run once per clone, and again whenever you like: it is safe to re-run. In +order: + +1. **Checks the tools a build needs and offers to install what's missing — + one tool at a time.** For each missing tool it says what the tool is + for, shows the exact command it would run, and waits for you: + + ``` + CMake 3.25 or newer -- configures and drives every build + Install CMake 3.25 or newer? [Y/n/a/q] y + $ sudo apt-get install -y cmake + ``` + + `y` installs it, `n` skips this one, `a` installs everything still to + come, `s` skips the rest of the *optional* tools, `q` stops asking + (Enter takes the default: yes for a required tool, no for an optional + one). Nothing is installed without your yes. What it knows about: + + | | Required | Optional | + |---|---|---| + | **Linux** | Git, CMake 3.25+, a C++ compiler (GCC) | Clang, clang-format, clang-tidy; on apt systems also the ARM cross compilers, MinGW-w64, `qemu-user` and `wine` for the cross builds | + | **macOS** | Xcode Command Line Tools, CMake 3.25+ (Homebrew is installed first if it's needed and missing) | clang-format, clang-tidy | + | **Windows** | Git, CMake 3.25+, and a compiler: Visual Studio Build Tools *or* MinGW-w64 via MSYS2 | LLVM (clang-cl, clang-format, clang-tidy), the other MSYS2 toolchains | + + It uses your platform's package manager — `apt`, `dnf` or `pacman` on + Linux, Homebrew on macOS, `winget` on Windows — and tells you what to + install by hand if it doesn't recognize yours (cross-compilers and + emulators are offered on apt systems only, the layout the toolchain + files were written and tested against). A few specifics worth knowing: + an old distro CMake (Ubuntu 22.04 ships 3.22) is detected and the + alternatives — `pipx`, `snap`, the cmake.org tarball — are listed rather + than silently accepted; on macOS a `xcode-select --install` dialog is + waited for, and Homebrew is fetched with its own documented installer; + on Windows, `winget` doesn't put CMake or LLVM on `PATH`, so setup adds + their folders to this session (so the remaining steps work) and asks + whether to add them to your user `PATH` for good, and a fresh MSYS2 is + brought up to date — twice, as MSYS2 recommends — before MinGW-w64 goes + into it. + + Flags: `--yes` installs the *required* tools without asking (for + scripts and CI), `--all` also the optional ones, `--check` only reports + and exits 1 if something required is missing, `--skip-tools` skips this + step. Without a terminal and without `--yes`/`--all` it asks nothing and + installs nothing, so a CI job can never hang on a prompt. +2. **Fetches a pinned Ninja and Doxygen** into `.cache/tools/` (via + `cmake/FetchNinja.cmake` / `FetchDoxygen.cmake` — skips anything + already fetched). Where upstream ships no binary for your machine + (Doxygen on ARM Linux/Windows; Ninja on 32-bit ARM Linux) or the + download fails, setup says so and carries on — it offers to install a + Ninja through your package manager instead, and the project builds fine + without Doxygen. +3. **Activates the pre-commit hook**: points `git config core.hooksPath` + at `.githooks/`, so the check below is active from your very first + commit (optional — the hook warns, rather than blocks, if + `clang-format`/`clang-tidy` aren't installed). + +The Linux/macOS side lives in `scripts/_tools.sh` (a table of tools and +their package names per package manager, plus the prompting); on Windows +`setup.bat` hands the same job to `scripts/_tools.ps1`, because prompts, +`PATH` handling and `winget` are far more robust in PowerShell than in +batch. Both are internal helpers, not entry points. + +### `scripts/build.sh` / `build.bat` + +``` +scripts/build.sh [--preset ] [BUILD_TYPE] [CMAKE_ARGS...] +scripts/build.sh --list-presets +scripts/build.sh --help +``` + +`--preset ` configures with a `CMakePresets.json` preset instead +of auto-detection — the preset selects that target's toolchain file, +which picks the compiler and declares `DIST_TARGET` together, so you +don't have to pass both by hand (see +[Supported targets](#supported-targets-dist_target) above for why that +pairing matters). `--list-presets` prints every preset for your OS and +exits. `--preset=` also works; whichever form is used must come +first if given at all. + +**One build directory per preset.** `--preset ` configures into +`out/build//`; without a preset it's `out/build/default/` (the +compiler is auto-detected there, so keep to one compiler per directory — +or use presets). CMake keeps a compiler in its cache, so a directory can +only ever belong to one compiler; giving each preset its own is what lets +you flip between GCC and Clang, or between x64 and a cross-built ARM64 +target, with each build staying up to date, and it's what plain +`cmake --preset` and IDEs get too (each preset's `binaryDir` is +`out/build/${presetName}`). Earlier versions of this script kept a single +`out/build/` and deleted it whenever the preset changed — if you're +upgrading, `out/build/` may still hold that old build's files; delete it +once (`scripts/run` and friends say so when that's all they find). The +install directory follows the same naming: `out/build/` builds +into `out/install/`. + +The build *type* is not part of the directory name, so flipping one +preset between `Debug` and `Release` reconfigures that preset's directory +(a full rebuild of it), as before. If you flip often, derive a preset — +in `CMakeUserPresets.json`, `{"name": "gcc-debug", "inherits": +"linux-x64-gcc", "cacheVariables": {"CMAKE_BUILD_TYPE": "Debug"}}` — which +inherits the toolchain and gets its own `out/build/gcc-debug/`. Set +`BUILD_DIR=` to use an exact directory of your own instead. + +Three things are rejected up front, before `cmake` even runs, each with +that same preset list shown right there instead of a bare error: +`--preset` with no value after it; `--preset` followed by something +that's clearly another flag rather than a name; and — the ambiguity +case — `--preset ` combined with an explicit `-DDIST_TARGET=...` +later in `CMAKE_ARGS`, since the preset's toolchain already declares +`DIST_TARGET` and a second value alongside it is never useful. +`build.sh` additionally checks the preset *name* itself against +`--list-presets` before configuring; `build.bat` instead lets `cmake` +itself catch an unrecognized name and shows the preset list as a +follow-up to that failure — different mechanism, same result, a +deliberate choice given how much less predictable pre-validating a +quoted string is in `cmd.exe` (see the `=` trap below for a taste of +that general problem). + +`BUILD_TYPE` (`Debug`/`Release`/`RelWithDebInfo`/`MinSizeRel`) is +optional and defaults to `Configuration.cmake`'s `DEFAULT_BUILD_TYPE`. +Anything else is forwarded to `cmake` verbatim at configure time: + +```bash +scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON +scripts/build.sh -DBUILD_TESTS=ON # BUILD_TYPE omitted +``` + +An argument is treated as `BUILD_TYPE` if it doesn't start with `-`; +otherwise there's no `BUILD_TYPE` override and every argument is a +CMake argument. + +Prefers a pinned Ninja (from `setup.sh`) over a system one, and falls +back to CMake's platform default generator with a warning — never an +error — if neither is available; run `setup.sh` for the faster, +version-pinned option. (A preset already fixes the generator to Ninja, +so with `--preset` this only controls *which* `ninja` gets used, pinned +or system, not whether one is used at all — if neither exists, the +warning says so and the preset's own configure step is left to fail on +its own.) + +**The `cmd.exe` `=` trap, and why `build.bat` avoids it:** `cmd.exe` +splits a batch file's own `%1`/`%2`/... parameters (and `shift`) on +space, comma, semicolon, *and* `=` — so a naively-written script reading +`-DBUILD_TESTS=ON` through `%1` sees it as two parameters, `-DBUILD_TESTS` +and `ON`, silently. `%*` (the raw, unsplit command line) doesn't have +this problem, and neither does `FOR /F`, which only splits on space/tab +by default — `build.bat` parses arguments (including `--preset`'s own +value) that way specifically so a `-D...=...` argument survives intact. +`build.sh`'s `"$@"`/`shift` never had this problem to begin with; this +paragraph is Windows-only trivia. + +### `scripts/run.sh` / `run.bat` + +``` +scripts/run.sh [--preset ] [-- program-args...] +``` + +Resolves a CMake target name to its built executable via the manifest +`_write_module_manifest()` generates at configure time +(`out/build//module_manifest-.txt`) — so this script +never needs updating when a module is added, removed, or renamed. Run +with no arguments to list every known target. + +Which build it runs: `--preset ` picks one; without it, the build +`scripts/build` made last (or the only one that exists), and it says +which on stderr (`==> Running hello_exe (out/build/linux-x64-gcc)`) so a +program's own stdout stays clean to pipe. If several builds exist and none +was made through the scripts, it asks for `--preset` instead of guessing. +A cross build runs under the emulator its toolchain registered — `qemu-user` +for ARM Linux, `wine` for MinGW-from-Linux — when one was installed at +configure time; otherwise it can only run on the target itself, and the +error says so. + +### `scripts/install.sh` / `install.bat` + +``` +scripts/install.sh [--preset ] [BUILD_TYPE] +``` + +Installs the build made with `--preset ` — by default the one made +last, as for `run` — to `out/install//{bin,lib}` (the same +name as the `out/build/` it came from), always passing +`--component` (read from `Configuration.cmake`'s `PROJECT_CODE_NAME`) +so a vendored or fetched dependency's own, untagged `install()` rules +are never pulled in alongside this project's own artifacts. + +### `scripts/docs.sh` / `docs.bat` + +``` +scripts/docs.sh [--preset ] [module-name] +``` + +Builds the `docs` target (every module) or `docs_` (just +one) and prints the path to the generated landing page, +`out/docs/index.html`. The output is shared by every preset — +documentation doesn't depend on which compiler built the code — so +`--preset` (default: the build made last) only chooses which configured +build directory the docs targets run from. A no-op with a clear message — not an error — +if Doxygen hasn't been fetched yet or `ENABLE_DOXYGEN=OFF`. + +### `scripts/refactor.sh` / `refactor.bat` + +``` +scripts/refactor.sh [path] +``` + +Runs `clang-format -i` and `clang-tidy --fix` across `source/` and +`c-api/` (or just the given path) — the "fix it" counterpart to the +pre-commit hook, which only checks. Review with `git diff` afterward, +like any auto-formatter. + +## Adding a module + +Every module — library, executable, test, or example — lives in its +own directory with a single-purpose `CMakeLists.txt`. The root +`CMakeLists.txt` discovers `source/*`, `source/tests/*`, and +`source/examples/*` automatically (`_discover_module_subdirectories()` +in `cmake/BuildHelpers.cmake`); nothing there needs editing. + +```cmake +# source/MyLib/CMakeLists.txt +add_lib_module(my_lib + TYPE SHARED # or STATIC; defaults to BUILD_SHARED_LIBS + DEPENDS fmt::fmt +) +``` + +```cmake +# source/MyApp/CMakeLists.txt +add_exe_module(my_app DEPENDS my_lib) +``` + +```cmake +# source/tests/my_lib_test/CMakeLists.txt +add_test_module(my_lib_test DEPENDS my_lib) # ctest-registered, built with -DBUILD_TESTS=ON +``` + +```cmake +# source/examples/my_example/CMakeLists.txt +add_example_module(my_example DEPENDS my_lib) # built with -DBUILD_EXAMPLES=ON +``` + +All four glob their own sources (`src/*.cpp` + `include/*.h(pp)` for a +library; `*.cpp` + `include/*.h(pp)` for an executable, recursively +from the module's own directory), apply per-target warning flags +(`/W4 /permissive-` on MSVC-family compilers, `-Wall -Wextra -Wpedantic` +elsewhere), set up `$ORIGIN`-relative `RPATH` on Linux, register the +target for install and for `scripts/run.sh`, and get their own Doxygen +target for free. `OUTPUT_NAME`, `EXTRA_SOURCES`, and `NO_DOCS` are +available on all four when the defaults don't fit — see +`cmake/BuildHelpers.cmake` for the full option list. + +## Adding a dependency + +Three patterns, in `cmake/Dependencies.cmake`, covering vendored and +fetched code: + +```cmake +# Header-only, vendored under vendor//include +add_vendor_header_only(tiny_ansi) + +# Vendored, with its own CMakeLists.txt, under vendor/ +add_vendor_subdirectory(tiny_case) + +# Fetched from a Git repo (or an already-installed copy that's new enough) +add_dependency(fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG 12.1.0 + GIT_SHALLOW TRUE + FIND_PACKAGE_ARGS 12 NAMES fmt +) +``` + +Always give `FIND_PACKAGE_ARGS` a minimum version (the `12`): without one, +*any* installed copy beats your pinned `GIT_TAG` — including a distro +package years older than the pin, which is how a "pinned" dependency +quietly stops being pinned. A dependency's own CMake options are set with +`set(