From b695b60153d56da4e1a2ffc4ba3828254641db05 Mon Sep 17 00:00:00 2001 From: TEC-GO Date: Sat, 26 Sep 2026 14:45:07 -0300 Subject: [PATCH] Movimiento de "Cuadrado" veloz en mapa blanco --- .clang-format | 117 +++ .clang-tidy | 161 ++++ .editorconfig | 34 + .gitattributes | 29 + .gitignore | 439 +++++++++ CMakeLists.txt | 397 ++++++++ CMakePresets.json | 166 ++++ Configuration.cmake | 861 ++++++++++++++++++ README.md | 825 +++++++++++++++++ StyleGuide.md | 748 +++++++++++++++ c-api/.clang-tidy | 57 ++ cmake/BuildDir.cmake | 126 +++ cmake/BuildHelpers.cmake | 407 +++++++++ cmake/Dependencies.cmake | 134 +++ cmake/Documentation.cmake | 190 ++++ cmake/DoxygenPin.cmake | 98 ++ cmake/FetchDoxygen.cmake | 111 +++ cmake/FetchNinja.cmake | 109 +++ cmake/HostArch.cmake | 155 ++++ cmake/NinjaPin.cmake | 109 +++ cmake/PrintConfig.cmake | 55 ++ cmake/toolchains/_common.cmake | 397 ++++++++ cmake/toolchains/linux-arm32-clang.cmake | 14 + cmake/toolchains/linux-arm32-gcc.cmake | 13 + cmake/toolchains/linux-arm64-clang.cmake | 14 + cmake/toolchains/linux-arm64-gcc.cmake | 13 + cmake/toolchains/linux-x64-clang.cmake | 14 + cmake/toolchains/linux-x64-gcc.cmake | 13 + cmake/toolchains/macos-arm64-clang.cmake | 16 + cmake/toolchains/macos-x64-clang.cmake | 16 + cmake/toolchains/windows-arm64-clang-cl.cmake | 13 + cmake/toolchains/windows-arm64-clang.cmake | 12 + cmake/toolchains/windows-arm64-msvc.cmake | 13 + cmake/toolchains/windows-x64-clang-cl.cmake | 12 + cmake/toolchains/windows-x64-clang.cmake | 12 + cmake/toolchains/windows-x64-mingw64.cmake | 13 + cmake/toolchains/windows-x64-msvc.cmake | 12 + cmake/toolchains/windows-x86-mingw32.cmake | 13 + scripts/_common.sh | 81 ++ scripts/_tools.ps1 | 581 ++++++++++++ scripts/_tools.sh | 626 +++++++++++++ scripts/build.bat | 358 ++++++++ scripts/build.sh | 280 ++++++ scripts/docs.bat | 136 +++ scripts/docs.sh | 99 ++ scripts/install.bat | 151 +++ scripts/install.sh | 97 ++ scripts/refactor.bat | 104 +++ scripts/refactor.sh | 103 +++ scripts/run.bat | 185 ++++ scripts/run.sh | 190 ++++ scripts/setup.bat | 219 +++++ scripts/setup.sh | 161 ++++ source/Pacman/CMakeLists.txt | 36 + source/Pacman/main.cpp | 46 + 55 files changed, 9391 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 Configuration.cmake create mode 100644 README.md create mode 100644 StyleGuide.md create mode 100644 c-api/.clang-tidy create mode 100644 cmake/BuildDir.cmake create mode 100644 cmake/BuildHelpers.cmake create mode 100644 cmake/Dependencies.cmake create mode 100644 cmake/Documentation.cmake create mode 100644 cmake/DoxygenPin.cmake create mode 100644 cmake/FetchDoxygen.cmake create mode 100644 cmake/FetchNinja.cmake create mode 100644 cmake/HostArch.cmake create mode 100644 cmake/NinjaPin.cmake create mode 100644 cmake/PrintConfig.cmake create mode 100644 cmake/toolchains/_common.cmake create mode 100644 cmake/toolchains/linux-arm32-clang.cmake create mode 100644 cmake/toolchains/linux-arm32-gcc.cmake create mode 100644 cmake/toolchains/linux-arm64-clang.cmake create mode 100644 cmake/toolchains/linux-arm64-gcc.cmake create mode 100644 cmake/toolchains/linux-x64-clang.cmake create mode 100644 cmake/toolchains/linux-x64-gcc.cmake create mode 100644 cmake/toolchains/macos-arm64-clang.cmake create mode 100644 cmake/toolchains/macos-x64-clang.cmake create mode 100644 cmake/toolchains/windows-arm64-clang-cl.cmake create mode 100644 cmake/toolchains/windows-arm64-clang.cmake create mode 100644 cmake/toolchains/windows-arm64-msvc.cmake create mode 100644 cmake/toolchains/windows-x64-clang-cl.cmake create mode 100644 cmake/toolchains/windows-x64-clang.cmake create mode 100644 cmake/toolchains/windows-x64-mingw64.cmake create mode 100644 cmake/toolchains/windows-x64-msvc.cmake create mode 100644 cmake/toolchains/windows-x86-mingw32.cmake create mode 100755 scripts/_common.sh create mode 100755 scripts/_tools.ps1 create mode 100755 scripts/_tools.sh create mode 100755 scripts/build.bat create mode 100755 scripts/build.sh create mode 100755 scripts/docs.bat create mode 100755 scripts/docs.sh create mode 100755 scripts/install.bat create mode 100755 scripts/install.sh create mode 100755 scripts/refactor.bat create mode 100755 scripts/refactor.sh create mode 100755 scripts/run.bat create mode 100755 scripts/run.sh create mode 100755 scripts/setup.bat create mode 100755 scripts/setup.sh create mode 100644 source/Pacman/CMakeLists.txt create mode 100644 source/Pacman/main.cpp 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(