Movimiento de "Cuadrado" veloz en mapa blanco

This commit is contained in:
Clinv committed 2026-09-26 14:45:07 -03:00
commit b695b60153
55 files changed
+9391

No files matched your search

+117
View File
@@ -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++ (<cstdio>, <cstdint>, ...).
- 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 — <vector>, <memory>, ...).
- Regex: '^<[a-z_]+>$'
Priority: 2
CaseSensitive: true
# 4. Third-party / external library headers (<fmt/format.h>, <boost/...>, ...).
- 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
+161
View File
@@ -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
+34
View File
@@ -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
+29
View File
@@ -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
+439
View File
@@ -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 <<<
+397
View File
@@ -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<NAME>=... 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 /<Config> 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/<preset> -- 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(
"$<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,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_<TAG>=...).
# -------------------------------------------------------
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/<Name>/CMakeLists.txt -- library or app/executable module
# source/tests/<Name>/CMakeLists.txt -- test module
# source/examples/<Name>/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 <build-dir>/module_manifest-<CONFIG>.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()
+166
View File
@@ -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" }
]
}
+861
View File
@@ -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<NAME>=... at configure time, instead of hunting through
# CMakeLists.txt/cmake/*.cmake -- or a script under scripts/ -- for a
# hardcoded copy. If you find yourself typing a value this file already
# defines anywhere else, that's a bug: read it from here instead (see
# "How scripts read this file" below for the non-CMake side of that).
#
# -----------------------------------------------------------------------
# Project identity
# -----------------------------------------------------------------------
# PROJECT_CODE_NAME machine-friendly identifier baked into every
# target as PROJECT_NAME_CODE (BuildHelpers.cmake's
# _finalize_module_target) and used as the
# `cmake --install --component` name (CMakeLists.txt's
# install() calls, scripts/install.sh / install.bat).
# DEFAULT_BUILD_TYPE what CMAKE_BUILD_TYPE (single-config Ninja) and
# the scripts' own BUILD_TYPE argument default to
# when nothing else is specified.
#
# -----------------------------------------------------------------------
# Target selection (DIST_TARGET)
# -----------------------------------------------------------------------
# DIST_TARGET identifies what you're building for as a single
# "<platform>-<arch>-<compiler>" string. It is declared by the toolchain
# file in use (cmake/toolchains/<DIST_TARGET>.cmake -- what every
# CMakePresets.json preset selects, and the recommended way to build), or
# given explicitly with -DDIST_TARGET=<value> (if both are present they
# must agree), or, with neither, auto-detected from the compiler CMake
# found. Every value in PMN_VALID_DIST_TARGETS below is supported:
#
# linux-x64-gcc GCC on x86-64 Linux
# linux-x64-clang Clang on x86-64 Linux
# linux-arm64-gcc GCC on 64-bit ARM Linux (aarch64)
# linux-arm64-clang Clang on 64-bit ARM Linux (aarch64)
# linux-arm32-gcc GCC on 32-bit ARM Linux (armhf/armv7l and similar)
# linux-arm32-clang Clang on 32-bit ARM Linux
# macos-arm64-clang Clang on Apple Silicon macOS -- Apple Clang from
# the Xcode Command Line Tools, or LLVM's own
# Clang (e.g. Homebrew's); both are "clang" here
# macos-x64-clang Clang on Intel macOS (or, given Rosetta 2, an
# Apple Silicon Mac: the toolchain just asks the
# compiler for -arch x86_64)
# windows-x64-msvc cl.exe (MSVC) targeting x64
# windows-arm64-msvc cl.exe (MSVC) targeting ARM64
# windows-x64-clang-cl clang-cl targeting x64 (Clang targeting the
# MSVC ABI -- CMake's MSVC variable is TRUE for
# this too)
# windows-arm64-clang-cl clang-cl targeting ARM64
# windows-x64-clang Clang with its own GNU-style driver on Windows,
# targeting x64 (e.g. an MSYS2 CLANG64
# environment) -- distinct from clang-cl above,
# which mimics cl.exe instead of gcc
# windows-arm64-clang Clang with its own GNU-style driver on Windows,
# targeting ARM64 (e.g. an MSYS2 CLANGARM64
# environment)
# windows-x86-mingw32 32-bit x86 GCC via MinGW
# windows-x64-mingw64 64-bit x64 GCC via MinGW-w64
#
# No macOS GCC target and no universal one. Apple's own toolchain IS Clang
# (a Homebrew GCC would be a second, unrelated compiler ecosystem needing
# its own SDK and sysroot handling), and a universal binary is a
# CMAKE_OSX_ARCHITECTURES list with two entries -- which doesn't fit a
# DIST_TARGET that names ONE architecture, and is rejected below with the
# reason rather than mislabeled. To ship one universal file, build both
# macOS presets and combine the results: `lipo -create <arm64 file>
# <x64 file> -output <file>`.
#
# No windows-arm32-* and no ARM64 MinGW target: 32-bit ARM Windows has no
# real toolchain support left in any of the compilers above (MSVC dropped
# it; it only ever existed for Windows RT / Windows 10 IoT Core), and
# mainline MinGW-w64's GCC has no ARM64 Windows (PE/COFF) backend -- ARM64
# Windows toolchains in that ecosystem are LLVM-based (MSYS2's
# CLANGARM64, covered by windows-arm64-clang above), not GCC-based.
#
# Architecture (x64/arm64/arm32) is detected differently depending on
# compiler family, because CMAKE_SYSTEM_PROCESSOR turns out not to be
# trustworthy for all of them:
# - Linux (GCC/Clang) goes through cmake/HostArch.cmake's
# pmn_target_arch(), i.e. CMAKE_SYSTEM_PROCESSOR: `uname -m` for a
# native build, and whatever the toolchain file sets it to for a
# cross build (so an aarch64 cross build from an x64 machine is
# labeled arm64, not x64). Clang's GNU-style driver on Windows goes
# through pmn_host_arch() instead, an environment-variable check --
# see that file for why CMAKE_SYSTEM_PROCESSOR/
# CMAKE_HOST_SYSTEM_PROCESSOR can't be trusted there (a second,
# related trap: a 32-bit cmake.exe under WoW64, independent of the
# MSVC one below).
# - macOS goes through CMAKE_OSX_ARCHITECTURES, which the macOS toolchain
# files set to exactly one architecture, and only falls back to
# CMAKE_SYSTEM_PROCESSOR (`uname -m`) without one. Apple's clang is a
# single multi-architecture compiler, so an x86_64 build on an Apple
# Silicon Mac is just a different -arch -- not a cross compiler, and
# nothing that CMAKE_SYSTEM_PROCESSOR (which under Rosetta 2 reports
# the architecture of the cmake PROCESS, the same class of trap as
# WoW64 on Windows) could describe reliably.
# - MSVC and clang-cl use CMAKE_CXX_COMPILER_ARCHITECTURE_ID instead --
# CMAKE_SYSTEM_PROCESSOR is a documented, confirmed-in-the-wild trap
# here (tracked as CMake issue #25466): it reflects the architecture
# of the cmake.exe process itself, not what the selected cl.exe/
# clang-cl actually targets, and those two routinely differ (a
# same-machine cross build -- e.g. an x64 Visual Studio install's
# "HostX64/ARM64" tools -- is the normal way to target ARM64 from an
# x64 dev machine).
#
# Toolchain files and cross-compiling: cmake/toolchains/ has one toolchain
# file per DIST_TARGET, named after it, and every CMakePresets.json preset
# selects the one named after itself. A toolchain file settles the compiler
# (and, for clang-cl, the --target triple) before project() runs, declares
# the DIST_TARGET it is for, and -- where the target isn't the machine
# running CMake -- sets up a real cross build: the linux-arm64-* and
# linux-arm32-* targets cross-compile from any other Linux machine (an x64
# one, say), and the windows-*-mingw* targets from Linux via mingw-w64,
# while on matching hardware (ARM64 for linux-arm64-*) the very same
# preset just builds natively. On macOS the toolchain pins the
# architecture with CMAKE_OSX_ARCHITECTURES and a minimum macOS version
# with CMAKE_OSX_DEPLOYMENT_TARGET (11.0 unless you pass your own) -- both
# are only honored when set BEFORE project(), which is exactly when a
# toolchain file runs. cmake/toolchains/_common.cmake documents
# exactly what each one does and which packages it needs. The exception is
# MSVC: cl.exe takes its architecture from the Visual Studio developer
# environment it is started from (vcvarsall.bat), which no CMake file can
# switch, so the windows-*-msvc toolchains only pick cl and check it is
# there -- the architecture check below then verifies it is the right one.
#
# Without a toolchain file, pointing -DCMAKE_CXX_COMPILER at a cross
# compiler is not enough on its own: CMake is never told the target is a
# different architecture, so the auto-detected DIST_TARGET follows the
# HOST and you'll get a cross-check mismatch (see below) instead, same as
# any other unmet assumption this file's auto-detection makes.
#
# IMPORTANT: -DDIST_TARGET=... on its own only labels the build (which
# install subdirectory it lands in, which metadata gets printed); it does
# NOT select a compiler or an architecture. CMake still uses whatever
# CMAKE_CXX_COMPILER it would otherwise find -- your PATH, your
# environment, or an explicit -DCMAKE_CXX_COMPILER=... -- same as if
# DIST_TARGET had never been mentioned. Passing
# -DDIST_TARGET=windows-x64-mingw64 alone, with no MinGW compiler actually
# configured, does not make CMake go find one; it configures whatever
# compiler it would have anyway (often MSVC, since CMake looks for that
# first on Windows) and mislabels the result. Configuration.cmake catches
# exactly this: a DIST_TARGET that disagrees with what the real compiler
# (and architecture) auto-detects to is a FATAL_ERROR, not a silent
# mislabel -- see the "cross-check" step below. A toolchain file doesn't
# have the problem, because it sets the label and the compiler together:
# use the toolchain file for that target
# (-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<DIST_TARGET>.cmake), or simply
# the matching CMakePresets.json preset, which selects it.
#
# Auto-detection FATAL_ERRORs rather than guessing when the compiler
# doesn't match any of the above, instead of silently assuming MSVC --
# that silent assumption is exactly what used to misidentify a MinGW GCC
# build as MSVC (COMPILER_ID "msvc" for an actual GNU compiler), which in
# turn misfiled its `cmake --install` output under .../windows-x64-msvc/.
#
# PLATFORM, ARCHITECTURE, and COMPILER_ID below are metadata derived from
# DIST_TARGET; CMakeLists.txt and the cmake/*.cmake modules consume these
# rather than re-deriving them.
#
# -----------------------------------------------------------------------
# Output layout
# -----------------------------------------------------------------------
# Everything this project generates lives under one OUT_DIR:
#
# out/
# build/<preset>/ CMAKE_BINARY_DIR -- one build tree per
# preset (BUILD_OUTPUT_DIR is the
# root), so switching presets never
# reuses, invalidates or wipes another
# preset's cache. CMakePresets.json's
# binaryDir and scripts/build.sh
# (-B out/build/<preset>) agree on it;
# without a preset the scripts use
# out/build/default. Compiled
# artifacts land in
# <build>/<RUNTIME_OUTPUT_SUBDIR> and
# <build>/<ARCHIVE_OUTPUT_SUBDIR>
# during normal day-to-day builds.
# install/<DIST_TARGET>/{bin,lib} what `cmake --install` / scripts/
# install.sh populates -- the same
# name as build/<preset>, since a
# preset is named after its target.
# dist/ reserved for packaged/archived
# output (e.g. a future CPack
# integration) -- not populated by
# this project yet, but the path is
# configured and ready.
# docs/<module>/html, docs/index.html generated Doxygen output
# (cmake/Documentation.cmake).
#
# One thing lives outside OUT_DIR on purpose: .cache/tools/ (TOOLCACHE_DIR)
# caches downloaded, pinned build tools (currently Ninja and Doxygen), kept
# separate so a clean `rm -rf out/` doesn't force a re-download.
#
# .gitignore is kept in sync with the paths below automatically, every
# configure -- see the bottom of this file.
#
# -----------------------------------------------------------------------
# How scripts read this file
# -----------------------------------------------------------------------
# scripts/build.sh|build.bat and scripts/install.sh|install.bat need
# values from here too (the install component name, the default build
# type, where the tool cache lives) but are running before any CMake
# configure exists to ask. They get them from cmake/PrintConfig.cmake, a
# tiny `cmake -P` script that includes this file and prints one value --
# see that file for the exact mechanism, and any of the four scripts
# above for how each shell dialect calls it. Choosing WHICH build
# directory a script works on (a --preset, or the one built last) is the
# same kind of job and is done the same way, by cmake/BuildDir.cmake, so
# that logic exists once for both shell dialects. That's what makes this file
# a *real* source of truth end to end: nothing outside it -- not a
# .cmake module, not a shell script, not .gitignore -- hardcodes a second
# copy of a value defined here.
#
# One consequence: a plain `cmake -P` script never runs project() and so
# never gets real compiler detection -- WIN32, CMAKE_CXX_COMPILER_ID,
# CMAKE_SIZEOF_VOID_P etc. can't be trusted there. Everything in "Target
# selection" above is therefore skipped when PMN_CONFIG_QUERY_ONLY is set
# (cmake/PrintConfig.cmake sets it before including this file), along
# with this file's own filesystem side effects (creating out/dist/,
# rewriting .gitignore) -- a query has no business doing either, and it
# runs far more often than a real configure, which makes that doubly
# true. Keep that in mind if you add to this file: anything that needs a
# real compiler belongs inside the `if(NOT PMN_CONFIG_QUERY_ONLY)` guard
# below (where it's simply unavailable to a script query, on purpose);
# anything else must stay evaluable without one, and must not
# message(STATUS ...) outside that guard either, or it'll show up as
# extra noise in a script's query output (see PrintConfig.cmake).
#
# Included by CMakeLists.txt right after project(), before any other
# variable defined here is used, so every value is available project-wide
# from that point on.
# =============================================================================
include_guard(GLOBAL)
# -------------------------------------------------------
# Project identity
# -------------------------------------------------------
set(PROJECT_CODE_NAME "demo" CACHE STRING
"Machine-friendly identifier: baked into targets as PROJECT_NAME_CODE and used as the `cmake --install` component name")
set(DEFAULT_BUILD_TYPE "Release" CACHE STRING
"CMAKE_BUILD_TYPE / scripts BUILD_TYPE default when none is given")
# -------------------------------------------------------
# Target selection -- see the file header for the full model. Skipped in
# query mode (see "How scripts read this file" above): it needs real
# compiler detection that a bare `cmake -P` script never performs, and no
# script currently needs DIST_TARGET/PLATFORM/ARCHITECTURE/COMPILER_ID
# anyway -- cmake --install reads what a real configure already baked
# into the build directory, not these variables directly.
# -------------------------------------------------------
if(NOT PMN_CONFIG_QUERY_ONLY)
set(PMN_VALID_DIST_TARGETS
linux-x64-gcc
linux-x64-clang
linux-arm64-gcc
linux-arm64-clang
linux-arm32-gcc
linux-arm32-clang
macos-arm64-clang
macos-x64-clang
windows-x64-msvc
windows-arm64-msvc
windows-x64-clang-cl
windows-arm64-clang-cl
windows-x64-clang
windows-arm64-clang
windows-x86-mingw32
windows-x64-mingw64
)
list(JOIN PMN_VALID_DIST_TARGETS " " _pmn_valid_dist_targets_str)
# Where DIST_TARGET came from, for the messages below. A toolchain file
# (cmake/toolchains/<DIST_TARGET>.cmake -- every CMakePresets.json preset
# selects the one named after itself) declares the target it is for via
# PMN_TOOLCHAIN_DIST_TARGET (see toolchains/_common.cmake), so the label
# and the compiler the toolchain picked travel together. It is resolved
# here, ahead of the auto-detection below, because
# _pmn_dist_target_undetected() decides how hard to fail based on
# whether DIST_TARGET is already known. An explicit -DDIST_TARGET is
# still accepted alongside a toolchain, but it has to agree with it.
set(_pmn_dist_target_source "")
if(PMN_TOOLCHAIN_DIST_TARGET)
if(DIST_TARGET AND NOT DIST_TARGET STREQUAL PMN_TOOLCHAIN_DIST_TARGET)
message(FATAL_ERROR
"DIST_TARGET was explicitly set to '${DIST_TARGET}', but the toolchain file in use "
"(${CMAKE_TOOLCHAIN_FILE}) is for '${PMN_TOOLCHAIN_DIST_TARGET}'.\n"
"Drop -DDIST_TARGET (the toolchain already says which target this is), or "
"use the toolchain file for '${DIST_TARGET}':\n"
" ${CMAKE_SOURCE_DIR}/cmake/toolchains/${DIST_TARGET}.cmake")
endif()
set(DIST_TARGET "${PMN_TOOLCHAIN_DIST_TARGET}")
set(_pmn_dist_target_source "toolchain")
elseif(DIST_TARGET)
set(_pmn_dist_target_source "explicit")
endif()
# _pmn_dist_target_undetected(REASON...): an auto-detection branch
# below hit a case with no DIST_TARGET for it. REASON explains why --
# shown whether or not DIST_TARGET was explicitly given (e.g. via a
# CMakePresets.json preset), so a preset user gets the same specific
# diagnosis a plain, no-DIST_TARGET user already did.
#
# Previously REASON was only ever shown when DIST_TARGET was unset;
# setting DIST_TARGET (which every preset does -- the recommended way
# to build, per CMakePresets.json and the README) silently discarded
# it in favor of the generic "auto-detects to none of the values this
# project recognizes" cross-check message further down, with no
# explanation of WHY -- exactly the confusing, unhelpful failure this
# macro exists to prevent.
#
# No DIST_TARGET means there's nothing to fall back on, so REASON is
# the final word: FATAL_ERROR, with the full valid-values list. With
# DIST_TARGET set, REASON is advisory context for the unified
# cross-check FATAL_ERROR further down (which still runs, and has the
# copy-pasteable fix) -- so this is STATUS, and _pmn_detected_dist_target
# is left at "" for that check to pick up, same as before this macro
# existed.
macro(_pmn_dist_target_undetected)
if(DIST_TARGET)
set(_pmn_detected_dist_target "")
message(STATUS "Could not auto-detect a DIST_TARGET: " ${ARGN})
else()
message(FATAL_ERROR "Could not auto-detect a DIST_TARGET: " ${ARGN}
"\nValid values: ${_pmn_valid_dist_targets_str}")
endif()
endmacro()
include("${CMAKE_SOURCE_DIR}/cmake/HostArch.cmake")
# What DIST_TARGET the CXX compiler CMake actually found auto-detects
# to -- computed unconditionally, not only when DIST_TARGET is unset,
# so an explicitly-given DIST_TARGET can be cross-checked against it
# below rather than trusted as a label. See the file header for why
# that check exists.
if(WIN32)
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" OR
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"))
# MSVC, and clang-cl (Clang's cl.exe-compatible driver
# personality, selected either by invoking a binary literally
# named clang-cl(.exe) or via `clang++ --driver-mode=cl`;
# CMake surfaces this as CMAKE_CXX_COMPILER_FRONTEND_VARIANT
# rather than a distinct COMPILER_ID, since it's the same
# Clang either way -- just a different argument dialect and
# default target ABI). Both get their architecture from
# CMAKE_CXX_COMPILER_ARCHITECTURE_ID, deliberately NOT from
# CMAKE_SYSTEM_PROCESSOR/CMAKE_HOST_SYSTEM_PROCESSOR (see
# HostArch.cmake's header for why those can't be trusted for
# this specific case). CMAKE_CXX_COMPILER_ARCHITECTURE_ID is
# what CMake itself derives by probing the compiler -- it
# needs this internally for the right /machine: linker flag --
# so it reflects the real target regardless of which cl.exe/
# clang-cl variant ended up on PATH. Its values are
# mixed-case by long-standing CMake convention (X86, x64,
# ARM, ARM64); normalized to uppercase here so the
# comparisons below aren't tripped by that inconsistency.
string(TOUPPER "${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}" _pmn_msvc_arch_id)
if(_pmn_msvc_arch_id STREQUAL "X64")
set(_pmn_win_arch "x64")
elseif(_pmn_msvc_arch_id STREQUAL "ARM64")
set(_pmn_win_arch "arm64")
else()
# Includes X86 and ARM (32-bit) -- real MSVC targets, but
# not ones this project has a DIST_TARGET for (32-bit ARM
# Windows has no real toolchain support left in any
# compiler; see the file header). Treated as unrecognized
# rather than silently mislabeled as x64 or arm64.
set(_pmn_win_arch "")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set(_pmn_win_compiler_id "msvc")
else()
set(_pmn_win_compiler_id "clang-cl")
endif()
if(_pmn_win_arch)
set(_pmn_detected_dist_target "windows-${_pmn_win_arch}-${_pmn_win_compiler_id}")
else()
# X86 specifically gets its own hint rather than the
# generic message below: it's by far the most likely way
# to actually hit this branch in practice (ARM on Windows
# without ARM64 is rare) -- not a machine that's genuinely
# 32-bit-only (vanishingly rare today), almost always
# PATH/environment handing CMake the 32-bit-hosted,
# 32-bit-targeting cl.exe/clang-cl instead of the x64 one,
# e.g. from opening a plain shell instead of an "x64
# Native Tools Command Prompt for VS". Reported whether or
# not DIST_TARGET was explicitly given -- see
# _pmn_dist_target_undetected above.
if(_pmn_msvc_arch_id STREQUAL "X86")
_pmn_dist_target_undetected(
"${_pmn_win_compiler_id} is "
"targeting X86 (32-bit) -- almost always a sign that PATH/the "
"current shell is handing CMake the 32-bit-hosted, "
"32-bit-targeting ${_pmn_win_compiler_id}, not that this machine "
"is actually 32-bit-only.\n"
"Open an \"x64 Native Tools Command Prompt for VS\" (or a "
"\"Developer PowerShell for VS\" defaulted to x64) so an x64 "
"${_pmn_win_compiler_id} is what's first on PATH, delete this "
"build directory (a failed configure's CMakeCache.txt won't just "
"fix itself), and reconfigure.\n"
"If 32-bit really is intentional, this project doesn't have a "
"DIST_TARGET for it -- set one explicitly: -DDIST_TARGET=<value>"
)
else()
_pmn_dist_target_undetected(
"${_pmn_win_compiler_id} is "
"targeting an architecture ('${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}') "
"this project doesn't have a DIST_TARGET for -- set one "
"explicitly: -DDIST_TARGET=<value>"
)
endif()
endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# Clang with its own GNU-style driver on Windows (e.g. an
# MSYS2 CLANG64/CLANGARM64 environment) -- distinct from
# clang-cl above, which mimics cl.exe instead of gcc.
# CMAKE_CXX_COMPILER_ARCHITECTURE_ID is specific to the MSVC-
# style detection used above and isn't populated for this
# driver mode. Falls back to HostArch.cmake instead -- unlike
# the raw cl.exe/clang-cl case, a GNU-style environment like
# MSYS2 is used as a coherent whole (its own cmake, its own
# clang++, both built for and normally run on the same
# architecture), the same assumption the MinGW pointer-size
# check below already makes for mingw32 vs. mingw64.
pmn_host_arch(_pmn_win_clang_arch _pmn_win_clang_raw)
if(_pmn_win_clang_arch STREQUAL "arm64")
set(_pmn_detected_dist_target "windows-arm64-clang")
elseif(_pmn_win_clang_arch STREQUAL "x64")
set(_pmn_detected_dist_target "windows-x64-clang")
else()
_pmn_dist_target_undetected(
"unrecognized host "
"architecture '${_pmn_win_clang_raw}' for Clang's GNU-style driver "
"on Windows -- set one explicitly: -DDIST_TARGET=<value>"
)
endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# A real GCC on Windows is MinGW (mingw.org's original,
# 32-bit-only project) or MinGW-w64 (adds 64-bit; also
# provides a 32-bit target, conventionally called
# "mingw32" too since it's the direct successor). Tell
# them apart by pointer size rather than parsing the
# compiler's own executable name, which is a config
# choice, not something to depend on. No ARM MinGW target
# here on purpose, unlike the Clang branch above: mainline
# MinGW-w64's GCC has no ARM64 Windows (PE/COFF) backend --
# ARM64 Windows toolchains in that ecosystem are LLVM-based
# (MSYS2's CLANGARM64), not GCC-based.
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(_pmn_detected_dist_target "windows-x64-mingw64")
else()
set(_pmn_detected_dist_target "windows-x86-mingw32")
endif()
elseif(DIST_TARGET)
# Can't cross-check an explicit value against a compiler this
# doesn't recognize at all -- the mismatch message below
# handles an empty detected value.
set(_pmn_detected_dist_target "")
else()
message(FATAL_ERROR
"Could not auto-detect a DIST_TARGET for CXX compiler "
"'${CMAKE_CXX_COMPILER_ID}' on Windows -- set one "
"explicitly: -DDIST_TARGET=<value>\n"
"Valid values: ${_pmn_valid_dist_targets_str}")
endif()
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# macOS (CMAKE_SYSTEM_NAME is "Darwin" for macOS only; iOS and
# friends have their own names). The architecture is what
# CMAKE_OSX_ARCHITECTURES says when it's set -- the macOS toolchain
# files always set it, to one value -- else CMAKE_SYSTEM_PROCESSOR.
# See the file header for why not CMAKE_SYSTEM_PROCESSOR first.
# Mapped explicitly rather than through HostArch.cmake's spelling
# table, which files anything starting with "arm" -- arm64e, say --
# under 32-bit ARM.
set(_pmn_macos_arch "")
if(CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _pmn_macos_arch_count)
if(_pmn_macos_arch_count GREATER 1)
message(FATAL_ERROR
"CMAKE_OSX_ARCHITECTURES is '${CMAKE_OSX_ARCHITECTURES}' -- several "
"architectures, i.e. a universal binary. A DIST_TARGET names exactly one "
"architecture (macos-arm64-clang, macos-x64-clang): build each with its "
"preset and combine the results with `lipo -create <arm64 file> <x64 file> "
"-output <file>` if you need a single universal file.")
elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
set(_pmn_macos_arch "arm64")
elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64")
set(_pmn_macos_arch "x64")
endif()
set(_pmn_macos_arch_raw "CMAKE_OSX_ARCHITECTURES '${CMAKE_OSX_ARCHITECTURES}'")
else()
pmn_target_arch(_pmn_macos_arch _pmn_macos_arch_processor)
set(_pmn_macos_arch_raw "CMAKE_SYSTEM_PROCESSOR '${_pmn_macos_arch_processor}'")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
if(_pmn_macos_arch)
set(_pmn_detected_dist_target "macos-${_pmn_macos_arch}-clang")
else()
_pmn_dist_target_undetected(
"unrecognized macOS architecture (${_pmn_macos_arch_raw}) -- set one "
"explicitly: -DDIST_TARGET=<value>"
)
endif()
elseif(DIST_TARGET)
set(_pmn_detected_dist_target "")
else()
message(FATAL_ERROR
"Could not auto-detect a DIST_TARGET for CXX compiler "
"'${CMAKE_CXX_COMPILER_ID}' on macOS -- only Clang (Apple's, from the "
"Xcode Command Line Tools, or LLVM's own) is a supported macOS target. "
"Set one explicitly: -DDIST_TARGET=<value>\n"
"Valid values: ${_pmn_valid_dist_targets_str}")
endif()
else()
# Linux: architecture comes from CMAKE_SYSTEM_PROCESSOR via
# HostArch.cmake -- reliable here (unlike the raw cl.exe/clang-cl
# case above). For a native build that is simply `uname -m`; for a
# cross build the toolchain file (cmake/toolchains/) sets it to the
# TARGET's architecture, which is exactly what should be labeled --
# asking the host machine instead would call an aarch64 cross
# build "x64".
pmn_target_arch(_pmn_linux_arch)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# Not a blind TOLOWER of CMAKE_CXX_COMPILER_ID: CMake's own ID
# for GCC is "GNU" (a case-fold gives "gnu", which none of
# PMN_VALID_DIST_TARGETS above or CMakePresets.json spell --
# every one of them, and everyone outside CMake, calls it
# "gcc"). Same reasoning the MSVC/clang-cl branch above
# already applies to its own two compiler IDs.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(_pmn_linux_compiler_id "gcc")
else()
set(_pmn_linux_compiler_id "clang")
endif()
if(_pmn_linux_arch)
set(_pmn_detected_dist_target "linux-${_pmn_linux_arch}-${_pmn_linux_compiler_id}")
else()
_pmn_dist_target_undetected(
"unrecognized "
"CMAKE_SYSTEM_PROCESSOR '${CMAKE_SYSTEM_PROCESSOR}' -- set one "
"explicitly: -DDIST_TARGET=<value>"
)
endif()
elseif(DIST_TARGET)
set(_pmn_detected_dist_target "")
else()
message(FATAL_ERROR
"Could not auto-detect a DIST_TARGET for CXX compiler "
"'${CMAKE_CXX_COMPILER_ID}' -- set one explicitly: "
"-DDIST_TARGET=<value>\n"
"Valid values: ${_pmn_valid_dist_targets_str}")
endif()
endif()
if(NOT DIST_TARGET)
set(DIST_TARGET "${_pmn_detected_dist_target}")
message(STATUS "DIST_TARGET not set -- auto-detected: ${DIST_TARGET}")
message(STATUS " (use a CMakePresets.json preset -- or -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<target>.cmake -- to set it explicitly)")
else()
if(NOT DIST_TARGET IN_LIST PMN_VALID_DIST_TARGETS)
message(FATAL_ERROR
"Unsupported DIST_TARGET: '${DIST_TARGET}'.\n"
"Valid values: ${_pmn_valid_dist_targets_str}\n"
"Tip: open CMakePresets.json in your IDE to select a preset, "
"or pass -DDIST_TARGET=<value> on the command line.")
endif()
if(NOT DIST_TARGET STREQUAL _pmn_detected_dist_target)
# This is the check described in the file header: a DIST_TARGET
# that disagrees with the compiler CMake actually found.
if(_pmn_detected_dist_target)
set(_pmn_detected_desc "'${_pmn_detected_dist_target}'")
else()
set(_pmn_detected_desc "none of the values this project recognizes")
endif()
# The fix depends on where DIST_TARGET came from. From a
# toolchain file the compiler was already chosen to match, so
# something overrode it afterwards; from a bare -DDIST_TARGET
# nothing ever chose a compiler at all.
#
# (string(CONCAT) rather than a multi-value set(): a list would
# swallow every ";" in the text when expanded into message().)
if(_pmn_dist_target_source STREQUAL "toolchain")
string(CONCAT _pmn_fix
"The toolchain file picked its own compiler, so this means it was overridden "
"afterwards -- most likely an explicit -DCMAKE_CXX_COMPILER=... (on macOS "
"also -DCMAKE_OSX_ARCHITECTURES=...), or a CMakeCache.txt left in this "
"build directory by an earlier configure for a different target.\n"
"Remove that override, or configure into a fresh build directory. The default "
"layout already gives every preset its own (out/build/<preset>); "
"${CMAKE_BINARY_DIR} is being shared between targets.")
else()
string(CONCAT _pmn_fix
"-DDIST_TARGET=... only labels the build; it does not select a compiler. Use "
"the toolchain file made for that target, which sets both together:\n"
" -DCMAKE_TOOLCHAIN_FILE=${CMAKE_SOURCE_DIR}/cmake/toolchains/${DIST_TARGET}.cmake\n"
"or the matching CMakePresets.json preset, which selects it for you:\n"
" cmake --preset ${DIST_TARGET}")
endif()
# Some compilers can't be redirected from CMake at all; say so
# rather than only pointing at a toolchain file that cannot
# help.
set(_pmn_family_hint "")
if(DIST_TARGET MATCHES "-msvc$")
set(_pmn_family_hint
"\ncl.exe's architecture is decided by the Visual Studio developer environment \
it is run from, not by CMake: open the matching Native Tools / Cross Tools Command Prompt \
(vcvarsall.bat).")
elseif(DIST_TARGET MATCHES "^windows-(x64|arm64)-clang$")
set(_pmn_family_hint
"\nThis GNU-style Clang gets its target from the shell environment it runs in: \
use an MSYS2 CLANG64 (x64) or CLANGARM64 (ARM64) shell.")
endif()
message(FATAL_ERROR
"DIST_TARGET is '${DIST_TARGET}', but the CXX compiler CMake actually found -- "
"${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} -- auto-detects to "
"${_pmn_detected_desc}, not '${DIST_TARGET}'.\n"
"${_pmn_fix}\n"
"${_pmn_family_hint}")
endif()
message(STATUS "DIST_TARGET = ${DIST_TARGET}")
endif()
# Platform / compiler metadata from DIST_TARGET.
if(DIST_TARGET STREQUAL "linux-x64-gcc")
set(PLATFORM "linux")
set(ARCHITECTURE "x64")
set(COMPILER_ID "gcc")
elseif(DIST_TARGET STREQUAL "linux-x64-clang")
set(PLATFORM "linux")
set(ARCHITECTURE "x64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "linux-arm64-gcc")
set(PLATFORM "linux")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "gcc")
elseif(DIST_TARGET STREQUAL "linux-arm64-clang")
set(PLATFORM "linux")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "linux-arm32-gcc")
set(PLATFORM "linux")
set(ARCHITECTURE "arm32")
set(COMPILER_ID "gcc")
elseif(DIST_TARGET STREQUAL "linux-arm32-clang")
set(PLATFORM "linux")
set(ARCHITECTURE "arm32")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "macos-arm64-clang")
set(PLATFORM "macos")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "macos-x64-clang")
set(PLATFORM "macos")
set(ARCHITECTURE "x64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "windows-x64-msvc")
set(PLATFORM "windows")
set(ARCHITECTURE "x64")
set(COMPILER_ID "msvc")
elseif(DIST_TARGET STREQUAL "windows-arm64-msvc")
set(PLATFORM "windows")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "msvc")
elseif(DIST_TARGET STREQUAL "windows-x64-clang-cl")
set(PLATFORM "windows")
set(ARCHITECTURE "x64")
set(COMPILER_ID "clang-cl")
elseif(DIST_TARGET STREQUAL "windows-arm64-clang-cl")
set(PLATFORM "windows")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "clang-cl")
elseif(DIST_TARGET STREQUAL "windows-x64-clang")
set(PLATFORM "windows")
set(ARCHITECTURE "x64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "windows-arm64-clang")
set(PLATFORM "windows")
set(ARCHITECTURE "arm64")
set(COMPILER_ID "clang")
elseif(DIST_TARGET STREQUAL "windows-x86-mingw32")
set(PLATFORM "windows")
set(ARCHITECTURE "x86")
set(COMPILER_ID "mingw32")
elseif(DIST_TARGET STREQUAL "windows-x64-mingw64")
set(PLATFORM "windows")
set(ARCHITECTURE "x64")
set(COMPILER_ID "mingw64")
else()
message(FATAL_ERROR
"Unsupported DIST_TARGET: '${DIST_TARGET}'.\n"
"Valid values: ${_pmn_valid_dist_targets_str}\n"
"Tip: open CMakePresets.json in your IDE to select a preset, "
"or pass -DDIST_TARGET=<value> on the command line.")
endif()
message(STATUS "Platform = ${PLATFORM}")
message(STATUS "Architecture = ${ARCHITECTURE}")
message(STATUS "Compiler = ${COMPILER_ID} (${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION})")
endif()
# -------------------------------------------------------
# Output paths
# -------------------------------------------------------
set(OUT_DIR "${CMAKE_SOURCE_DIR}/out" CACHE PATH
"Root directory for everything this project generates: build artifacts, install trees, docs, and reserved dist packaging output")
set(DIST_OUTPUT_DIR "${OUT_DIR}/dist" CACHE PATH
"Reserved for packaged/archived distribution output (e.g. a future CPack integration). Not populated by this project yet.")
set(BUILD_OUTPUT_DIR "${OUT_DIR}/build" CACHE PATH
"Root of the build trees: one subdirectory (the CMAKE_BINARY_DIR) per preset, so switching presets never reuses or wipes another preset's build")
set(INSTALL_OUTPUT_DIR "${OUT_DIR}/install" CACHE PATH
"Root of the cmake --install destination tree, one subdirectory per DIST_TARGET")
set(DOCS_OUTPUT_DIR "${OUT_DIR}/docs" CACHE PATH
"Root of generated Doxygen documentation, one subdirectory per module")
set(COMPILE_COMMANDS_DESTINATION "${CMAKE_SOURCE_DIR}/compile_commands.json" CACHE FILEPATH
"Where compile_commands.json is copied to after every build, for clangd/clang-tidy tooling (see CMakeLists.txt)")
# Deliberately NOT under OUT_DIR: this caches downloaded, pinned build
# tools (Ninja and Doxygen -- see cmake/FetchNinja.cmake and
# cmake/FetchDoxygen.cmake), which are machine-local and expensive to
# re-fetch, not build output. Keeping it separate means a clean `rm -rf
# out/` (a normal "start fresh" gesture) doesn't force a re-download.
set(TOOLCACHE_DIR "${CMAKE_SOURCE_DIR}/.cache/tools" CACHE PATH
"Where pinned build tools (currently Ninja and Doxygen) are cached, kept separate from OUT_DIR")
if(NOT PMN_CONFIG_QUERY_ONLY)
# out/dist/ is reserved (see the header comment above) and nothing writes
# to it yet, so it's created eagerly here -- otherwise it simply wouldn't
# exist until something populates it, which could easily read as "this
# was never wired up" rather than "intentionally reserved for later".
# Skipped in query mode along with the .gitignore sync below -- a value
# lookup shouldn't have filesystem side effects, and it runs far more
# often than a real configure.
file(MAKE_DIRECTORY "${DIST_OUTPUT_DIR}")
endif()
# Subdirectory NAMES only (not full paths) for where compiled artifacts
# land inside whatever the current build directory is (CMAKE_BINARY_DIR) --
# these apply correctly no matter where -B points, including if you build
# outside out/build/<preset> entirely. LIBRARY defaults to the same subdirectory as
# RUNTIME because shared libraries need to sit next to the executables
# that load them via the project's $ORIGIN-relative RPATH strategy (see
# CMakeLists.txt).
set(RUNTIME_OUTPUT_SUBDIR "bin" CACHE STRING "Build-dir subdirectory for runtime artifacts (executables, .dll, .so)")
set(LIBRARY_OUTPUT_SUBDIR "bin" CACHE STRING "Build-dir subdirectory for shared-library artifacts")
set(ARCHIVE_OUTPUT_SUBDIR "lib" CACHE STRING "Build-dir subdirectory for static/import-library artifacts (.a, .lib)")
# =============================================================================
# .gitignore sync
#
# Keeps a marked block in .gitignore in sync with the actual, current
# values of the path variables above -- every configure, automatically.
# Nothing outside that block (e.g. the Visual Studio template this
# project's .gitignore starts from) is ever touched. This is what makes
# this file the real source of truth for these paths specifically: change
# OUT_DIR (or override it with -DOUT_DIR=...) and .gitignore updates on
# the next configure instead of silently drifting out of sync with what
# actually gets generated.
#
# If .gitignore doesn't exist yet, it's created with just this block. If
# it exists but has no markers yet, the block is appended. If the markers
# are already there, only the content between them is replaced. The file
# is only rewritten when the computed content actually differs, so a
# normal reconfigure doesn't spuriously touch it (or its mtime).
#
# A path is only added if it resolves inside the repo (CMAKE_SOURCE_DIR)
# -- e.g. an overridden TOOLCACHE_DIR pointing somewhere else entirely is
# silently skipped, since .gitignore has nothing meaningful to say about
# a location outside the repository it lives in.
#
# Skipped entirely in query mode -- see "How scripts read this file"
# above.
# =============================================================================
if(NOT PMN_CONFIG_QUERY_ONLY)
set(_pmn_gitignore_begin "# >>> Configuration.cmake generated paths -- BEGIN (do not edit by hand; edit Configuration.cmake and reconfigure) >>>")
set(_pmn_gitignore_end "# <<< Configuration.cmake generated paths -- END <<<")
function(_pmn_gitignore_relpath ABSOLUTE_VALUE OUT_VAR)
file(RELATIVE_PATH _pmn_rel "${CMAKE_SOURCE_DIR}" "${ABSOLUTE_VALUE}")
if(_pmn_rel MATCHES "^\\.\\.")
set(${OUT_VAR} "" PARENT_SCOPE)
else()
set(${OUT_VAR} "${_pmn_rel}" PARENT_SCOPE)
endif()
endfunction()
set(_pmn_gitignore_entries "")
foreach(_pmn_dir_var OUT_DIR BUILD_OUTPUT_DIR DIST_OUTPUT_DIR INSTALL_OUTPUT_DIR DOCS_OUTPUT_DIR TOOLCACHE_DIR)
_pmn_gitignore_relpath("${${_pmn_dir_var}}" _pmn_rel)
if(_pmn_rel)
list(APPEND _pmn_gitignore_entries "${_pmn_rel}/")
endif()
endforeach()
_pmn_gitignore_relpath("${COMPILE_COMMANDS_DESTINATION}" _pmn_cc_rel)
if(_pmn_cc_rel)
list(APPEND _pmn_gitignore_entries "${_pmn_cc_rel}")
endif()
list(REMOVE_DUPLICATES _pmn_gitignore_entries)
list(SORT _pmn_gitignore_entries)
set(_pmn_gitignore_block_content "")
foreach(_pmn_entry ${_pmn_gitignore_entries})
string(APPEND _pmn_gitignore_block_content "${_pmn_entry}\n")
endforeach()
set(_pmn_gitignore_path "${CMAKE_SOURCE_DIR}/.gitignore")
set(_pmn_gitignore_new_block "${_pmn_gitignore_begin}\n${_pmn_gitignore_block_content}${_pmn_gitignore_end}")
if(EXISTS "${_pmn_gitignore_path}")
file(READ "${_pmn_gitignore_path}" _pmn_gitignore_content)
else()
set(_pmn_gitignore_content "")
endif()
string(FIND "${_pmn_gitignore_content}" "${_pmn_gitignore_begin}" _pmn_gitignore_begin_pos)
string(FIND "${_pmn_gitignore_content}" "${_pmn_gitignore_end}" _pmn_gitignore_end_pos)
if(_pmn_gitignore_begin_pos GREATER -1 AND _pmn_gitignore_end_pos GREATER -1
AND _pmn_gitignore_end_pos GREATER _pmn_gitignore_begin_pos)
# Replace only the content between the existing markers.
string(SUBSTRING "${_pmn_gitignore_content}" 0 ${_pmn_gitignore_begin_pos} _pmn_gitignore_before)
string(LENGTH "${_pmn_gitignore_end}" _pmn_gitignore_end_marker_len)
math(EXPR _pmn_gitignore_after_start "${_pmn_gitignore_end_pos} + ${_pmn_gitignore_end_marker_len}")
string(SUBSTRING "${_pmn_gitignore_content}" ${_pmn_gitignore_after_start} -1 _pmn_gitignore_after)
set(_pmn_gitignore_final "${_pmn_gitignore_before}${_pmn_gitignore_new_block}${_pmn_gitignore_after}")
else()
# No markers yet -- append (trimming trailing blank lines first so we
# don't accumulate extra blank lines across repeated appends).
string(REGEX REPLACE "\n+$" "" _pmn_gitignore_trimmed "${_pmn_gitignore_content}")
if(_pmn_gitignore_trimmed STREQUAL "")
set(_pmn_gitignore_final "${_pmn_gitignore_new_block}\n")
else()
set(_pmn_gitignore_final "${_pmn_gitignore_trimmed}\n\n${_pmn_gitignore_new_block}\n")
endif()
endif()
if(NOT _pmn_gitignore_final STREQUAL _pmn_gitignore_content)
file(WRITE "${_pmn_gitignore_path}" "${_pmn_gitignore_final}")
endif()
endif()
+825
View File
@@ -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/<preset>/`, 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<NAME>=...` 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 `<platform>-<arch>-<compiler>` string that
picks the toolchain metadata (`PLATFORM`, `ARCHITECTURE`, `COMPILER_ID`)
used throughout the build and as the `cmake --install` destination
subdirectory (`out/install/<DIST_TARGET>/...`). Each value below has a
[toolchain file](#toolchain-files-and-cross-compiling) of the same name,
`cmake/toolchains/<DIST_TARGET>.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: `<triplet>-g++` (`apt install g++-aarch64-linux-gnu`, `g++-arm-linux-gnueabihf`), or `clang++ --target=<triplet>` 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 `<triplet>-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/<preset>`), 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=<value>` 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 <name>` 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 <name>] [BUILD_TYPE] [CMAKE_ARGS...]
scripts/build.sh --list-presets
scripts/build.sh --help
```
`--preset <name>` 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=<name>` also works; whichever form is used must come
first if given at all.
**One build directory per preset.** `--preset <name>` configures into
`out/build/<name>/`; 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/<preset>` builds
into `out/install/<preset>`.
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=<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 <name>` 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 <name>] <target-name> [-- program-args...]
```
Resolves a CMake target name to its built executable via the manifest
`_write_module_manifest()` generates at configure time
(`out/build/<preset>/module_manifest-<CONFIG>.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 <name>` 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 <name>] [BUILD_TYPE]
```
Installs the build made with `--preset <name>` — by default the one made
last, as for `run` — to `out/install/<DIST_TARGET>/{bin,lib}` (the same
name as the `out/build/<preset>` 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 <name>] [module-name]
```
Builds the `docs` target (every module) or `docs_<module-name>` (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/<name>/include
add_vendor_header_only(tiny_ansi)
# Vendored, with its own CMakeLists.txt, under vendor/<subdir>
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(<OPTION> <value> CACHE ...)` *before* the call;
[`source/Core/CMakeLists.txt`](source/Core/CMakeLists.txt) is the real,
commented instance of both, including two fmt-specific traps it
documents: why the pin is 12.1.0 rather than older (Clang ≥ 21 rejects
fmt < 11.1's own `FMT_STRING` macro) or newer (12.2.0 doesn't compile for
32-bit x86), and why `FMT_MODULE` is switched off (fmt 12 otherwise
builds a C++20 module library by default, which needs a module scanner
that not every Clang install has).
Then depend on whichever target the dependency defines, exactly like
depending on another module: `add_lib_module(my_lib DEPENDS fmt::fmt)`.
All three keep the dependency's warnings out of your build and its
sources out of `compile_commands.json`, and are safe to call more than
once for the same name if two modules need the same dependency.
## Documentation generation
Every module gets a Doxygen target automatically — nothing to add to
its own `CMakeLists.txt`. `scripts/docs.sh` / `docs.bat` build all of
them and print the landing page path; see
[scripts/docs.sh / docs.bat](#scriptsdocssh--docsbat) above. Doxygen
itself is fetched by `scripts/setup.sh` (pinned, not your system's, so
every machine's output matches) — documentation targets print a
pointer to that script instead of building until it has been run once. A
pinned Doxygen exists for x64 Linux, x64 Windows and macOS (Apple Silicon
and Intel); upstream publishes none for ARM Linux or ARM Windows, so
documentation isn't available on those machines (the project builds fine
without it).
Configure it with `-DENABLE_DOXYGEN=OFF`, `-DDOXYGEN_<TAG>=...` for any
[Doxyfile tag](https://www.doxygen.nl/manual/config.html), or opt a
single module out with `NO_DOCS` on its `add_*_module(...)` call. A
module's own `README.md`, if it has one, becomes that module's Doxygen
mainpage automatically.
## Code style & tooling
This project follows [`StyleGuide.md`](StyleGuide.md),
enforced by:
- **`.clang-format`** — formatting (indentation, brace style, line
length). Comments throughout cross-reference the style guide section
each rule comes from.
- **`.clang-tidy`** (and the stricter `c-api/.clang-tidy`, for a C-API
subtree if you add one) — naming conventions and a handful of
bug-prone patterns. Comments note what each check *can't* catch, so
those gaps stay visible instead of silently unenforced.
- **`.githooks/pre-commit`** — runs both tools against staged files
before every commit, installed automatically by `scripts/setup.sh` /
`setup.bat`. Neither tool is required to have this hook active: if a
binary isn't on `PATH`, the hook warns and skips that check instead
of blocking the commit. Only an actual formatting or lint issue from
a tool that *is* installed blocks anything.
- **`scripts/refactor.sh` / `refactor.bat`** — the auto-fix counterpart
to the hook's check-only behavior.
- **`.editorconfig`** / **`.gitattributes`** — editor-level whitespace
consistency and normalized line endings (`.bat` files are kept
`CRLF` deliberately; see the file for the rest).
## Output layout
Everything generated lands under `out/`, kept out of git by an
auto-synced block in `.gitignore` (regenerated from
`Configuration.cmake`'s actual path variables on every configure, so it
can't silently drift out of sync with what's really produced):
```
out/
build/<preset>/ One CMAKE_BINARY_DIR per preset (out/build/default/
when scripts/build.sh is run without one)
bin/ Executables, .dll/.so
lib/ Static libs, import libs
install/<DIST_TARGET>/{bin,lib} cmake --install destination -- named like the
out/build/<preset> it came from
docs/<module>/html/, index.html Generated Doxygen output
dist/ Reserved for packaged output (e.g. a future CPack integration)
.cache/tools/ Pinned Ninja + Doxygen (outside out/ on purpose --
`rm -rf out/` shouldn't force a re-download)
compile_commands.json Copied to the project root every build, for clangd/clang-tidy
```
+748
View File
@@ -0,0 +1,748 @@
# C/C++ Coding Style Guide
This guide is split into two parts:
- **[Part I: C++ Style](#part-i-c-style)** applies to all internal application/library code written in C++.
- **[Part II: C API Style](#part-ii-c-api-style)** applies specifically to public API surfaces exposed as plain C (`extern "C"`, `.h` headers) — e.g. for dynamic library boundaries, plugin interfaces, or C-consumer compatibility. Keep these two worlds separate: don't let C-API constraints (no exceptions, no classes, `snake_case`) leak into internal C++ code, and don't let C++ constructs leak into a public C API.
## Contents
**[Part I: C++ Style](#part-i-c-style)**
1. [Naming Conventions](#1-naming-conventions)
2. [Indentation](#2-indentation)
3. [Brace Placement (Allman Style)](#3-brace-placement-allman-style)
4. [Pointers and References](#4-pointers-and-references)
5. [Spacing](#5-spacing)
6. [Header Files](#6-header-files)
- [Namespaces](#namespaces)
- [Headers as Public API: Keep Implementation Types Out](#headers-as-public-api-keep-implementation-types-out)
- [Templates](#templates)
- [Implementing Templates (Project Standard)](#implementing-templates-project-standard)
- [Class Member Organization](#class-member-organization)
7. [Include Directive Order](#7-include-directive-order)
8. [Line Length](#8-line-length)
9. [Comments](#9-comments)
10. [Language Feature Do's and Don'ts](#10-language-feature-dos-and-donts)
- [Memory and Ownership](#memory-and-ownership)
- [Error Handling](#error-handling)
- [Modern C++ Features](#modern-c-features)
11. [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading)
- [Static Linking](#static-linking)
- [Dynamic Linking (Shared Libraries)](#dynamic-linking-shared-libraries)
- [Runtime Loading (dlopen and LoadLibrary)](#runtime-loading-dlopen-and-loadlibrary)
- [Summary](#summary)
12. [Miscellaneous](#12-miscellaneous)
**[Part II: C API Style](#part-ii-c-api-style)**
13. [Naming Conventions (C API)](#13-naming-conventions-c-api)
14. [File Extensions and Structure](#14-file-extensions-and-structure)
15. [Indentation and Braces](#15-indentation-and-braces)
16. [API Design Do's and Don'ts](#16-api-design-dos-and-donts)
17. [Boundary Rule of Thumb](#17-boundary-rule-of-thumb)
---
# Part I: C++ Style
## 1. Naming Conventions
| Symbol Type | Convention | Example |
|-------------------------|-------------------|-----------------------------------|
| Classes / Structs | `PascalCase` | `class NetworkManager;` |
| Functions / Methods | `camelCase` | `void connectToServer();` |
| Local Variables | `camelCase` | `int retryCount = 0;` |
| Member Variables (private/protected) | `camelCase` with `m_` prefix | `int m_connectionTimeout;` |
| Static Member Variables (class-scope `static`) | `camelCase` with `s_` prefix | `static int s_instanceCount;` |
| Global Variables (namespace/file scope, mutable) | `camelCase` with `g_` prefix | `int g_activeConnectionCount;` |
| Static Variables (function-local or file-scope `static`) | `camelCase` with `s_` prefix | `static int s_retryAttempts;` |
| Constants | `PascalCase` | `constexpr int MaxRetries = 5;` |
| Enum Type Names | `PascalCase` | `enum class ConnectionState` |
| Enum Values | `PascalCase` | `ConnectionState::Connected` |
| Namespaces | `snake_case` | `namespace network_utils` |
| Template Parameters | `PascalCase` | `template <typename ValueType>` |
| Macros | `ALL_CAPS_SNAKE_CASE` | `#define MAX_BUFFER_SIZE 1024` |
| File Names | `PascalCase` | `NetworkManager.cpp` / `.hpp` |
> **Note:** Macros are an exception to the general PascalCase/camelCase scheme since `ALL_CAPS` is the near-universal C/C++ convention and helps macros stand out visually from regular code.
> **Note:** `g_`/`s_` mark *mutable* global/static state specifically so it stands out at every use site — genuinely global mutable variables are rare and worth flagging by name. A `constexpr` or `const` global/static value is a **constant**, not a variable for this rule's purposes: it keeps the `PascalCase` constant convention above, with no `g_`/`s_` prefix, since it can't cause the aliasing/lifetime bugs the prefix exists to call out (e.g. `constexpr int MaxRetries = 5;` at namespace scope, not `g_MaxRetries`). A `static` variable inside a function keeps `s_` for the same reason as a file-scope one — both persist across calls and are exactly the kind of hidden state the prefix is meant to surface.
---
## 2. Indentation
- Use **tabs** for indentation (not spaces).
- Do not mix tabs and spaces.
- Configure your editor's tab width to display consistently (recommended: 4 columns wide) across the team.
- **Namespace bodies are indented**, exactly like any other block (`if`, `class`, `for`, ...) — a namespace is not a special case left flush against the margin. See [Namespaces](#namespaces) for the full rule and examples.
```cpp
void doSomething()
{
if (condition)
{
performAction();
}
}
```
---
## 3. Brace Placement (Allman Style)
Braces always go on their **own new line**, aligned with the statement that opens the block.
```cpp
// Correct (Allman)
if (isReady)
{
start();
}
else
{
waitForReady();
}
class Widget
{
public:
Widget();
~Widget();
};
void process()
{
for (int i = 0; i < count; ++i)
{
handle(i);
}
}
```
```cpp
// Incorrect (K&R style — do not use)
if (isReady) {
start();
}
```
---
## 4. Pointers and References
Attach `*` and `&` to the **type**, not the variable name.
```cpp
int* pointerToValue;
const std::string& nameRef;
void processData(const Data* data, int& outResult);
```
---
## 5. Spacing
- One space around binary operators: `a + b`, `x == y`.
- No space between a function name and its parentheses: `doWork()`, not `doWork ()`.
- One space after control-flow keywords: `if (`, `for (`, `while (`.
```cpp
if (a == b)
{
total = a + b * c;
}
```
---
## 6. Header Files
- Use `#pragma once` at the top of every header.
- **`.hpp` for C++ headers, `.h` for C headers.** Any header containing C++-only constructs (classes, templates, namespaces, overloads, `enum class`, etc.) uses `.hpp`. Reserve `.h` for headers meant to be valid C — see [Part II: C API Style](#part-ii-c-api-style) for the public C API convention.
- **One `.cpp` per `.hpp`.** Each header declaring a class or module has exactly one corresponding source file implementing it, sharing the same base name (`NetworkManager.hpp` ↔ `NetworkManager.cpp`). Avoid dumping multiple unrelated classes' implementations into one `.cpp`, and avoid splitting one class's implementation across multiple `.cpp` files.
- **One class, struct, or enum per header — meaning one *publicly usable* type.** Each `.hpp` exposes exactly one type as part of its public API: the primary class/struct/enum a caller `#include`s the header to use. A header should never declare two independent, separately-usable types (e.g. `NetworkManager` and an unrelated `RetryPolicy`) side by side — each gets its own `.hpp`. This is what prevents the dependency problem: a file that only needs `RetryPolicy` should never be forced to also compile/depend on everything `NetworkManager` pulls in. Small types that have no meaning or use *outside* the primary type — a nested `enum class` used only as one of its parameters, a tag type only passed to its constructor — aren't "separate" types for this rule; they're part of the primary type's interface and stay in the same header.
- Header-only components (templates, small inline utilities) are the exception to the one-`.cpp`-per-`.hpp` rule — they may have a `.hpp` with no matching `.cpp` — but the one-type-per-header rule still applies to them (see [Templates](#templates) below).
- Match header/source file names to the primary class (`PascalCase`), e.g. `NetworkManager.hpp` / `NetworkManager.cpp` for `class NetworkManager`.
- **Do not add a trailing comment on a closing brace** (`}; // class NetworkManager`, `} // namespace network_utils`, `} // if (isReady)`, etc.), for namespaces, classes, functions, or any other block. Keep blocks short and readable enough that the comment isn't needed — if a block is too long to tell what its closing brace belongs to at a glance, the fix is to shorten or restructure the block, not to annotate the brace.
```cpp
// NetworkManager.hpp
#pragma once
namespace network_utils
{
class NetworkManager
{
public:
NetworkManager();
~NetworkManager();
void connectToServer();
private:
int m_connectionTimeout;
};
}
```
### Namespaces
- **Namespace contents are indented**, exactly like the body of any other block. Don't leave everything inside a namespace flush against the left margin just because the namespace itself doesn't "do" anything at runtime — indentation is what makes nesting depth obvious at a glance, and a namespace is nesting.
- **Use the C++17 nested-namespace syntax (`namespace outer::inner { ... }`) instead of manually nesting namespace blocks**, whenever you're declaring a fixed, known multi-level namespace (e.g. `network_utils::detail`). Manually nesting `namespace network_utils { namespace detail { ... } }` for a case like this just adds an extra level of indentation and an extra closing brace for no benefit.
- The one case that still requires manual nesting: wrapping an **anonymous** namespace inside a named one (e.g. hiding a `.cpp`-local helper type inside `network_utils`) — anonymous namespaces have no name, so there's nothing to join with `::`.
```cpp
// Preferred — a fixed multi-level namespace uses the single nested-namespace form
namespace network_utils::detail
{
struct RetryState
{
int attemptCount;
int backoffMs;
};
}
```
```cpp
// Avoid — manually nesting two named namespaces that could be joined with ::
namespace network_utils
{
namespace detail
{
struct RetryState
{
int attemptCount;
int backoffMs;
};
}
}
```
### Headers as Public API: Keep Implementation Types Out
Treat every header as a **public interface contract**, not a scratchpad for whatever the implementation happens to need. A caller including `NetworkManager.hpp` should see only what they need to *use* `NetworkManager` — not the types that exist purely to help `NetworkManager.cpp` implement it internally.
- **Implementation-only helper types belong in the `.cpp`, not the `.hpp`.** If a class needs a small helper struct, enum, or class that callers never see or interact with (a parser's internal token type, a lookup-table entry, a strategy object used only inside one function), define it in the `.cpp` file, wrapped in an unnamed (anonymous) namespace so it has internal linkage and can't leak or collide across translation units:
```cpp
// NetworkManager.cpp
#include "NetworkManager.hpp"
namespace network_utils
{
namespace
{
// Implementation detail — never appears in the header, never used outside this file.
struct RetryState
{
int attemptCount;
int backoffMs;
};
}
void NetworkManager::connectToServer()
{
RetryState state{0, 100};
// ...
}
}
```
- **Forward-declare instead of including, whenever the header only needs a pointer or reference to another type.** This keeps the header's dependency footprint to exactly the API surface it exposes, rather than transitively dragging in every header its implementation happens to use.
```cpp
// NetworkManager.hpp
#pragma once
namespace network_utils
{
class Socket; // forward declaration — full definition only needed in NetworkManager.cpp
class NetworkManager
{
public:
void connectToServer();
private:
Socket* m_socket;
};
}
```
- **One real C++ constraint to know:** a class's *own* private member variables (as opposed to helper types) generally must still be declared in the header, because C++ needs the complete type — including all members — to know the class's size and layout wherever it's used by value, on the stack, or as a direct member of another class. This is different from hiding a *helper type*; you're not hiding the fact that `NetworkManager` has a timeout, only hiding the definition of an unrelated struct it happens to use internally.
- If you need to hide private *members* too — for a stable ABI across a shared-library boundary (see [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading)) or to shrink recompiles when private internals change — the standard technique is the **Pimpl idiom** (pointer to implementation): the header holds only a forward-declared pointer, and the real member variables live in a struct defined entirely in the `.cpp`.
```cpp
// NetworkManager.hpp
#pragma once
#include <memory>
namespace network_utils
{
class NetworkManager
{
public:
NetworkManager();
~NetworkManager();
void connectToServer();
private:
struct Impl; // forward-declared, defined only in NetworkManager.cpp
std::unique_ptr<Impl> m_impl;
};
}
```
```cpp
// NetworkManager.cpp
#include "NetworkManager.hpp"
namespace network_utils
{
struct NetworkManager::Impl
{
int connectionTimeout;
};
NetworkManager::NetworkManager() : m_impl(std::make_unique<Impl>()) {}
NetworkManager::~NetworkManager() = default;
void NetworkManager::connectToServer()
{
// use m_impl->connectionTimeout
}
}
```
Pimpl isn't required for every class — it adds an extra pointer indirection and a heap allocation, so reserve it for types where header-hiding genuinely matters (public library API, ABI-sensitive dynamic-library boundaries, or classes whose private internals churn often enough that reducing rebuild fan-out is worth the cost). For ordinary internal application classes, plain private members in the header (as in the example at the top of this section) are fine — only the *helper types*, not the members themselves, need to move out.
### Templates
Templates are *more* exposed to the one-type-per-header problem than ordinary classes, for two structural reasons specific to how templates compile:
1. **No compiled-`.cpp` firewall.** An ordinary class hides its implementation behind a `.cpp` — callers only need the declaration to link against it. A template has no such firewall: the compiler needs the *full definition* everywhere it's instantiated, so templates live entirely in headers (see the exception to "one `.cpp` per `.hpp`" in [Header Files](#6-header-files)). That means anything sharing a header with a template gets pulled into every translation unit that only wanted that one template.
2. **Parsing cost applies even to unused templates.** Bundling `template <typename T> class Cache` and `template <typename T> class Pool` in the same header means a file that only uses `Cache` still has to parse `Pool`'s full definition, and a change to `Pool` still bumps the header's timestamp — forcing a rebuild of every translation unit that includes it, including the ones that never touch `Pool` at all.
Given that, the rules:
- **One template class/function per header.** `Cache` and `Pool` each get their own `.hpp`, even if they're conceptually related utilities that usually get used together.
- **Keep a template's declaration and inline implementation in the same `.hpp`** — templates generally can't be split into a separate `.cpp` the way non-template code can (see reason 1 above) — but that's not license to merge *other*, unrelated templates into the same file alongside it.
- **If several templates genuinely share implementation details** (a common traits struct, a shared constant), factor *only that shared piece* into its own single-purpose header, rather than merging the templates themselves into one file. If the shared piece belongs in a nested namespace (e.g. an internal `detail` namespace not meant for outside use), declare it with the `::` nested-namespace syntax from [Namespaces](#namespaces).
- **Avoid including heavy headers** (e.g. `<iostream>`, large third-party headers) inside a widely-used template header purely for convenience — because template headers are included wherever they're instantiated, an unnecessary include here silently bloats compile times project-wide, multiplied across every instantiation site.
```cpp
// Avoid — two unrelated templates sharing one header
// Cache.hpp
#pragma once
namespace storage
{
template <typename Key, typename Value>
class Cache
{
public:
void insert(const Key& key, const Value& value);
const Value* find(const Key& key) const;
};
template <typename T>
class Pool
{
public:
T* acquire();
void release(T* item);
};
}
```
```cpp
// Preferred — one template per header
// Cache.hpp
#pragma once
namespace storage
{
template <typename Key, typename Value>
class Cache
{
public:
void insert(const Key& key, const Value& value);
const Value* find(const Key& key) const;
};
}
```
```cpp
// Pool.hpp — separate file, included only where Pool is actually needed
#pragma once
namespace storage
{
template <typename T>
class Pool
{
public:
T* acquire();
void release(T* item);
};
}
```
If `Cache` and `Pool` both rely on the same internal helper — say, a fixed-capacity ring buffer trait — that shared piece moves to its own header, in an internal namespace declared with `::`:
```cpp
// DetailTraits.hpp — included by both Cache.hpp and Pool.hpp, nothing else
#pragma once
namespace storage::detail
{
template <typename T, std::size_t Capacity>
struct RingBufferTraits
{
using ValueType = T;
static constexpr std::size_t capacity = Capacity;
};
}
```
#### Implementing Templates (Project Standard)
Templates are the one exception to this guide's header-declares/`.cpp`-implements split: the compiler needs a template's *complete definition* at every point it gets instantiated, and a separately-compiled `.cpp` can't supply that. So the implementation has to live in the header — but to keep this consistent across the codebase rather than a per-template judgment call, **this project uses one standard pattern**:
> **Declare template members in the class body, and define them out-of-line further down in the same header** — the same shape as an out-of-line member definition of a non-template class, just kept in the `.hpp` instead of moved to a `.cpp`.
```cpp
// Cache.hpp
#pragma once
namespace storage
{
template <typename Key, typename Value>
class Cache
{
public:
void insert(const Key& key, const Value& value);
const Value* find(const Key& key) const;
private:
std::unordered_map<Key, Value> m_entries;
};
template <typename Key, typename Value>
void Cache<Key, Value>::insert(const Key& key, const Value& value)
{
m_entries[key] = value;
}
template <typename Key, typename Value>
const Value* Cache<Key, Value>::find(const Key& key) const
{
auto it = m_entries.find(key);
return it != m_entries.end() ? &it->second : nullptr;
}
}
```
- **Exception:** a genuinely trivial one-line body (a plain getter, a one-expression forwarding call) can stay inline in the class, same as it would for a non-template class. Don't force it out-of-line just for consistency's sake.
- **Escape hatch, not a pattern:** if a template is only ever meant to be used with a small, fixed set of types, its definitions *can* move to a `.cpp` via explicit instantiation (`template class Cache<int, std::string>;`). This is a deliberate, rare trade-off — it turns a general-purpose template into one that only supports the explicitly instantiated types — and should be a documented exception with a stated reason (e.g. reducing header bloat for a widely-included, ABI-sensitive type), not the default way templates get written.
> **See also:** [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading) — the Pimpl idiom above exists mainly to satisfy the ABI constraints described there · [Part II: C API Style](#part-ii-c-api-style) — the `.h`/`.hpp` split introduced here is defined in full there.
### Class Member Organization
- **Member order: `public`, then `protected`, then `private`.** Group each access level together, in that order. The widest audience — everyone who includes the header — reads what's relevant to them (the public interface) first, without wading through implementation details to find it. Don't reopen an access specifier further down "for organization"; interleaving defeats the ordering rule's purpose.
- **Avoid inline member function definitions in the class body.** Declare the member in the class; define its body separately — out-of-line in the matching `.cpp` for an ordinary class (see the `NetworkManager` example in [Header Files](#6-header-files) above), or out-of-line further down the same header for a template member (see [Implementing Templates](#implementing-templates-project-standard) above). This keeps the class body itself a clean, scannable interface instead of a mix of signatures and implementation.
- **Exception:** a genuinely trivial one-line body (a plain getter, a one-expression forwarding call) may stay inline in the class — the same exception already noted for templates above.
- **Inline template members go at the bottom of their access section.** When a class mixes ordinary members with template members that use the trivial-one-liner exception just above, keep the plain declarations together at the top of each `public`/`protected`/`private` block, and place the inline template members after them, at the bottom of that same block — not interleaved. A reader scanning the interface sees the plain signatures first; the inline implementation detail comes last, where it doesn't interrupt that scan.
```cpp
class NetworkManager
{
public:
NetworkManager();
~NetworkManager();
void connectToServer();
bool isConnected() const;
// Inline template member (trivial-one-liner exception, see Templates
// above) -- kept at the bottom of this access section rather than
// interleaved with the plain declarations above.
template <typename T> T configValue() const { return static_cast<T>(m_connectionTimeout); }
protected:
virtual void onDisconnected();
private:
int m_connectionTimeout;
};
```
None of this is mechanically enforced — `readability-redundant-inline-specifier` (see `.clang-tidy`) catches one narrow related mistake (a redundant explicit `inline` keyword) but not the member-order or inline-body rules themselves.
---
## 7. Include Directive Order
Group includes into blocks, separated by a blank line, in the following order. Within each block, sort alphabetically.
1. **Corresponding header** — for a `.cpp`, its own `.hpp` comes first (catches missing-include bugs in the header itself).
2. **C system headers** — `<cstdio>`, `<cstdlib>`, etc.
3. **C++ standard library headers** — `<vector>`, `<memory>`, `<string>`, etc.
4. **Third-party / external library headers** — e.g. `<boost/...>`, `<fmt/...>`.
5. **Project headers** — other headers from this codebase, using quoted includes (`"..."`).
```cpp
// NetworkManager.cpp
#include "NetworkManager.hpp" // 1. corresponding header
#include <cstdint> // 2. C system headers
#include <memory> // 3. C++ standard library
#include <string>
#include <vector>
#include <fmt/format.h> // 4. third-party libraries
#include "Logger.hpp" // 5. project headers
#include "SocketWrapper.hpp"
```
- Use angle brackets `<...>` for system, standard library, and third-party headers.
- Use quotes `"..."` for project-local headers.
- This ordering guarantees each header can be compiled standalone (no hidden dependency on something included before it) and makes missing includes fail fast in CI rather than silently compiling due to include order in some other file.
---
## 8. Line Length
- Prefer a maximum of **100 characters** per line.
- **Do not wrap parameters onto their own line.** Keep a function's parameter list together — on the declaration line if it fits, otherwise packed as tightly as the line allows on a continuation line. Never lay parameters out one per line; that turns a short signature into a tall list a reader has to scroll past to see the return type and name.
- `clang-format` enforces the "never one-per-line" part. It can't force a genuinely long signature to fit within 100 characters without wrapping at all — that would need line length limits disabled project-wide, which would also stop ordinary long expressions and comments from ever wrapping, so it isn't done. If a signature is long enough that even packing parameters together still overflows, let it overflow rather than reach for one-per-line as the fallback.
---
## 9. Comments
- **Document symbols with Doxygen-style comments, never a plain `//`.** Any comment documenting a function, class, struct, enum, member variable, or macro — explaining what it *is* or *does*, as opposed to annotating a line of implementation — uses `///` or `/** ... */`. This is what makes the project's generated documentation (see the Doxygen integration) actually useful instead of a bare list of undocumented signatures.
- **`///` for most documentation; `/** ... */` for longer, multi-paragraph explanations.** Both are valid Doxygen syntax — pick based on length, not per-file preference.
- **Plain `//` is still correct for everything that isn't documenting a symbol** — a note on a tricky line inside a function body, a `// TODO`, a block disabled during debugging. The rule above is about comments attached to a declaration, not every comment everywhere.
```cpp
/// Establishes a connection to the configured server.
/// @return true if the connection succeeded.
bool connectToServer();
```
```cpp
/**
* Parses and validates a server configuration blob.
*
* Accepts either JSON or the legacy INI format; the format is
* auto-detected from the first non-whitespace byte.
*
* @param raw the raw configuration bytes, in either supported format.
* @return a parsed Config, or std::nullopt if raw is malformed.
*/
std::optional<Config> parseConfig(std::string_view raw);
```
---
## 10. Language Feature Do's and Don'ts
### Memory and Ownership
| Do | Don't |
|----|-------|
| Use `std::unique_ptr` / `std::shared_ptr` for owning dynamic objects | Use raw `new` / `delete` for ownership |
| Use raw pointers or references for **non-owning** access | Use a raw pointer to express or transfer ownership |
| Use `std::make_unique` / `std::make_shared` to construct smart pointers | Construct a smart pointer with `new` directly (`std::unique_ptr<T>(new T())`) |
| Follow RAII for all resources (files, locks, sockets, etc.) | Manually pair acquire/release calls |
### Error Handling
| Do | Don't |
|----|-------|
| Throw exceptions for **true exceptional situations** (unrecoverable errors, invariant violations, constructor failures) | Use exceptions for expected/recoverable control flow (e.g. "value not found") |
| Use return values (`bool`, `std::optional`, error codes, `std::expected` where available) for expected failure cases | Use exceptions as a substitute for normal branching logic |
| Catch exceptions by `const&` | Catch exceptions by value (causes slicing) |
| Keep destructors `noexcept` (the default) | Let exceptions escape a destructor |
### Modern C++ Features
| Do | Don't |
|----|-------|
| Use `auto` when it improves readability (iterators, lambdas, long template types) | Use `auto` when it hides an important or non-obvious type |
| Use range-based `for` loops over index-based loops when the index isn't needed | Write manual index loops just to iterate a container |
| Use lambdas for short, local callbacks and algorithm predicates | Write a named functor class for a one-off, trivial operation |
| Use structured bindings for multi-value returns (`auto [key, value] = ...`) | Unpack pairs/tuples manually with `.first` / `.second` when structured bindings are clearer |
| Use `constexpr` / `consteval` for compile-time constants and computations | Use `#define` macros for constants |
| Use `enum class` over plain `enum` | Use unscoped `enum` (pollutes namespace, weak typing) |
| Use `override` and `final` explicitly on overriding virtual functions | Omit `override`, relying on the compiler to catch signature mismatches |
| Use `nullptr` | Use `NULL` or `0` for pointers |
| Prefer `std::array` / `std::vector` over C-style arrays | Use raw C-style arrays for anything beyond fixed, tiny buffers |
| Pass small, cheap-to-copy types by value; pass large objects by `const&` | Pass large objects by value out of habit |
| Use `std::string_view` for read-only, non-owning string parameters | Take `const std::string&` when the caller may pass a string literal or substring |
---
## 11. Library Linking and Runtime Loading
How a component is built and linked shapes how strictly some of the above rules should be enforced.
### Static Linking
- Libraries are compiled directly into the final binary at build time.
- Header/implementation split still matters for compile-time isolation, but ABI stability across the boundary is **not** a concern — the whole binary is rebuilt together.
- Inline-heavy, template-heavy, and header-only code (common with `auto`, templates, `constexpr`) is safe here since everything resolves at compile time in one binary.
### Dynamic Linking (Shared Libraries)
- Applies to `.so` (Linux), `.dll` (Windows), and `.dylib` (macOS) targets.
- The public headers exposed across a shared-library boundary form an **ABI contract** — style choices here have real consequences:
- Avoid exposing `inline` functions, templates, or `constexpr` values in the public API surface if the library and its consumers may be compiled by different compilers/versions — inlined code bakes assumptions into the caller.
- Avoid changing the layout of any `class`/`struct` exposed across the boundary (adding/reordering member variables) without a version bump — it breaks binary compatibility for already-compiled consumers.
- Prefer exposing a stable, minimal interface at the boundary (see [Part II: C API Style](#part-ii-c-api-style)), and keep the `PascalCase` classes with full modern C++ usage as **internal implementation detail** behind it.
- Mark exported symbols explicitly (e.g. `__declspec(dllexport)` / `__attribute__((visibility("default")))`), and keep everything else hidden by default, to keep the ABI surface intentional and small.
### Runtime Loading (dlopen and LoadLibrary)
- Any function loaded dynamically at runtime **must** be declared with C linkage (`extern "C"`) to avoid name-mangling issues — this is exactly the case [Part II: C API Style](#part-ii-c-api-style) exists for, even in an otherwise all-`.hpp` C++ codebase.
- Keep the loaded interface as a plain function-pointer table or `extern "C"` API — no classes, no exceptions crossing the boundary (an exception thrown from a plugin loaded via `dlopen` and caught in the host app is undefined behavior across incompatible runtimes/compilers).
- Document plugin/module boundaries clearly with `.hpp` interface headers describing the contract, even though the actual crossing point is a plain C-style vtable or function table.
### Summary
| Context | Modern C++ features across the boundary | ABI stability required |
|---|---|---|
| Static link | Freely (templates, `auto`, inline, `constexpr`) | No — whole program rebuilt together |
| Dynamic link (`.so`/`.dll`) | Restrict at the public boundary; free internally | Yes — layout & symbol changes are breaking |
| Runtime load (`dlopen`) | `extern "C"` only at the boundary ([Part II](#part-ii-c-api-style) style) | Yes — plus no exceptions/classes across it |
> **See also:** [Boundary Rule of Thumb](#17-boundary-rule-of-thumb) — decides, file by file, whether a piece of code follows this section or Part II · [Part II: C API Style](#part-ii-c-api-style) — the actual convention referenced throughout this section.
---
## 12. Miscellaneous
- Mark single-argument constructors `explicit` unless implicit conversion is intended.
- Prefer `const` correctness everywhere it applies.
- **Leave a blank line after an early exit** — a `return`, `break`, or `continue` statement, or a block whose last statement is one of these — whenever more code follows at that same level. It visually marks where the early-exit path ends and the normal-case code begins. Skip the blank line if the statement (or its block) is the last thing in its own enclosing function or loop; there's nothing after it to separate from.
- Not enforced by `clang-format` or `clang-tidy` — neither has a check for this. It's a code-review convention.
```cpp
void process(const std::vector<int>& values)
{
if (values.empty())
{
return;
}
for (int value : values)
{
accumulate(value);
}
}
```
---
# Part II: C API Style
This section governs any code written as **plain C**, or any `extern "C"` surface exposed *from* a C++ library for consumption by C callers, dynamic loaders (`dlopen`), or other languages via FFI. Nothing in this section applies to internal C++ implementation — only to the boundary itself and to files that are genuinely compiled as C.
## 13. Naming Conventions (C API)
| Symbol Type | Convention | Example |
|-------------------------|----------------------|----------------------------------------|
| Public API functions | `snake_case` with library prefix | `net_manager_connect(NetManager* mgr);` |
| Types (`struct`/`enum`/typedef) | `PascalCase` with library prefix, `_t` suffix optional | `NetManagerT` or `NetManager` |
| Struct fields | `snake_case` | `int connection_timeout;` |
| Function parameters | `snake_case` | `int retry_count` |
| Constants / `#define` | `ALL_CAPS_SNAKE_CASE` with library prefix | `#define NET_MAX_RETRIES 5` |
| Enum values | `ALL_CAPS_SNAKE_CASE` with library + enum prefix | `NET_STATE_CONNECTED` |
| Internal global variables (translation-unit-visible, mutable) | `g_snake_case` | `int g_active_connection_count;` |
| Static variables (function-local or file-scope `static`) | `s_snake_case` | `static int s_retry_attempts;` |
| File names | `snake_case` | `net_manager.h` / `net_manager.c` |
- Every public symbol (function, type, macro, enum value) is **prefixed with the library/module name** to avoid collisions in the global C symbol namespace, since C has no namespaces.
- `snake_case` is used throughout the C API — this is a deliberate departure from the `camelCase`/`PascalCase` used in Part I, and is one of the clearest visual signals that a symbol belongs to the C boundary rather than internal C++.
- The `g_`/`s_` prefixes are for **internal, non-exported** state — the kind of file-scope `static` variable that never appears in a public header. A genuinely public global exported from the API surface is unusual enough that it should get the full library prefix instead (`net_manager_default_timeout`, not `g_net_manager_default_timeout`) — don't stack both prefixes on the same symbol.
---
## 14. File Extensions and Structure
- **`.h`** for all public C API headers — must be valid to include from both C and C++ translation units.
- **`.c`** for files actually compiled as C. If the "C API" is really a C++ implementation exposing an `extern "C"` shim, the shim itself lives in a `.cpp` file but declares its exported functions in a `.h` header guarded for C++ (see below).
- One `.h` per logical module/library surface (e.g. `net_manager.h`), mirroring the one-header-per-component rule from Part I, but scoped to the *public* API rather than one per internal class.
```c
/* net_manager.h */
#ifndef NET_MANAGER_H
#define NET_MANAGER_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct NetManager NetManager;
NetManager* net_manager_create(void);
void net_manager_destroy(NetManager* mgr);
int net_manager_connect(NetManager* mgr, const char* host, int port);
#ifdef __cplusplus
}
#endif
#endif /* NET_MANAGER_H */
```
- Use traditional `#ifndef`/`#define`/`#endif` header guards in `.h` files rather than `#pragma once` — `.h` files must remain portable to strict C compilers where `#pragma once` support is less universally guaranteed.
- Always wrap declarations in `extern "C" { ... }` guarded by `#ifdef __cplusplus`, so the same header works whether included from a C or a C++ translation unit.
---
## 15. Indentation and Braces
Match Part I for consistency across the codebase: **tabs** for indentation, **Allman** brace placement.
```c
int net_manager_connect(NetManager* mgr, const char* host, int port)
{
if (mgr == NULL)
{
return NET_ERROR_INVALID_ARGUMENT;
}
return connect_impl(mgr, host, port);
}
```
---
## 16. API Design Do's and Don'ts
| Do | Don't |
|----|-------|
| Return an integer error code / status enum from every fallible function | Throw or let a C++ exception cross into `extern "C"` code (undefined behavior for C callers) |
| Use opaque pointers (`typedef struct NetManager NetManager;` with the definition hidden in the `.c`/`.cpp`) to hide implementation details and preserve ABI stability | Expose full `struct` layouts in the public header if the layout may change — this breaks binary compatibility |
| Provide explicit `_create` / `_destroy` (or `_init` / `_free`) pairs for any type owning resources | Rely on constructors/destructors (C has none) or leave lifetime management implicit |
| Take a `void* user_data` parameter on any callback-accepting function | Assume the caller can supply capturing closures — plain C function pointers can't capture state |
| Check all pointer parameters for `NULL` before dereferencing at public entry points | Assume public API callers always pass valid pointers |
| Keep the header C89/C99-compatible (no `//` comments, no variable declarations mid-block if targeting strict C89) unless the project's minimum C standard is confirmed higher | Use C++-only syntax anywhere in a `.h` that must remain includable from C |
| Document ownership transfer explicitly in comments (e.g. "caller owns the returned pointer and must call `net_manager_destroy`") | Leave ownership of returned pointers ambiguous |
---
## 17. Boundary Rule of Thumb
- If a file is `.cpp`/`.hpp` and never crosses a dynamic-library, plugin, or FFI boundary: **use [Part I](#part-i-c-style), full stop.**
- If a file declares or implements symbols meant to be called from C, another language, or loaded via `dlopen`/`LoadLibrary`: **use [Part II](#part-ii-c-api-style)** for that surface, even if the implementation behind it is C++.
- Never mix the two conventions within the same file — a `.h` should never contain `camelCase` C++-flavored declarations, and a `.hpp` should never contain `snake_case` C-flavored ones. Where a C++ class needs a C API, write a thin `extern "C"` shim (Part II style) that internally calls into the real C++ implementation (Part I style).
> **See also:** [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading) — the dynamic-library and `dlopen` boundaries that make this rule necessary in the first place.
+57
View File
@@ -0,0 +1,57 @@
---
# c-api/.clang-tidy
# Place this file at the root of whatever directory holds your Part II
# (C API) surface — e.g. c-api/, public-include/, or wherever your extern "C"
# headers and shims live. clang-tidy always uses the closest .clang-tidy file
# up the directory tree, so files under this directory pick up these
# snake_case naming rules instead of the PascalCase/camelCase rules in the
# repo-root .clang-tidy, without any extra flags or per-file configuration.
#
# Formatting (tabs, Allman braces, line length) still comes from the single
# root-level .clang-format — only naming differs between Part I and Part II,
# per style guide §15 (Indentation and Braces).
InheritParentConfig: true
Checks: >
readability-identifier-naming,
bugprone-exception-escape
CheckOptions:
# --- C API naming — style guide §13 ---
# Every public symbol uses snake_case with a library/module prefix
# (e.g. net_manager_connect, NET_MAX_RETRIES). clang-tidy can enforce the
# casing; the prefix itself is a code-review convention.
- key: readability-identifier-naming.FunctionCase
value: lower_case
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: lower_case
- key: readability-identifier-naming.LocalVariableCase
value: lower_case
- key: readability-identifier-naming.StructCase
value: CamelCase
- key: readability-identifier-naming.EnumCase
value: CamelCase
# Enum values — ALL_CAPS_SNAKE_CASE with a library + enum prefix
# (e.g. NET_STATE_CONNECTED).
- key: readability-identifier-naming.EnumConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.GlobalConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.MacroDefinitionCase
value: UPPER_CASE
# Internal (non-exported) global/static variables — style guide §13's
# g_/s_ rows. Only applies to mutable state; GlobalConstantCase above
# still governs const globals, which stay UPPER_CASE with no prefix.
- key: readability-identifier-naming.GlobalVariableCase
value: lower_case
- key: readability-identifier-naming.GlobalVariablePrefix
value: g_
- key: readability-identifier-naming.StaticVariableCase
value: lower_case
- key: readability-identifier-naming.StaticVariablePrefix
value: s_
+126
View File
@@ -0,0 +1,126 @@
# =============================================================================
# BuildDir.cmake
#
# Script-mode bridge, the same idea as PrintConfig.cmake: tells
# scripts/*.sh and scripts/*.bat which build directory to work on, so that
# logic exists once here instead of twice in two shell dialects.
#
# Every preset builds in its own directory, out/build/<name> (see
# BUILD_OUTPUT_DIR in Configuration.cmake), which is what lets you switch
# presets without one's cache clobbering another's. The scripts have to
# pick one of them:
#
# build.* knows the name already (--preset <name>, else
# "default") and only needs the path. After a
# successful configure it also records the choice as
# the one built last.
# run.* install.* take an optional --preset <name>; without it they use
# docs.* the directory that was built last, or the only one
# that exists, or say what to do if that's ambiguous.
#
# Usage:
# cmake [-DPMN_NAME=<name>] [-DPMN_RECORD=ON] -P cmake/BuildDir.cmake
#
# PMN_NAME a preset name, or "default". Given: the answer is
# BUILD_OUTPUT_DIR/<name>, whether or not it exists yet (build
# is about to create it). Not given: discover one -- the one
# built last, else the only one, else an error.
# PMN_RECORD also remember PMN_NAME as the one built last (a one-line
# file, out/build/.last).
#
# Prints exactly one line, via message(STATUS ...) for the reason
# PrintConfig.cmake documents (STATUS is the one message() mode CMake sends
# to stdout, at the cost of a "-- " prefix every caller strips):
#
# -- BUILD_DIR=<path> relative to the project root with forward slashes
# when it's inside the project (the normal case),
# absolute otherwise
# -- ERROR=<text> what to tell the user
#
# The exit status is 0 either way -- callers key on which line they got. The
# error text sticks to plain words and avoids the characters cmd.exe treats
# specially (& | < > ^ % ! and parentheses), so a .bat can echo it back
# verbatim.
# =============================================================================
set(PMN_CONFIG_QUERY_ONLY TRUE)
get_filename_component(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
include("${CMAKE_SOURCE_DIR}/Configuration.cmake")
# <path> shown relative to the project root when it's inside it.
function(_pmn_display_path ABSOLUTE_PATH OUT_VAR)
file(RELATIVE_PATH _pmn_rel "${CMAKE_SOURCE_DIR}" "${ABSOLUTE_PATH}")
if(_pmn_rel MATCHES "^\\.\\.")
set(${OUT_VAR} "${ABSOLUTE_PATH}" PARENT_SCOPE)
else()
set(${OUT_VAR} "${_pmn_rel}" PARENT_SCOPE)
endif()
endfunction()
set(_pmn_root "${BUILD_OUTPUT_DIR}")
_pmn_display_path("${_pmn_root}" _pmn_root_shown)
set(_pmn_key "")
set(_pmn_value "")
if(NOT "${PMN_NAME}" STREQUAL "")
# ---- A name was given: the answer is just the path. ----------------------
if(PMN_NAME MATCHES "[/\\\\]" OR PMN_NAME STREQUAL "." OR PMN_NAME STREQUAL "..")
set(_pmn_key "ERROR")
set(_pmn_value "'${PMN_NAME}' is not a valid preset name: it must be a single directory name, not a path.")
else()
_pmn_display_path("${_pmn_root}/${PMN_NAME}" _pmn_shown)
set(_pmn_key "BUILD_DIR")
set(_pmn_value "${_pmn_shown}")
if(PMN_RECORD)
file(MAKE_DIRECTORY "${_pmn_root}")
file(WRITE "${_pmn_root}/.last" "${PMN_NAME}\n")
endif()
endif()
else()
# ---- No name: discover one. ---------------------------------------------
# Candidates are the subdirectories that hold a CMake cache.
set(_pmn_candidates "")
file(GLOB _pmn_children LIST_DIRECTORIES true "${_pmn_root}/*")
foreach(_pmn_child IN LISTS _pmn_children)
if(IS_DIRECTORY "${_pmn_child}" AND EXISTS "${_pmn_child}/CMakeCache.txt")
get_filename_component(_pmn_child_name "${_pmn_child}" NAME)
list(APPEND _pmn_candidates "${_pmn_child_name}")
endif()
endforeach()
list(SORT _pmn_candidates)
list(LENGTH _pmn_candidates _pmn_count)
# The one built last, if that pointer still names a real build.
set(_pmn_last "")
if(EXISTS "${_pmn_root}/.last")
file(STRINGS "${_pmn_root}/.last" _pmn_last_lines LIMIT_COUNT 1)
string(STRIP "${_pmn_last_lines}" _pmn_last)
endif()
set(_pmn_chosen "")
if(NOT "${_pmn_last}" STREQUAL "" AND "${_pmn_last}" IN_LIST _pmn_candidates)
set(_pmn_chosen "${_pmn_last}")
elseif(_pmn_count EQUAL 1)
set(_pmn_chosen "${_pmn_candidates}")
endif()
if(NOT "${_pmn_chosen}" STREQUAL "")
_pmn_display_path("${_pmn_root}/${_pmn_chosen}" _pmn_shown)
set(_pmn_key "BUILD_DIR")
set(_pmn_value "${_pmn_shown}")
elseif(_pmn_count EQUAL 0)
set(_pmn_key "ERROR")
if(EXISTS "${_pmn_root}/CMakeCache.txt")
set(_pmn_value "${_pmn_root_shown} holds a build from the old single-directory layout, which this version no longer uses. Delete it, then run scripts/build to create one build directory per preset.")
else()
set(_pmn_value "No build found under ${_pmn_root_shown}. Run scripts/build first, optionally with --preset NAME.")
endif()
else()
list(JOIN _pmn_candidates ", " _pmn_names)
set(_pmn_key "ERROR")
set(_pmn_value "Several builds exist under ${_pmn_root_shown} and none was built last through the scripts: ${_pmn_names}. Pass --preset NAME to choose one.")
endif()
endif()
message(STATUS "${_pmn_key}=${_pmn_value}")
+407
View File
@@ -0,0 +1,407 @@
# =============================================================================
# BuildHelpers.cmake
#
# Reusable, project-agnostic build machinery: module discovery, per-target
# compiler/RPATH settings, and the module-registration macros (add_lib_module,
# add_exe_module, add_test_module, add_example_module) that a module's own
# CMakeLists.txt calls.
#
# This file is meant to change rarely and hold nothing project-specific.
# New targets should not need edits here, or in the root CMakeLists.txt --
# see the "Module discovery" section of CMakeLists.txt and the per-module
# CMakeLists.txt convention described there.
#
# Expects the including CMakeLists.txt to have already set (from project()
# and the project-identity block): PROJECT_NAME, PROJECT_VERSION,
# PROJECT_CODE_NAME. Multiplatform: MSVC/WIN32 branches guard the
# Windows-only paths; everything else is generic.
# =============================================================================
include_guard(GLOBAL)
# -----------------------------------------------------------------------
# all_tests / all_examples -- aggregate targets so groups can be built
# without CMake target-name collisions:
# cmake --build . --target all_tests
# cmake --build . --target all_examples
#
# enable_testing() is required for add_test_module()'s add_test() calls to
# actually be discoverable by ctest -- without it, tests build fine but
# `ctest` reports "No tests were found" with no error anywhere else.
# -----------------------------------------------------------------------
enable_testing()
add_custom_target(all_tests COMMENT "Build all test targets")
add_custom_target(all_examples COMMENT "Build all example targets")
# -----------------------------------------------------------------------
# _discover_module_subdirectories(ROOT_DIR): add_subdirectory() every
# immediate child of ROOT_DIR that contains its own CMakeLists.txt.
#
# This is the whole mechanism behind "no root edits to add a module": drop
# a new directory with a one-line CMakeLists.txt under one of the known
# module roots (source/ for libraries and apps, source/tests/ or
# source/examples/ for tests and examples) and it is picked up automatically.
# CONFIGURE_DEPENDS means the *next build* reconfigures on its own after a
# module directory is added or removed -- no manual `cmake` re-run needed.
#
# Order between add_subdirectory() calls does not matter for correctness:
# CMake resolves target_link_libraries() names against the whole project
# at generate time, not sequentially, so a module may DEPENDS on a module
# discovered later (verified empirically -- this isn't a guess).
#
# A macro (not a function) so add_subdirectory() behaves exactly as if it
# were called directly from the caller -- no extra scope layer involved.
# -----------------------------------------------------------------------
macro(_discover_module_subdirectories ROOT_DIR)
if(IS_DIRECTORY "${ROOT_DIR}")
file(GLOB _pmn_module_candidates CONFIGURE_DEPENDS LIST_DIRECTORIES true "${ROOT_DIR}/*")
foreach(_pmn_candidate ${_pmn_module_candidates})
if(IS_DIRECTORY "${_pmn_candidate}" AND EXISTS "${_pmn_candidate}/CMakeLists.txt")
add_subdirectory("${_pmn_candidate}")
endif()
endforeach()
endif()
endmacro()
# -----------------------------------------------------------------------
# _apply_compile_options: per-target warning flags.
# MSVC path : /W4 (high warnings) + /permissive- (strict conformance).
# GCC/Clang : -Wall -Wextra -Wpedantic.
# -----------------------------------------------------------------------
function(_apply_compile_options TARGET)
if(MSVC)
target_compile_options(${TARGET} PRIVATE /W4 /permissive-)
else()
target_compile_options(${TARGET} PRIVATE -Wall -Wextra -Wpedantic)
endif()
endfunction()
# -----------------------------------------------------------------------
# _apply_rpath: RPATH is an ELF (Linux) and Mach-O (macOS) concept; skip
# entirely on Windows. Callers spell "relative to this binary" the ELF way,
# $ORIGIN[/..]; Mach-O's loader has never heard of that token -- its
# equivalent is @loader_path -- so it is translated here, once, instead of
# every call site having to know which platform it's on. (For an executable
# @loader_path is the executable's own directory; for a library, the
# library's. That is exactly the layout this project uses: everything
# lands flat in bin/, tests and examples one level down in bin/tests and
# bin/examples.)
# -----------------------------------------------------------------------
function(_apply_rpath TARGET RPATH_VAL)
if(NOT WIN32)
if(APPLE)
string(REPLACE "$ORIGIN" "@loader_path" RPATH_VAL "${RPATH_VAL}")
endif()
set_target_properties(${TARGET} PROPERTIES
BUILD_RPATH "${RPATH_VAL}"
INSTALL_RPATH "${RPATH_VAL}"
)
endif()
endfunction()
# -----------------------------------------------------------------------
# _set_output_dirs(TARGET RUNTIME_DIR [LIBRARY_DIR]):
#
# CMakeLists.txt sets CMAKE_RUNTIME_OUTPUT_DIRECTORY_<CONFIG> /
# CMAKE_LIBRARY_OUTPUT_DIRECTORY_<CONFIG> globally (once, near the top) so
# every target defaults to the flat bin/ layout. That also means every
# new target's per-config RUNTIME_OUTPUT_DIRECTORY_<CONFIG> /
# LIBRARY_OUTPUT_DIRECTORY_<CONFIG> property is pre-populated at creation
# time -- and a per-config property always wins over the plain
# (config-independent) one. So a module that needs a *different* output
# directory (tests -> bin/tests, examples -> bin/examples) must override
# the per-config properties too, or the override silently has no effect.
# This helper does both in one place.
# -----------------------------------------------------------------------
function(_set_output_dirs TARGET RUNTIME_DIR)
set(_library_dir "${RUNTIME_DIR}")
if(ARGC GREATER 2)
set(_library_dir "${ARGV2}")
endif()
set_target_properties(${TARGET} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${RUNTIME_DIR}"
LIBRARY_OUTPUT_DIRECTORY "${_library_dir}"
)
foreach(_cfg Debug Release RelWithDebInfo MinSizeRel)
string(TOUPPER "${_cfg}" _CFG)
set_target_properties(${TARGET} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${_CFG} "${RUNTIME_DIR}"
LIBRARY_OUTPUT_DIRECTORY_${_CFG} "${_library_dir}"
)
endforeach()
endfunction()
# -----------------------------------------------------------------------
# _register_executable_target(TARGET): tracks every executable module
# (exe/test/example) in a global property so _write_module_manifest()
# can find them all without anything needing to know their names ahead
# of time.
# -----------------------------------------------------------------------
function(_register_executable_target TARGET)
set_property(GLOBAL APPEND PROPERTY PMN_EXECUTABLE_TARGETS ${TARGET})
endfunction()
# -----------------------------------------------------------------------
# _register_module_target(CATEGORY TARGET): tracks a target under
# CATEGORY (PRIMARY, TEST, or EXAMPLE) in a global property so
# CMakeLists.txt's install() rules can be generated from whatever modules
# actually got discovered, instead of a hardcoded target list that would
# need editing every time a module is added or removed.
# -----------------------------------------------------------------------
function(_register_module_target CATEGORY TARGET)
set_property(GLOBAL APPEND PROPERTY PMN_MODULE_TARGETS_${CATEGORY} ${TARGET})
endfunction()
# -----------------------------------------------------------------------
# _write_module_manifest(OUTPUT_DIR): call once, after every module has
# been discovered, to emit "<target-name>=<absolute-path-to-binary>"
# lines for every executable module into
# OUTPUT_DIR/module_manifest-<CONFIG>.txt.
#
# scripts/run.sh / run.bat read this file to resolve a CMake target name to its
# actual executable path -- so they never hardcode per-module paths and
# never need editing when a module is added, removed, or renamed.
# $<CONFIG> is part of the filename because file(GENERATE) requires that
# for content (like $<TARGET_FILE:...>) that can differ per build
# configuration under a multi-config generator (e.g. Ninja Multi-Config).
# -----------------------------------------------------------------------
function(_write_module_manifest OUTPUT_DIR)
get_property(_pmn_targets GLOBAL PROPERTY PMN_EXECUTABLE_TARGETS)
set(_manifest_content "")
foreach(_t ${_pmn_targets})
string(APPEND _manifest_content "${_t}=$<TARGET_FILE:${_t}>\n")
endforeach()
file(GENERATE
OUTPUT "${OUTPUT_DIR}/module_manifest-$<CONFIG>.txt"
CONTENT "${_manifest_content}"
)
endfunction()
# -----------------------------------------------------------------------
# _finalize_module_target(TARGET MODULE_NAME [OUTPUT_NAME name] [DEPENDS ...])
#
# The tail every module macro below shares: identity compile definitions,
# warning flags, OUTPUT_NAME, and DEPENDS linking. Centralized so all four
# macros stay in lockstep -- edit once here instead of four times.
# Re-parses the caller's raw ARGN, so it's safe to call as
# _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) even though
# ARGN also contains keywords (TYPE, EXTRA_SOURCES, ...) this function
# doesn't know about -- those are simply ignored.
# -----------------------------------------------------------------------
# -----------------------------------------------------------------------
# _finalize_module_target(TARGET MODULE_NAME [OUTPUT_NAME name] [DEPENDS ...]
# [NO_DOCS])
#
# The tail every module macro below shares: identity compile definitions,
# warning flags, OUTPUT_NAME, DEPENDS linking, and an automatic per-module
# Doxygen target (see cmake/Documentation.cmake) unless NO_DOCS is given.
# Centralized so all four macros stay in lockstep -- edit once here
# instead of four times. Re-parses the caller's raw ARGN, so it's safe to
# call as _finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN}) even
# though ARGN also contains keywords (TYPE, EXTRA_SOURCES, ...) this
# function doesn't know about -- those are simply ignored.
# -----------------------------------------------------------------------
function(_finalize_module_target TARGET MODULE_NAME)
cmake_parse_arguments(FIN "NO_DOCS" "OUTPUT_NAME" "DEPENDS" ${ARGN})
target_compile_definitions(${TARGET} PRIVATE
$<$<CONFIG:Debug>:DEBUG>
MODULE_NAME="${MODULE_NAME}"
PROJECT_NAME="${PROJECT_NAME}"
PROJECT_NAME_CODE="${PROJECT_CODE_NAME}"
PROJECT_VER="${PROJECT_VERSION}"
)
_apply_compile_options(${TARGET})
if(FIN_OUTPUT_NAME)
set_target_properties(${TARGET} PROPERTIES OUTPUT_NAME ${FIN_OUTPUT_NAME})
endif()
if(FIN_DEPENDS)
target_link_libraries(${TARGET} PRIVATE ${FIN_DEPENDS})
endif()
if(NOT FIN_NO_DOCS)
_add_module_docs(${TARGET})
endif()
endfunction()
# =============================================================================
# add_lib_module(NAME [TYPE STATIC|SHARED] [OUTPUT_NAME name] [DEPENDS ...]
# [EXTRA_SOURCES ...] [NO_DOCS])
#
# Called from a library module's own CMakeLists.txt (e.g. source/Core/CMakeLists.txt).
# Sources/headers are globbed relative to that module's own directory:
# src/*.cpp and include/*.h(pp) underneath it. TYPE picks STATIC or SHARED
# for this module only; omit it to use the project-wide default
# (BUILD_SHARED_LIBS -- set in CMakeLists.txt's OPTIONS section, or pass
# -DBUILD_SHARED_LIBS=ON at configure time).
#
# Headers are globbed in purely so they show up in IDE project trees --
# CMake never compiles headers, so this is inert for the actual build.
# =============================================================================
macro(add_lib_module NAME)
cmake_parse_arguments(ARG "" "OUTPUT_NAME;TYPE" "DEPENDS;EXTRA_SOURCES" ${ARGN})
if(ARG_OUTPUT_NAME)
set(_MODULE_NAME ${ARG_OUTPUT_NAME})
else()
set(_MODULE_NAME ${NAME})
endif()
if(ARG_TYPE)
set(_LIB_TYPE ${ARG_TYPE})
elseif(BUILD_SHARED_LIBS)
set(_LIB_TYPE SHARED)
else()
set(_LIB_TYPE STATIC)
endif()
if(NOT _LIB_TYPE MATCHES "^(STATIC|SHARED)$")
message(FATAL_ERROR "add_lib_module(${NAME}): TYPE must be STATIC or SHARED, got '${_LIB_TYPE}'")
endif()
file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS src/*.cpp)
file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp)
add_library(${NAME} ${_LIB_TYPE} ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES})
target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
if(_LIB_TYPE STREQUAL "STATIC")
# Keep static libs relocatable in case they end up linked into a
# shared library or PIE executable later.
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
else()
# LIBRARY_OUTPUT_DIRECTORY controls .so placement on Linux.
# RUNTIME_OUTPUT_DIRECTORY controls .dll placement on Windows.
# WINDOWS_EXPORT_ALL_SYMBOLS auto-generates a .def file so all
# public symbols are exported without __declspec(dllexport).
# Remove if you prefer explicit visibility macros instead.
_set_output_dirs(${NAME} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}" "${CMAKE_BINARY_DIR}/${LIBRARY_OUTPUT_SUBDIR}")
set_target_properties(${NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
# MinGW builds keep this target dynamically linked to
# libgcc/libstdc++ (see CMakeLists.txt's "MinGW runtime linking"
# comment for why); copy those DLLs next to it in the build tree
# so it runs without a manual step. install()-time placement is
# the matching install(FILES ...) in CMakeLists.txt's "install"
# section -- PMN_MINGW_RUNTIME_DLLS is empty on every other
# platform, so this is a no-op there.
if(MINGW AND PMN_MINGW_RUNTIME_DLLS)
add_custom_command(TARGET ${NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PMN_MINGW_RUNTIME_DLLS} "$<TARGET_FILE_DIR:${NAME}>"
COMMENT "Copying MinGW runtime DLLs next to ${NAME}"
VERBATIM
)
endif()
endif()
_register_module_target(PRIMARY ${NAME})
_finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN})
endmacro()
# Back-compat wrappers for the old two-macro spelling.
# Prefer add_lib_module(... TYPE STATIC|SHARED) directly in new code.
macro(add_static_lib_module NAME)
add_lib_module(${NAME} TYPE STATIC ${ARGN})
endmacro()
macro(add_shared_lib_module NAME)
add_lib_module(${NAME} TYPE SHARED ${ARGN})
endmacro()
# =============================================================================
# add_exe_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS])
#
# Called from an app module's own CMakeLists.txt (e.g. HelloApp/CMakeLists.txt).
# Sources/headers are globbed relative to that module's own directory:
# *.cpp and include/*.h(pp) underneath it.
# =============================================================================
macro(add_exe_module NAME)
cmake_parse_arguments(ARG "" "OUTPUT_NAME" "DEPENDS;EXTRA_SOURCES" ${ARGN})
if(ARG_OUTPUT_NAME)
set(_MODULE_NAME ${ARG_OUTPUT_NAME})
else()
set(_MODULE_NAME ${NAME})
endif()
file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS *.cpp)
file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp)
add_executable(${NAME} ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES})
target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
_set_output_dirs(${NAME} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}")
_apply_rpath(${NAME} "$ORIGIN")
_register_module_target(PRIMARY ${NAME})
_register_executable_target(${NAME})
_finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN})
endmacro()
# =============================================================================
# add_test_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS])
#
# Called from a test module's own CMakeLists.txt (e.g. source/tests/mytest/CMakeLists.txt).
# Lands in bin/tests/, so shared-lib deps one level up need $ORIGIN/...
# EXCLUDE_FROM_ALL keeps it out of the default build; BUILD_TESTS opts it
# back in.
# =============================================================================
macro(add_test_module NAME)
cmake_parse_arguments(ARG "" "OUTPUT_NAME" "DEPENDS;EXTRA_SOURCES" ${ARGN})
if(ARG_OUTPUT_NAME)
set(_MODULE_NAME ${ARG_OUTPUT_NAME})
else()
set(_MODULE_NAME ${NAME})
endif()
file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS *.cpp)
file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp)
if(BUILD_TESTS)
add_executable(${NAME} ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES})
else()
add_executable(${NAME} EXCLUDE_FROM_ALL ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES})
endif()
target_include_directories(${NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
_set_output_dirs(${NAME} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/tests")
_apply_rpath(${NAME} "$ORIGIN/..")
_register_module_target(TEST ${NAME})
_register_executable_target(${NAME})
_finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN})
add_test(NAME ${NAME} COMMAND ${NAME})
add_dependencies(all_tests ${NAME})
endmacro()
# =============================================================================
# add_example_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS])
#
# Called from an example module's own CMakeLists.txt (e.g. source/examples/myexample/CMakeLists.txt).
# Same bin/examples/ + $ORIGIN/.. layout as tests.
# =============================================================================
macro(add_example_module NAME)
cmake_parse_arguments(ARG "" "OUTPUT_NAME" "DEPENDS;EXTRA_SOURCES" ${ARGN})
if(ARG_OUTPUT_NAME)
set(_MODULE_NAME ${ARG_OUTPUT_NAME})
else()
set(_MODULE_NAME ${NAME})
endif()
file(GLOB_RECURSE ${NAME}_SOURCES CONFIGURE_DEPENDS *.cpp)
file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp)
if(BUILD_EXAMPLES)
add_executable(${NAME} ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES})
else()
add_executable(${NAME} EXCLUDE_FROM_ALL ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES})
endif()
target_include_directories(${NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
_set_output_dirs(${NAME} "${CMAKE_BINARY_DIR}/${RUNTIME_OUTPUT_SUBDIR}/examples")
_apply_rpath(${NAME} "$ORIGIN/..")
_register_module_target(EXAMPLE ${NAME})
_register_executable_target(${NAME})
_finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN})
add_dependencies(all_examples ${NAME})
endmacro()
+134
View File
@@ -0,0 +1,134 @@
# =============================================================================
# Dependencies.cmake
#
# Three ways to bring in a third-party dependency, and one helper that ties
# two of them together. All three keep the dependency's own warnings out of
# your build (SYSTEM) and its sources out of compile_commands.json, so
# clangd/clang-tidy stay focused on code this project owns.
#
# add_vendor_header_only(NAME [INCLUDE_DIR <dir>])
# A buildless (header-only) drop checked into vendor/<NAME>/include
# (or vendor/<INCLUDE_DIR>). Wraps it as an INTERFACE target NAME.
#
# add_vendor_subdirectory(SUBDIR)
# A vendored dependency that ships its own CMakeLists.txt, checked
# into vendor/<SUBDIR> (a plain source drop, git submodule, or git
# subtree). Whatever target names that CMakeLists.txt defines are
# what you DEPENDS on.
#
# add_dependency(NAME <FetchContent_Declare args...>)
# CMake's native find_package + FetchContent integration: pass
# FIND_PACKAGE_ARGS ... to try an already-installed copy (an apt/
# vcpkg/conan package) first, and only download + build from source
# if nothing is found. Omit FIND_PACKAGE_ARGS to always fetch.
# https://cmake.org/cmake/help/latest/module/FetchContent.html#integrating-with-find-package
#
# All three are safe to call more than once for the same name (e.g. two
# modules that both need the same dependency) -- later calls are no-ops.
#
# A module DEPENDS on whatever target the chosen path defines, exactly
# like depending on another module -- add_lib_module/add_exe_module don't
# need to know or care which of the three supplied it.
#
# IMPORTANT -- installing: a vendored/fetched dependency's own
# CMakeLists.txt usually has its own install() rules, and there is no
# reliable, version-independent way to suppress them from here (EXCLUDE_
# FROM_ALL's effect on install() is explicitly documented as undefined,
# and CMAKE_SKIP_INSTALL_RULES breaks `cmake --install` outright for
# FetchContent -- both were tried and rejected). The actual fix lives in
# CMakeLists.txt: our own install(TARGETS ...) calls are tagged
# COMPONENT ${PROJECT_CODE_NAME}, and scripts/install.sh / install.bat always pass
# --component so a third-party dependency's untagged install rules are
# never pulled in. Installing with plain `cmake --install build` (no
# --component) will also install whatever the dependency wanted to
# install, typically under CMAKE_INSTALL_PREFIX (e.g. /usr/local) --
# use the install script, or pass --component yourself.
# =============================================================================
include_guard(GLOBAL)
# -----------------------------------------------------------------------
# _exclude_from_compile_commands(DIR): drop every target defined under
# DIR from compile_commands.json. Applied to vendored/fetched third-party
# code so the exported compile database stays focused on code this
# project actually owns.
# -----------------------------------------------------------------------
function(_exclude_from_compile_commands DIR)
if(NOT IS_DIRECTORY "${DIR}")
return()
endif()
get_property(_pmn_dir_targets DIRECTORY "${DIR}" PROPERTY BUILDSYSTEM_TARGETS)
foreach(_t ${_pmn_dir_targets})
set_target_properties(${_t} PROPERTIES EXPORT_COMPILE_COMMANDS OFF)
endforeach()
endfunction()
# -----------------------------------------------------------------------
# add_vendor_header_only(NAME [INCLUDE_DIR <dir>])
# -----------------------------------------------------------------------
macro(add_vendor_header_only NAME)
cmake_parse_arguments(ARG "" "INCLUDE_DIR" "" ${ARGN})
if(NOT TARGET ${NAME})
add_library(${NAME} INTERFACE)
if(ARG_INCLUDE_DIR)
target_include_directories(${NAME} SYSTEM INTERFACE "${CMAKE_SOURCE_DIR}/vendor/${ARG_INCLUDE_DIR}")
else()
target_include_directories(${NAME} SYSTEM INTERFACE "${CMAKE_SOURCE_DIR}/vendor/${NAME}/include")
endif()
endif()
endmacro()
# -----------------------------------------------------------------------
# add_vendor_subdirectory(SUBDIR)
#
# EXCLUDE_FROM_ALL means only targets actually depended on via
# target_link_libraries get built. SYSTEM marks its headers so our
# per-module warning flags (added only by add_lib_module/add_exe_module/
# etc., never globally) don't fire when *our* code includes them.
# -----------------------------------------------------------------------
macro(add_vendor_subdirectory SUBDIR)
get_property(_pmn_vendor_added GLOBAL PROPERTY PMN_VENDOR_ADDED_${SUBDIR} SET)
if(NOT _pmn_vendor_added)
set_property(GLOBAL PROPERTY PMN_VENDOR_ADDED_${SUBDIR} TRUE)
add_subdirectory(
"${CMAKE_SOURCE_DIR}/vendor/${SUBDIR}"
"${CMAKE_BINARY_DIR}/vendor/${SUBDIR}"
EXCLUDE_FROM_ALL SYSTEM
)
_exclude_from_compile_commands("${CMAKE_SOURCE_DIR}/vendor/${SUBDIR}")
endif()
endmacro()
# -----------------------------------------------------------------------
# add_dependency(NAME <FetchContent_Declare args...>)
#
# Example -- prefer an installed copy that's new enough, fetch+build from
# source otherwise:
# 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
# )
# add_lib_module(mylib DEPENDS fmt::fmt)
#
# Always give FIND_PACKAGE_ARGS a minimum version (the "12" above). Without
# one, ANY installed copy beats the pinned GIT_TAG -- including a distro
# package years older than the pin -- so a "pinned" dependency isn't. A
# dependency's own CMake options (fmt's FMT_MODULE, say) are set with
# set(<OPTION> <value> CACHE ...) BEFORE this call. source/Core/CMakeLists.txt
# is the real, commented instance of both.
#
# FetchContent_MakeAvailable() is documented idempotent per name, so
# calling this more than once for the same NAME (from multiple modules)
# is safe.
# -----------------------------------------------------------------------
macro(add_dependency NAME)
include(FetchContent)
FetchContent_Declare(${NAME} SYSTEM ${ARGN})
FetchContent_MakeAvailable(${NAME})
string(TOLOWER "${NAME}" _pmn_lc_name)
if(DEFINED ${_pmn_lc_name}_SOURCE_DIR)
_exclude_from_compile_commands("${${_pmn_lc_name}_SOURCE_DIR}")
endif()
endmacro()
+190
View File
@@ -0,0 +1,190 @@
# =============================================================================
# Documentation.cmake
#
# Automatic, per-module Doxygen documentation, using a pinned Doxygen
# version -- never your system's doxygen, so every machine gets identical
# output. Zero setup required at the CMakeLists.txt level: every module
# registered via add_lib_module/add_exe_module/add_test_module/
# add_example_module gets its own documentation target for free -- nothing
# to add to a module's own CMakeLists.txt. Vendored and fetched
# dependencies (cmake/Dependencies.cmake) are never auto-documented, same
# reasoning as their compile_commands.json exclusion: it's not your code.
#
# IMPORTANT: this file only ever LOOKS for an already-downloaded pinned
# Doxygen (see cmake/DoxygenPin.cmake for where) -- it never downloads
# anything itself, so a plain `scripts/build.sh` never touches Doxygen or
# needs network access. Run `scripts/setup.sh` / `setup.bat` once (which
# runs cmake/FetchDoxygen.cmake) to actually fetch it; until then,
# documentation targets print a message pointing at that script instead
# of building, and everything else builds completely normally.
#
# What gets generated, per module <target>, once set up:
# out/docs/<target>/html/index.html -- via `cmake --build <build-dir> --target docs_<target>`
# Plus one landing page linking to all of them:
# out/docs/index.html -- via `cmake --build <build-dir> --target docs`
# (or just run scripts/docs.sh / docs.bat, which builds `docs` and prints the path)
# Paths above use the defaults from Configuration.cmake (DOCS_OUTPUT_DIR).
#
# Configuring it:
# -DENABLE_DOXYGEN=OFF disable entirely (skips even the presence check)
# -DDOXYGEN_PINNED_VERSION=1.17.0 use a different pinned version (also
# update the two SHA256 hashes in cmake/DoxygenPin.cmake to match
# that release's assets, and re-run scripts/setup.sh)
# -DDOXYGEN_WARN_IF_UNDOCUMENTED=YES / -DDOXYGEN_QUIET=NO stricter/louder output
# -DDOXYGEN_<ANY_DOXYFILE_TAG>=... any Doxyfile tag can be set this way --
# see https://www.doxygen.nl/manual/config.html
# A module can opt out with NO_DOCS, e.g.:
# add_lib_module(core_lib TYPE SHARED NO_DOCS ...)
# =============================================================================
include_guard(GLOBAL)
option(ENABLE_DOXYGEN "Generate per-module Doxygen documentation using a pinned, project-managed Doxygen (run scripts/setup.sh once to fetch it)" ON)
if(ENABLE_DOXYGEN)
include("${CMAKE_CURRENT_LIST_DIR}/DoxygenPin.cmake")
if(NOT DOXYGEN_PIN_PLATFORM_SUPPORTED)
message(WARNING "No pinned Doxygen ${DOXYGEN_PINNED_VERSION} binary is configured for this "
"host platform. Install Doxygen ${DOXYGEN_PINNED_VERSION} manually, make sure "
"it's on PATH or pass -DDOXYGEN_EXECUTABLE=/path/to/doxygen, or extend "
"cmake/DoxygenPin.cmake with a download URL + hash for this platform.")
set(ENABLE_DOXYGEN OFF)
elseif(NOT EXISTS "${DOXYGEN_PIN_EXECUTABLE}")
# Not fetched yet -- see the file header. Deliberately not a
# message() here: this is the normal, expected state before
# scripts/setup.sh has ever run, not something to warn about.
set(ENABLE_DOXYGEN OFF)
else()
# Pre-seed DOXYGEN_EXECUTABLE with our pinned path *before*
# find_package(Doxygen) runs, rather than overriding it
# afterward. find_program() (which find_package uses internally)
# does not re-search when the cache variable already holds a
# valid path -- so this makes find_package() treat our pinned
# copy as the one it found, and run its own normal success path
# using it: creating the Doxygen::doxygen imported target,
# determining DOXYGEN_VERSION, and generating the Doxyfile
# template doxygen_add_docs() needs. All of that only happens as
# a side effect of find_package() succeeding -- overriding
# DOXYGEN_EXECUTABLE *after* calling it (the previous approach
# here) leaves those steps never having run at all, which
# doxygen_add_docs() then fails on. This way we don't have to
# reimplement any of find_package's own (undocumented, and
# confirmed version-dependent -- CMake 3.28 and 4.3 both hit
# this differently) internal machinery ourselves.
set(DOXYGEN_EXECUTABLE "${DOXYGEN_PIN_EXECUTABLE}" CACHE FILEPATH "Pinned Doxygen executable (never the system's)" FORCE)
endif()
endif()
if(ENABLE_DOXYGEN)
# Also searches for `dot` (Graphviz) for call graphs -- an optional,
# system-detected enhancement, unrelated to the doxygen executable
# itself, which is pre-seeded above to always resolve to our pinned
# copy rather than a system one.
find_package(Doxygen QUIET OPTIONAL_COMPONENTS dot)
if(NOT DOXYGEN_FOUND)
message(WARNING "Pre-seeding DOXYGEN_EXECUTABLE with the pinned Doxygen at "
"${DOXYGEN_PIN_EXECUTABLE} did not result in find_package(Doxygen) "
"succeeding. Documentation will be disabled for this configure.")
set(ENABLE_DOXYGEN OFF)
endif()
endif()
if(ENABLE_DOXYGEN)
message(STATUS "Doxygen ${DOXYGEN_VERSION} ready (pinned, not system-installed: ${DOXYGEN_EXECUTABLE}) -- per-module documentation enabled")
# Project-wide defaults, one Doxyfile tag per DOXYGEN_<TAG> variable.
# Override any of them with -DDOXYGEN_<TAG>=... at configure time.
# Kept deliberately quiet and lenient by default (no warnings about
# undocumented entities) so a freshly generated module with zero
# doxygen comments still produces clean, useful output -- browsable
# files/functions/call graphs -- instead of a wall of warnings.
set(DOXYGEN_GENERATE_HTML YES)
set(DOXYGEN_GENERATE_LATEX NO)
set(DOXYGEN_EXTRACT_ALL YES)
set(DOXYGEN_EXTRACT_STATIC YES)
set(DOXYGEN_EXTRACT_PRIVATE NO)
set(DOXYGEN_WARN_IF_UNDOCUMENTED NO)
set(DOXYGEN_QUIET YES)
set(DOXYGEN_RECURSIVE YES)
set(DOXYGEN_SOURCE_BROWSER YES)
set(DOXYGEN_GENERATE_TREEVIEW YES)
if(DOXYGEN_DOT_FOUND)
set(DOXYGEN_HAVE_DOT YES)
set(DOXYGEN_CALL_GRAPH YES)
set(DOXYGEN_CALLER_GRAPH YES)
endif()
add_custom_target(docs COMMENT "Build documentation for every module")
else()
add_custom_target(docs
COMMAND ${CMAKE_COMMAND} -E echo
"Documentation isn't set up yet (or ENABLE_DOXYGEN=OFF). Run scripts/setup.sh (or setup.bat) once to fetch the pinned Doxygen, then reconfigure."
VERBATIM
)
endif()
# -----------------------------------------------------------------------
# _add_module_docs(TARGET): called automatically from _finalize_module_target
# for every module -- creates docs_<TARGET> (a Doxygen run scoped to that
# module's own directory) and wires it into the aggregate `docs` target.
# A no-op if ENABLE_DOXYGEN is off. Excluded from the default build same
# as tests/examples -- documentation is opt-in per build, not generated
# on every compile.
#
# If the module's own directory has a README.md, it's used as that
# module's Doxygen mainpage automatically.
# -----------------------------------------------------------------------
function(_add_module_docs TARGET)
if(NOT ENABLE_DOXYGEN)
return()
endif()
set(DOXYGEN_OUTPUT_DIRECTORY "${DOCS_OUTPUT_DIR}/${TARGET}")
set(DOXYGEN_PROJECT_NAME "${TARGET}")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
else()
unset(DOXYGEN_USE_MDFILE_AS_MAINPAGE)
endif()
doxygen_add_docs(docs_${TARGET}
"${CMAKE_CURRENT_SOURCE_DIR}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating documentation for module '${TARGET}'"
)
set_target_properties(docs_${TARGET} PROPERTIES EXCLUDE_FROM_ALL TRUE)
add_dependencies(docs docs_${TARGET})
set_property(GLOBAL APPEND PROPERTY PMN_DOC_MODULES ${TARGET})
endfunction()
# -----------------------------------------------------------------------
# _write_docs_index(OUTPUT_DIR): call once, after every module has been
# discovered, to write a plain landing page at OUTPUT_DIR/index.html
# linking to each module's own generated docs -- so "where do I start
# reading" has one obvious answer regardless of how many modules exist.
# -----------------------------------------------------------------------
function(_write_docs_index OUTPUT_DIR)
if(NOT ENABLE_DOXYGEN)
return()
endif()
file(MAKE_DIRECTORY "${OUTPUT_DIR}")
get_property(_pmn_doc_modules GLOBAL PROPERTY PMN_DOC_MODULES)
list(SORT _pmn_doc_modules)
set(_pmn_doc_links "")
foreach(_pmn_mod ${_pmn_doc_modules})
string(APPEND _pmn_doc_links " <li><a href=\"${_pmn_mod}/html/index.html\">${_pmn_mod}</a></li>\n")
endforeach()
file(WRITE "${OUTPUT_DIR}/index.html" "<!DOCTYPE html>
<html>
<head><meta charset=\"utf-8\"><title>${PROJECT_NAME} -- module documentation</title></head>
<body>
<h1>${PROJECT_NAME} module documentation</h1>
<ul>
${_pmn_doc_links} </ul>
</body>
</html>
")
endfunction()
+98
View File
@@ -0,0 +1,98 @@
# =============================================================================
# DoxygenPin.cmake
#
# Pure computation of "where does the pinned Doxygen live, and where would
# it be downloaded from" -- no network calls, no download, no extraction.
# Shared by two very different callers, which is why this is its own file:
#
# cmake/Documentation.cmake include()s this during the normal CMake
# configure to find out where to LOOK for an
# already-downloaded pinned Doxygen. Never
# downloads anything itself -- see that file.
#
# cmake/FetchDoxygen.cmake include()s this when run standalone via
# `cmake -P cmake/FetchDoxygen.cmake` (what
# scripts/setup.sh / setup.bat call) to know
# what to actually download.
#
# Needs TOOLCACHE_DIR to already be set -- Documentation.cmake gets it from
# Configuration.cmake (included earlier by CMakeLists.txt); FetchDoxygen.cmake
# sets it manually since script mode (`cmake -P`) never runs CMakeLists.txt.
#
# Bump DOXYGEN_PINNED_VERSION to change the pinned version project-wide --
# doing so also requires updating the SHA256 hashes below to match the
# new release's assets (each was checked against the digest GitHub
# publishes for the asset AND a local download, not trusted from either
# alone). A stale hash fails loud and hard, on purpose, in
# FetchDoxygen.cmake rather than silently accepting an unexpected file).
# =============================================================================
include_guard(GLOBAL)
if(NOT DEFINED DOXYGEN_PINNED_VERSION)
set(DOXYGEN_PINNED_VERSION "1.16.1")
endif()
string(REPLACE "." "_" _pmn_doxygen_tag_version "${DOXYGEN_PINNED_VERSION}")
set(DOXYGEN_PIN_TAG "Release_${_pmn_doxygen_tag_version}")
set(DOXYGEN_PIN_ROOT "${TOOLCACHE_DIR}/doxygen-${DOXYGEN_PINNED_VERSION}")
# This is a HOST question (what machine is actually running Doxygen), not
# a DIST_TARGET one -- see HostArch.cmake's header. Needed even on the
# cross DIST_TARGETs (an ARM64 Linux build from an x64 machine, an x64
# macOS build on an Apple Silicon one, ...): the machine running Doxygen and
# the DIST_TARGET being built need not share an architecture there, and
# this file only ever cares about the former.
include("${CMAKE_CURRENT_LIST_DIR}/HostArch.cmake")
pmn_host_arch(_pmn_doxygen_host_arch)
if(CMAKE_HOST_WIN32 AND _pmn_doxygen_host_arch STREQUAL "x64")
set(DOXYGEN_PIN_URL "https://github.com/doxygen/doxygen/releases/download/${DOXYGEN_PIN_TAG}/doxygen-${DOXYGEN_PINNED_VERSION}.windows.x64.bin.zip")
set(DOXYGEN_PIN_HASH "ddfa59a4ae9549651330471c2e387ec7a9891080543faed541c859e5ce448653")
set(DOXYGEN_PIN_EXECUTABLE "${DOXYGEN_PIN_ROOT}/doxygen.exe")
set(DOXYGEN_PIN_PLATFORM_SUPPORTED TRUE)
elseif(CMAKE_HOST_UNIX AND NOT CMAKE_HOST_APPLE AND _pmn_doxygen_host_arch STREQUAL "x64")
set(DOXYGEN_PIN_URL "https://github.com/doxygen/doxygen/releases/download/${DOXYGEN_PIN_TAG}/doxygen-${DOXYGEN_PINNED_VERSION}.linux.bin.tar.gz")
set(DOXYGEN_PIN_HASH "a56f885d37e3aae08a99f638d17bbb381224c03a878d9e2dda4f9fa4baf1d8bd")
set(DOXYGEN_PIN_EXECUTABLE "${DOXYGEN_PIN_ROOT}/doxygen-${DOXYGEN_PINNED_VERSION}/bin/doxygen")
set(DOXYGEN_PIN_PLATFORM_SUPPORTED TRUE)
elseif(CMAKE_HOST_APPLE AND _pmn_doxygen_host_arch STREQUAL "arm64")
# macOS ships two plain zips (next to the .dmg GUI installers, which are
# deliberately not used here: mounting a disk image needs hdiutil and a
# GUI app bundle around what is, inside, just this CLI binary). Each
# holds doxygen-<version>/doxygen -- no bin/ level, unlike the Linux
# tarball. Which one is the MACHINE's architecture, not the cmake
# process's, courtesy of HostArch.cmake's Rosetta handling.
set(DOXYGEN_PIN_URL "https://github.com/doxygen/doxygen/releases/download/${DOXYGEN_PIN_TAG}/doxygen-${DOXYGEN_PINNED_VERSION}-mac-arm.zip")
set(DOXYGEN_PIN_HASH "f75d4e7253ad959d691cb65b6c3a9bb0c138ce75bce7cef17f5ebf36c7b54bdc")
set(DOXYGEN_PIN_EXECUTABLE "${DOXYGEN_PIN_ROOT}/doxygen-${DOXYGEN_PINNED_VERSION}/doxygen")
set(DOXYGEN_PIN_PLATFORM_SUPPORTED TRUE)
elseif(CMAKE_HOST_APPLE AND _pmn_doxygen_host_arch STREQUAL "x64")
set(DOXYGEN_PIN_URL "https://github.com/doxygen/doxygen/releases/download/${DOXYGEN_PIN_TAG}/doxygen-${DOXYGEN_PINNED_VERSION}-mac-intel.zip")
set(DOXYGEN_PIN_HASH "1530e5eca05b442e4cdd80a8ad6b22e1bbb3fc14e12b64b07c61de4be48cc5f1")
set(DOXYGEN_PIN_EXECUTABLE "${DOXYGEN_PIN_ROOT}/doxygen-${DOXYGEN_PINNED_VERSION}/doxygen")
set(DOXYGEN_PIN_PLATFORM_SUPPORTED TRUE)
else()
# What's left: Windows or Linux on ARM64/ARM32, and any other exotic
# host. (macOS has its own branches above.) Unlike Ninja (see the
# equivalent comment in NinjaPin.cmake), the upstream Doxygen
# project's GitHub releases don't publish ARM Linux or ARM Windows
# binaries at all as of DOXYGEN_PINNED_VERSION -- the release's asset
# list is the two macOS zips, two macOS disk images, the x64 Linux
# tarball, the x64 Windows zip and installer, the sources, and the
# manual -- so this isn't fillable yet.
# To add support once/if such an asset exists: find its exact name at
# https://github.com/doxygen/doxygen/releases/tag/${DOXYGEN_PIN_TAG},
# download and verify it out-of-band, and add a branch here with its
# URL and SHA256, following the pattern above; this project won't put
# an unverified hash into one meant to catch supply-chain tampering
# (see the file-header comment above). Until then this degrades the
# same way any other unsupported host already does: FetchDoxygen.cmake
# gives a clear error, and Documentation.cmake disables ENABLE_DOXYGEN
# with a warning instead of failing the build (see that file) -- a
# plain build never depends on Doxygen regardless.
set(DOXYGEN_PIN_URL "")
set(DOXYGEN_PIN_HASH "")
set(DOXYGEN_PIN_EXECUTABLE "")
set(DOXYGEN_PIN_PLATFORM_SUPPORTED FALSE)
endif()
+111
View File
@@ -0,0 +1,111 @@
# =============================================================================
# FetchDoxygen.cmake
#
# Downloads, verifies, and extracts the pinned Doxygen (see DoxygenPin.cmake
# for the version/URL/hash) into TOOLCACHE_DIR. Run standalone, NOT included
# by the normal CMake configure:
#
# cmake -P cmake/FetchDoxygen.cmake
#
# This is what scripts/setup.sh / setup.bat call. It's deliberately not part
# of the main `cmake --preset <name>` configure -- see cmake/Documentation.cmake,
# which only ever LOOKS for the result of this script and never downloads
# anything itself, so a plain build never depends on Doxygen or on network
# access. Safe to re-run any time; a no-op if the pinned version is already
# present.
# =============================================================================
# TOOLCACHE_DIR normally comes from Configuration.cmake, which isn't
# loaded in this standalone script-mode invocation -- read it from there
# directly (in query mode, so we get just the value, none of
# Configuration.cmake's normal configure-time side effects) rather than
# hardcoding a second copy of its default. If you've overridden
# TOOLCACHE_DIR via -DTOOLCACHE_DIR=... at configure time, pass the same
# value here to match: cmake -DTOOLCACHE_DIR=... -P cmake/FetchDoxygen.cmake
if(NOT DEFINED TOOLCACHE_DIR)
set(PMN_CONFIG_QUERY_ONLY TRUE)
get_filename_component(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
include("${CMAKE_SOURCE_DIR}/Configuration.cmake")
endif()
include("${CMAKE_CURRENT_LIST_DIR}/DoxygenPin.cmake")
if(NOT DOXYGEN_PIN_PLATFORM_SUPPORTED)
set(_pmn_doxygen_platform_hint "")
if(CMAKE_HOST_WIN32)
set(_pmn_doxygen_platform_hint
" If this looks like an ordinary 64-bit machine, double-check this isn't "
"a 32-bit cmake.exe running under WoW64 (installed under \"Program Files "
"(x86)\" is a good sign it is) -- that's a common, easy way to end up here "
"even on x64/ARM64 hardware; installing the 64-bit CMake from cmake.org "
"fixes it.")
endif()
message(FATAL_ERROR "No pinned Doxygen ${DOXYGEN_PINNED_VERSION} binary is configured for this host "
"platform.${_pmn_doxygen_platform_hint} Install Doxygen ${DOXYGEN_PINNED_VERSION} "
"manually, make sure it's on PATH or pass "
"-DDOXYGEN_EXECUTABLE=/path/to/doxygen at project-configure time, or extend "
"cmake/DoxygenPin.cmake with a download URL + hash for this platform.")
endif()
if(EXISTS "${DOXYGEN_PIN_EXECUTABLE}")
message(STATUS "Doxygen ${DOXYGEN_PINNED_VERSION} already present: ${DOXYGEN_PIN_EXECUTABLE}")
return()
endif()
message(STATUS "Downloading pinned Doxygen ${DOXYGEN_PINNED_VERSION} (cached under ${TOOLCACHE_DIR})")
set(_pmn_doxygen_archive "${TOOLCACHE_DIR}/_doxygen_download.archive")
# No EXPECTED_HASH here on purpose: file(DOWNLOAD ... EXPECTED_HASH ...)
# raises a hard, unrecoverable CMake Error on ANY failure -- including
# plain network/connectivity failures -- which would turn a "you're
# offline" hiccup into a worse error than it needs to be. Downloading
# first and verifying the hash ourselves afterward lets us give a clearer
# message for a network failure, while still treating a genuine hash
# mismatch (see below) as fully fatal.
file(DOWNLOAD "${DOXYGEN_PIN_URL}" "${_pmn_doxygen_archive}"
STATUS _pmn_doxygen_dl_status
SHOW_PROGRESS
)
list(GET _pmn_doxygen_dl_status 0 _pmn_doxygen_dl_code)
if(NOT _pmn_doxygen_dl_code EQUAL 0)
list(GET _pmn_doxygen_dl_status 1 _pmn_doxygen_dl_msg)
file(REMOVE "${_pmn_doxygen_archive}")
message(FATAL_ERROR "Failed to download pinned Doxygen ${DOXYGEN_PINNED_VERSION}: ${_pmn_doxygen_dl_msg}. "
"Check your network connection and try again -- your build doesn't need this to "
"succeed, only `scripts/docs.sh` / `docs.bat` does.")
endif()
# Verify integrity, deliberately as a hard failure -- a mismatch here means
# the downloaded artifact doesn't match what this project expects
# (corruption or tampering in transit, or a stale pinned hash after
# bumping DOXYGEN_PINNED_VERSION in DoxygenPin.cmake without updating the
# matching hash there): a supply-chain-relevant integrity failure, not
# something to silently continue past.
file(SHA256 "${_pmn_doxygen_archive}" _pmn_doxygen_actual_hash)
string(TOLOWER "${_pmn_doxygen_actual_hash}" _pmn_doxygen_actual_hash)
string(TOLOWER "${DOXYGEN_PIN_HASH}" _pmn_doxygen_expected_hash_lc)
if(NOT _pmn_doxygen_actual_hash STREQUAL _pmn_doxygen_expected_hash_lc)
file(REMOVE "${_pmn_doxygen_archive}")
message(FATAL_ERROR "Pinned Doxygen ${DOXYGEN_PINNED_VERSION} download hash mismatch -- "
"expected ${_pmn_doxygen_expected_hash_lc}, got ${_pmn_doxygen_actual_hash}. "
"This usually means DOXYGEN_PINNED_VERSION was bumped in cmake/DoxygenPin.cmake "
"without updating the matching hash there, or the download was corrupted or "
"tampered with in transit.")
endif()
file(ARCHIVE_EXTRACT INPUT "${_pmn_doxygen_archive}" DESTINATION "${DOXYGEN_PIN_ROOT}")
file(REMOVE "${_pmn_doxygen_archive}")
if(NOT CMAKE_HOST_WIN32)
file(CHMOD "${DOXYGEN_PIN_EXECUTABLE}" PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
endif()
if(EXISTS "${DOXYGEN_PIN_EXECUTABLE}")
message(STATUS "Doxygen ${DOXYGEN_PINNED_VERSION} ready: ${DOXYGEN_PIN_EXECUTABLE}")
else()
message(FATAL_ERROR "Download/extract completed but the expected executable is missing: "
"${DOXYGEN_PIN_EXECUTABLE}")
endif()
+109
View File
@@ -0,0 +1,109 @@
# =============================================================================
# FetchNinja.cmake
#
# Downloads, verifies, and extracts the pinned ninja (see NinjaPin.cmake for
# the version/URL/hash) into TOOLCACHE_DIR. Run standalone, never included by
# the normal CMake configure -- ninja is needed as the generator itself
# (`cmake -G Ninja`), so it has to be resolved *before* configure runs, unlike
# Doxygen (see FetchDoxygen.cmake), which is only needed for a downstream
# custom target:
#
# cmake -P cmake/FetchNinja.cmake
#
# This is what scripts/setup.sh / setup.bat call, and also what scripts/
# build.sh / build.bat call on demand if neither a pinned nor a system ninja
# is found (see the comment there). Safe to re-run any time; a no-op if the
# pinned version is already present.
# =============================================================================
# TOOLCACHE_DIR normally comes from Configuration.cmake, which isn't
# loaded in this standalone script-mode invocation -- read it from there
# directly (in query mode, so we get just the value, none of
# Configuration.cmake's normal configure-time side effects) rather than
# hardcoding a second copy of its default. If you've overridden
# TOOLCACHE_DIR via -DTOOLCACHE_DIR=... at configure time, pass the same
# value here to match: cmake -DTOOLCACHE_DIR=... -P cmake/FetchNinja.cmake
if(NOT DEFINED TOOLCACHE_DIR)
set(PMN_CONFIG_QUERY_ONLY TRUE)
get_filename_component(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
include("${CMAKE_SOURCE_DIR}/Configuration.cmake")
endif()
include("${CMAKE_CURRENT_LIST_DIR}/NinjaPin.cmake")
if(NOT NINJA_PIN_PLATFORM_SUPPORTED)
set(_pmn_ninja_platform_hint "")
if(CMAKE_HOST_WIN32)
set(_pmn_ninja_platform_hint
" If this looks like an ordinary 64-bit machine, double-check this isn't "
"a 32-bit cmake.exe running under WoW64 (installed under \"Program Files "
"(x86)\" is a good sign it is) -- that's a common, easy way to end up here "
"even on x64/ARM64 hardware; installing the 64-bit CMake from cmake.org "
"fixes it.")
endif()
message(FATAL_ERROR "No pinned ninja ${NINJA_PINNED_VERSION} binary is configured for this host "
"platform.${_pmn_ninja_platform_hint} Install ninja manually and make sure "
"it's on PATH, or extend cmake/NinjaPin.cmake with a download URL + hash for "
"this platform.")
endif()
if(EXISTS "${NINJA_PIN_EXECUTABLE}")
message(STATUS "ninja ${NINJA_PINNED_VERSION} already present: ${NINJA_PIN_EXECUTABLE}")
return()
endif()
message(STATUS "Downloading pinned ninja ${NINJA_PINNED_VERSION} (cached under ${TOOLCACHE_DIR})")
set(_pmn_ninja_archive "${TOOLCACHE_DIR}/_ninja_download.archive")
# No EXPECTED_HASH here on purpose -- see the identical comment in
# FetchDoxygen.cmake: file(DOWNLOAD ... EXPECTED_HASH ...) raises a hard,
# unrecoverable CMake Error on ANY failure, including plain network
# failures. Downloading first and verifying the hash ourselves afterward
# lets a network failure and a genuine integrity failure be told apart and
# handled differently (see below).
file(DOWNLOAD "${NINJA_PIN_URL}" "${_pmn_ninja_archive}"
STATUS _pmn_ninja_dl_status
SHOW_PROGRESS
)
list(GET _pmn_ninja_dl_status 0 _pmn_ninja_dl_code)
if(NOT _pmn_ninja_dl_code EQUAL 0)
list(GET _pmn_ninja_dl_status 1 _pmn_ninja_dl_msg)
file(REMOVE "${_pmn_ninja_archive}")
message(FATAL_ERROR "Failed to download pinned ninja ${NINJA_PINNED_VERSION}: ${_pmn_ninja_dl_msg}. "
"Check your network connection and try again, or install ninja yourself and "
"make sure it's on PATH -- scripts/build.sh / build.bat will use a system ninja "
"if no pinned one is found.")
endif()
# Verify integrity, deliberately as a hard failure -- see the identical
# comment in FetchDoxygen.cmake: a mismatch here means corruption/tampering
# in transit, or a stale pinned hash after bumping NINJA_PINNED_VERSION in
# NinjaPin.cmake without updating the matching hash there.
file(SHA256 "${_pmn_ninja_archive}" _pmn_ninja_actual_hash)
string(TOLOWER "${_pmn_ninja_actual_hash}" _pmn_ninja_actual_hash)
string(TOLOWER "${NINJA_PIN_HASH}" _pmn_ninja_expected_hash_lc)
if(NOT _pmn_ninja_actual_hash STREQUAL _pmn_ninja_expected_hash_lc)
file(REMOVE "${_pmn_ninja_archive}")
message(FATAL_ERROR "Pinned ninja ${NINJA_PINNED_VERSION} download hash mismatch -- "
"expected ${_pmn_ninja_expected_hash_lc}, got ${_pmn_ninja_actual_hash}. "
"This usually means NINJA_PINNED_VERSION was bumped in cmake/NinjaPin.cmake "
"without updating the matching hash there, or the download was corrupted or "
"tampered with in transit.")
endif()
file(ARCHIVE_EXTRACT INPUT "${_pmn_ninja_archive}" DESTINATION "${NINJA_PIN_ROOT}")
file(REMOVE "${_pmn_ninja_archive}")
if(NOT CMAKE_HOST_WIN32)
file(CHMOD "${NINJA_PIN_EXECUTABLE}" PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
endif()
if(EXISTS "${NINJA_PIN_EXECUTABLE}")
message(STATUS "ninja ${NINJA_PINNED_VERSION} ready: ${NINJA_PIN_EXECUTABLE}")
else()
message(FATAL_ERROR "Download/extract completed but the expected executable is missing: "
"${NINJA_PIN_EXECUTABLE}")
endif()
+155
View File
@@ -0,0 +1,155 @@
# =============================================================================
# HostArch.cmake
#
# One place to turn this project's own architecture labels -- x64, arm64,
# or arm32 (empty if unrecognized) -- out of a processor string. Two entry
# points, because "which architecture" has two different questions behind it:
#
# pmn_host_arch() the machine CMake is actually running ON.
# NinjaPin.cmake / DoxygenPin.cmake use it to decide
# whether a pinned-tool download exists for the machine
# running CMake -- always a HOST question, independent
# of what's being built. Configuration.cmake also uses
# it for Clang's GNU-style driver on Windows (see
# below for why MSVC/clang-cl need a different check,
# done separately in that file).
#
# pmn_target_arch() the machine the build is FOR, i.e.
# CMAKE_SYSTEM_PROCESSOR. Configuration.cmake's Linux
# DIST_TARGET detection uses it. On a native build it
# is the same answer as pmn_host_arch() (CMake sets
# CMAKE_SYSTEM_PROCESSOR from the host unless told
# otherwise); the difference is a cross build, where a
# toolchain file (cmake/toolchains/) sets it to the
# target -- asking the host there would label an
# aarch64 cross build "x64".
#
# Both share one aarch64/armv7l/... spelling list, and the platform-specific
# unreliability below is only handled once. Needs no project()/compiler
# detection, so it's safe to include from a bare `cmake -P` script.
#
# About pmn_host_arch() on each platform: on Linux it is
# CMAKE_HOST_SYSTEM_PROCESSOR, which is just `uname -m` -- reliable, with
# one catch that used to break the pinned-tool download on every Linux
# machine: `cmake -P` (script mode -- how scripts/setup.sh runs
# FetchNinja.cmake and FetchDoxygen.cmake) never runs CMake's system
# detection, so CMAKE_HOST_SYSTEM_PROCESSOR is simply not defined there.
# In that case this asks the OS itself, with `uname -m`. On macOS it is
# the same `uname -m`, corrected for Rosetta 2 (see the comment inside the
# function). On
# Windows it deliberately does NOT use CMAKE_HOST_SYSTEM_
# PROCESSOR (or CMAKE_SYSTEM_PROCESSOR): both are just the
# PROCESSOR_ARCHITECTURE environment variable, which reports the
# architecture of the CURRENT PROCESS, not the machine -- a 32-bit
# cmake.exe (still a completely ordinary, common download; nothing exotic)
# running under WoW64 on an ordinary 64-bit machine reports "x86" here even
# though the machine itself is x64 or ARM64. That's the same class of
# problem as the MSVC/clang-cl architecture-detection trap in
# Configuration.cmake (CMake issue #25466: gitlab.kitware.com/cmake/cmake/-/issues/25466),
# just via WoW64 process redirection rather than a cross-targeting
# compiler -- confirmed the hard way, as exactly what broke the pinned-
# ninja/-doxygen lookup below on an entirely ordinary 64-bit machine with a
# 32-bit cmake.exe on PATH. PROCESSOR_ARCHITEW6432 is what Windows itself
# sets to expose the TRUE architecture to a WoW64-redirected 32-bit
# process; it's simply unset outside WoW64, which is exactly when
# PROCESSOR_ARCHITECTURE was already correct anyway -- so preferring it
# when present, falling back to PROCESSOR_ARCHITECTURE otherwise, is
# correct either way.
# =============================================================================
include_guard(GLOBAL)
# _pmn_arch_label(<processor-string> <out-var>)
#
# The one place the aarch64/x86_64/armv7l/... spellings are mapped to this
# project's x64/arm64/arm32 labels. <out-var> gets one of those, or "" if
# unrecognized.
function(_pmn_arch_label PROCESSOR OUT_VAR)
if(PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(${OUT_VAR} "arm64" PARENT_SCOPE)
elseif(PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
set(${OUT_VAR} "x64" PARENT_SCOPE)
elseif(PROCESSOR MATCHES "^[Aa][Rr][Mm]")
# Every other spelling of 32-bit ARM this project has seen: armv6l
# (Raspberry Pi 1/Zero), armv7l (the common case -- Raspberry Pi
# 2/3/4 32-bit OS, generic armhf), armv8l (a 32-bit personality/
# chroot on 64-bit ARM hardware), or bare "arm"/"ARM". Checked
# after arm64/aarch64 above since those also start with "arm".
set(${OUT_VAR} "arm32" PARENT_SCOPE)
else()
set(${OUT_VAR} "" PARENT_SCOPE)
endif()
endfunction()
# pmn_host_arch(<out-var> [<raw-out-var>])
#
# <out-var> gets one of x64/arm64/arm32/"" (unrecognized) for the machine
# running CMake. The optional <raw-out-var> gets whatever raw string was
# actually checked to produce that -- callers building an error message for
# the "" case should use this rather than assuming which underlying
# variable/environment variable was consulted, since that differs by
# platform (see above).
function(pmn_host_arch OUT_VAR)
if(CMAKE_HOST_WIN32)
if(NOT "$ENV{PROCESSOR_ARCHITEW6432}" STREQUAL "")
set(_pmn_ha_processor "$ENV{PROCESSOR_ARCHITEW6432}")
else()
set(_pmn_ha_processor "$ENV{PROCESSOR_ARCHITECTURE}")
endif()
elseif(CMAKE_HOST_SYSTEM_PROCESSOR)
set(_pmn_ha_processor "${CMAKE_HOST_SYSTEM_PROCESSOR}")
elseif(CMAKE_HOST_UNIX)
# Script mode (cmake -P): not defined -- see the header. This is
# exactly what CMake itself does to fill it in during a configure.
execute_process(COMMAND uname -m
OUTPUT_VARIABLE _pmn_ha_processor
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
else()
set(_pmn_ha_processor "${CMAKE_SYSTEM_PROCESSOR}")
endif()
# macOS: a process running under Rosetta 2 -- an x86_64 cmake on an Apple
# Silicon Mac, entirely ordinary since Intel builds of CMake and of most
# developer tools still circulate -- is told it's on an x86_64 machine,
# by `uname -m` and so by CMAKE_HOST_SYSTEM_PROCESSOR alike. The same
# class of trap as a 32-bit process under WoW64 on Windows (see the
# header), and fixed the same way: ask about the MACHINE.
# hw.optional.arm64 is 1 on any Apple Silicon Mac no matter whether the
# asking process is translated, and doesn't exist on Intel (-i makes a
# missing OID a silent empty answer instead of an error).
if(CMAKE_HOST_APPLE)
_pmn_arch_label("${_pmn_ha_processor}" _pmn_ha_probe)
if(_pmn_ha_probe STREQUAL "x64")
execute_process(COMMAND sysctl -in hw.optional.arm64
OUTPUT_VARIABLE _pmn_ha_arm64
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(_pmn_ha_arm64 STREQUAL "1")
set(_pmn_ha_processor "arm64")
endif()
endif()
endif()
if(ARGC GREATER 1)
set(${ARGV1} "${_pmn_ha_processor}" PARENT_SCOPE)
endif()
_pmn_arch_label("${_pmn_ha_processor}" _pmn_ha_label)
set(${OUT_VAR} "${_pmn_ha_label}" PARENT_SCOPE)
endfunction()
# pmn_target_arch(<out-var> [<raw-out-var>])
#
# Same contract as pmn_host_arch(), but for the machine the build is FOR:
# CMAKE_SYSTEM_PROCESSOR. Only meaningful where that variable can be
# trusted -- Linux, where it is `uname -m` for a native build and whatever
# the toolchain file says for a cross build. Not for MSVC/clang-cl, where
# it reflects the cmake.exe process instead (see the header).
function(pmn_target_arch OUT_VAR)
if(ARGC GREATER 1)
set(${ARGV1} "${CMAKE_SYSTEM_PROCESSOR}" PARENT_SCOPE)
endif()
_pmn_arch_label("${CMAKE_SYSTEM_PROCESSOR}" _pmn_ta_label)
set(${OUT_VAR} "${_pmn_ta_label}" PARENT_SCOPE)
endfunction()
+109
View File
@@ -0,0 +1,109 @@
# =============================================================================
# NinjaPin.cmake
#
# Pure computation of "where does the pinned ninja live, and where would it
# be downloaded from" -- no network calls, no download, no extraction.
# Mirrors DoxygenPin.cmake exactly; see that file for the full rationale.
# Shared by two very different callers:
#
# scripts/build.sh / build.bat include() this (via `cmake -P`, same
# mechanism as FetchNinja.cmake below) to
# find out where to LOOK for an
# already-downloaded pinned ninja, before
# CMake configure even runs -- ninja is
# needed as the generator itself, unlike
# Doxygen, which is only ever needed for a
# downstream custom target. Never
# downloads anything itself.
#
# cmake/FetchNinja.cmake include()s this when run standalone via
# `cmake -P cmake/FetchNinja.cmake` (what
# scripts/setup.sh / setup.bat call) to
# know what to actually download.
#
# Needs TOOLCACHE_DIR to already be set. Both callers above set it manually
# (matching Configuration.cmake's default) since neither runs through
# CMakeLists.txt.
#
# Bump NINJA_PINNED_VERSION to change the pinned version project-wide --
# doing so also requires updating the SHA256 hashes below to match the
# new release's assets (a stale hash fails loud and hard, on purpose, in
# FetchNinja.cmake rather than silently accepting an unexpected file).
# GitHub publishes a sha256 digest for every release asset (the
# "digest" field of its releases API, shown on the release page too); the
# hashes below were each checked against it AND against a local download,
# rather than trusted from either alone.
# =============================================================================
include_guard(GLOBAL)
if(NOT DEFINED NINJA_PINNED_VERSION)
set(NINJA_PINNED_VERSION "1.13.2")
endif()
set(NINJA_PIN_TAG "v${NINJA_PINNED_VERSION}")
set(NINJA_PIN_ROOT "${TOOLCACHE_DIR}/ninja-${NINJA_PINNED_VERSION}")
# This is a HOST question (what machine is actually running ninja), not a
# DIST_TARGET one -- see HostArch.cmake's header. Needed even on the
# cross DIST_TARGETs (an ARM64 Linux build from an x64 machine, an x64
# macOS build on an Apple Silicon one, ...): the machine running ninja and
# the DIST_TARGET being built need not share an architecture there, and
# this file only ever cares about the former.
include("${CMAKE_CURRENT_LIST_DIR}/HostArch.cmake")
pmn_host_arch(_pmn_ninja_host_arch)
if(CMAKE_HOST_WIN32 AND _pmn_ninja_host_arch STREQUAL "x64")
set(NINJA_PIN_URL "https://github.com/ninja-build/ninja/releases/download/${NINJA_PIN_TAG}/ninja-win.zip")
set(NINJA_PIN_HASH "07fc8261b42b20e71d1720b39068c2e14ffcee6396b76fb7a795fb460b78dc65")
set(NINJA_PIN_EXECUTABLE "${NINJA_PIN_ROOT}/ninja.exe")
set(NINJA_PIN_PLATFORM_SUPPORTED TRUE)
elseif(CMAKE_HOST_UNIX AND NOT CMAKE_HOST_APPLE AND _pmn_ninja_host_arch STREQUAL "x64")
set(NINJA_PIN_URL "https://github.com/ninja-build/ninja/releases/download/${NINJA_PIN_TAG}/ninja-linux.zip")
set(NINJA_PIN_HASH "5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6")
set(NINJA_PIN_EXECUTABLE "${NINJA_PIN_ROOT}/ninja")
set(NINJA_PIN_PLATFORM_SUPPORTED TRUE)
elseif(CMAKE_HOST_UNIX AND NOT CMAKE_HOST_APPLE AND _pmn_ninja_host_arch STREQUAL "arm64")
# Ninja has shipped ARM64 builds since v1.12.0 (see the comment on the
# unsupported fallback below).
set(NINJA_PIN_URL "https://github.com/ninja-build/ninja/releases/download/${NINJA_PIN_TAG}/ninja-linux-aarch64.zip")
set(NINJA_PIN_HASH "fd2cacc8050a7f12a16a2e48f9e06fca5c14fc4c2bee2babb67b58be17a607fc")
set(NINJA_PIN_EXECUTABLE "${NINJA_PIN_ROOT}/ninja")
set(NINJA_PIN_PLATFORM_SUPPORTED TRUE)
elseif(CMAKE_HOST_WIN32 AND _pmn_ninja_host_arch STREQUAL "arm64")
set(NINJA_PIN_URL "https://github.com/ninja-build/ninja/releases/download/${NINJA_PIN_TAG}/ninja-winarm64.zip")
set(NINJA_PIN_HASH "e52f0bdef9dfb1003229dbd6508a508c4073fd017247002adc66e5e806cb0391")
set(NINJA_PIN_EXECUTABLE "${NINJA_PIN_ROOT}/ninja.exe")
set(NINJA_PIN_PLATFORM_SUPPORTED TRUE)
elseif(CMAKE_HOST_APPLE)
# ONE asset for both Intel and Apple Silicon: ninja-mac.zip holds a
# universal (x86_64 + arm64) Mach-O binary, so unlike Linux/Windows
# this branch doesn't care which architecture the host is -- which also
# sidesteps the Rosetta trap HostArch.cmake describes.
set(NINJA_PIN_URL "https://github.com/ninja-build/ninja/releases/download/${NINJA_PIN_TAG}/ninja-mac.zip")
set(NINJA_PIN_HASH "c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b")
set(NINJA_PIN_EXECUTABLE "${NINJA_PIN_ROOT}/ninja")
set(NINJA_PIN_PLATFORM_SUPPORTED TRUE)
else()
# What's left: 32-bit ARM Linux, and any other exotic host. (macOS and
# ARM64 Linux/Windows have their own branches above -- Ninja's GitHub
# releases have shipped those binaries since v1.12.0, "ARM binaries are
# now available for Windows and Linux, too" per that release's notes.)
# Not a guess left blank -- there is simply no 32-bit ARM Linux asset
# upstream at NINJA_PINNED_VERSION. This project won't put an
# unverified SHA256 into a hash meant to catch supply-chain tampering
# (see the file-header comment above), so to add support for another
# host: find that asset's exact name at
# https://github.com/ninja-build/ninja/releases/tag/${NINJA_PIN_TAG},
# download and verify it out-of-band, and add a branch here with its
# URL and SHA256, following the pattern above. Until then this
# degrades the same way any other unsupported host already does:
# FetchNinja.cmake / scripts/setup.sh give a clear error, and
# scripts/build.sh / build.bat fall back to a system ninja with a
# warning (see those files) -- the build itself still works, just
# without a pinned/version-locked ninja.
set(NINJA_PIN_URL "")
set(NINJA_PIN_HASH "")
set(NINJA_PIN_EXECUTABLE "")
set(NINJA_PIN_PLATFORM_SUPPORTED FALSE)
endif()
+55
View File
@@ -0,0 +1,55 @@
# =============================================================================
# PrintConfig.cmake
#
# Script-mode bridge: lets scripts/*.sh and scripts/*.bat read values out of
# Configuration.cmake -- this project's single source of truth (see that
# file) -- instead of hardcoding a second copy that can silently drift out
# of sync. This is exactly the bug it replaces: scripts/install.sh and
# install.bat used to each hardcode their own literal "demo", with a
# comment warning it had to be kept in sync with PROJECT_CODE_NAME by hand.
#
# Usage:
# cmake "-DPRINT_VARS=NAME1;NAME2;..." -P cmake/PrintConfig.cmake
#
# Prints one "NAME=value" line per requested variable, in the order given.
# Every line is emitted via message(STATUS ...) -- deliberately: it's the
# only message() mode CMake sends to stdout (verified directly: the
# no-keyword default, NOTICE, and WARNING all go to stderr; STATUS is the
# one exception), which is what lets a caller capture it with plain output
# redirection/command substitution instead of needing a temp file. The
# tradeoff is that STATUS always prefixes the line with "-- ", so every
# caller strips that off along with the "NAME=" it already knows: see
# scripts/build.sh's _pmn_read_config() or scripts/build.bat's
# :read_config for the one small wrapper each shell dialect uses -- every
# script that needs a value calls that wrapper rather than re-deriving
# this convention itself.
#
# Runs with PMN_CONFIG_QUERY_ONLY set before including Configuration.cmake,
# so that file skips its normal side effects (creating out/dist/,
# rewriting .gitignore -- a value lookup shouldn't touch the filesystem,
# and this runs far more often than a real configure) and skips resolving
# DIST_TARGET/PLATFORM/ARCHITECTURE/COMPILER_ID, which need real compiler
# detection (project(... LANGUAGES CXX)) that a plain `cmake -P` script
# never performs and would get wrong. Only ask for names that don't need
# that -- PROJECT_CODE_NAME, DEFAULT_BUILD_TYPE, and the path variables
# are all safe; see Configuration.cmake's header for the full list of
# what's inside vs. outside that guard.
# =============================================================================
if(NOT DEFINED PRINT_VARS)
message(FATAL_ERROR "Usage: cmake \"-DPRINT_VARS=NAME1;NAME2;...\" -P cmake/PrintConfig.cmake")
endif()
set(PMN_CONFIG_QUERY_ONLY TRUE)
get_filename_component(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
include("${CMAKE_SOURCE_DIR}/Configuration.cmake")
foreach(_pmn_print_name ${PRINT_VARS})
if(NOT DEFINED ${_pmn_print_name})
message(FATAL_ERROR
"PrintConfig.cmake: '${_pmn_print_name}' is not defined by "
"Configuration.cmake, or isn't available in query mode -- see "
"that file's header ('How scripts read this file').")
endif()
message(STATUS "${_pmn_print_name}=${${_pmn_print_name}}")
endforeach()
+397
View File
@@ -0,0 +1,397 @@
# =============================================================================
# toolchains/_common.cmake
#
# Helpers shared by the per-DIST_TARGET toolchain files next to this one
# (linux-x64-gcc.cmake, windows-arm64-clang-cl.cmake, ...). This is not a
# toolchain file itself -- nothing should point CMAKE_TOOLCHAIN_FILE here.
#
# Why toolchain files at all
# --------------------------
# A toolchain file is CMake's own mechanism for "build with THIS compiler
# for THIS target". CMake reads it before project() detects anything, so
# the compiler and target are settled first instead of being discovered
# and then cross-checked after the fact. Every CMakePresets.json preset
# points at the toolchain file with its own name (see the "base" preset),
# and each file does three things:
#
# 1. Declares which DIST_TARGET it is for (PMN_TOOLCHAIN_DIST_TARGET), so
# Configuration.cmake no longer has to guess from the compiler and
# can't be handed a mislabeled build -- the label and the compiler
# travel together.
# 2. Picks the compiler (and, for clang-cl, the --target triple).
# 3. Where the target isn't the machine running CMake, sets up a real
# cross-compile: CMAKE_SYSTEM_NAME / CMAKE_SYSTEM_PROCESSOR, restricts
# find_package/find_library to the target's sysroot (so a host-arch
# library is never linked into a foreign-arch binary), and registers
# an emulator (qemu-user / wine) if one is installed, so ctest can run
# what it just built.
#
# Two behaviors worth knowing, both verified rather than assumed:
#
# * CMAKE_SYSTEM_NAME is only set when the build really is a cross build.
# Setting it -- even to the value the host already has -- flips
# CMAKE_CROSSCOMPILING to TRUE, which changes how try_run and
# several third-party CMakeLists behave. Native builds leave it alone.
#
# * A compiler chosen here never overrides an explicit
# -DCMAKE_CXX_COMPILER=... on the command line (an unguarded set()
# in a toolchain file silently would). The toolchain DOES beat a stray
# CXX environment variable, since a target called "linux-x64-gcc"
# shouldn't be built with whatever CXX=clang++ happens to be exported.
#
# * macOS is deliberately not a "cross build" even when the architecture
# differs from the machine's (an x86_64 build on an Apple Silicon Mac):
# Apple's clang is one multi-architecture compiler, so that is a
# different -arch flag, not a different compiler. pmn_macos_toolchain()
# leaves CMAKE_SYSTEM_NAME alone and pins CMAKE_OSX_ARCHITECTURES.
#
# Toolchain files are read more than once per configure (CMake re-reads
# them for every try_compile), so everything here must stay cheap and
# idempotent.
# =============================================================================
include_guard(GLOBAL)
include("${CMAKE_CURRENT_LIST_DIR}/../HostArch.cmake")
# -----------------------------------------------------------------------
# pmn_toolchain_target(<DIST_TARGET>)
#
# Declares which DIST_TARGET the calling toolchain file is for.
# Configuration.cmake reads PMN_TOOLCHAIN_DIST_TARGET and uses it as
# DIST_TARGET (an explicit, different -DDIST_TARGET is a FATAL_ERROR).
# -----------------------------------------------------------------------
macro(pmn_toolchain_target _pmn_tc_dist_target)
set(PMN_TOOLCHAIN_DIST_TARGET "${_pmn_tc_dist_target}")
endmacro()
# -----------------------------------------------------------------------
# pmn_use_compilers(<cxx-program> <c-program> "<install hint>")
#
# Selects the C++ (and C) compiler by program name, resolved on PATH to an
# absolute path so it's identical in every try_compile. Skipped for
# whichever of CMAKE_CXX_COMPILER / CMAKE_C_COMPILER the user already
# chose explicitly.
#
# A missing C++ compiler is a FATAL_ERROR here, with the install hint,
# rather than CMake's generic "compiler ... is not a full path and was not
# found in the PATH". The C compiler is set too even though the project
# is C++-only: if someone later adds a C source or a C dependency, CMake
# must not silently auto-detect the HOST's cc for a cross build and emit
# wrong-architecture objects. If the C compiler can't be found, its bare
# name is set anyway so that mistake fails loudly.
#
# A function (not a macro) on purpose: the hint is free text that a macro
# would evaluate twice, and its temporaries stay out of the caller's scope.
# PARENT_SCOPE lands in the toolchain file's own scope because the macros
# below, which call this, don't create one.
# -----------------------------------------------------------------------
function(pmn_use_compilers CXX_PROGRAM C_PROGRAM HINT)
if(NOT CMAKE_CXX_COMPILER)
find_program(_pmn_tc_cxx_path NAMES "${CXX_PROGRAM}" NO_CACHE)
if(NOT _pmn_tc_cxx_path)
message(FATAL_ERROR
"The '${PMN_TOOLCHAIN_DIST_TARGET}' toolchain needs the C++ compiler "
"'${CXX_PROGRAM}', but it isn't on PATH.\n${HINT}\n"
"(Or name a different compiler explicitly: -DCMAKE_CXX_COMPILER=<path>)")
endif()
set(CMAKE_CXX_COMPILER "${_pmn_tc_cxx_path}" PARENT_SCOPE)
endif()
if(NOT CMAKE_C_COMPILER)
find_program(_pmn_tc_c_path NAMES "${C_PROGRAM}" NO_CACHE)
if(_pmn_tc_c_path)
set(CMAKE_C_COMPILER "${_pmn_tc_c_path}" PARENT_SCOPE)
else()
set(CMAKE_C_COMPILER "${C_PROGRAM}" PARENT_SCOPE)
endif()
endif()
endfunction()
# -----------------------------------------------------------------------
# pmn_cross_target(<system-name> <system-processor> <find-root>)
#
# Turns this into a cross build. <find-root> is the target's sysroot-like
# prefix (Debian/Ubuntu put a cross toolchain's target headers and
# libraries under /usr/<triplet>); an explicit -DCMAKE_SYSROOT=... and
# -DCMAKE_FIND_ROOT_PATH=... from the user are kept and extended, not
# replaced.
#
# The four MODE settings are the standard cross-compile split: programs
# (Doxygen, a pinned ninja, ...) come from the HOST; libraries, headers
# and CMake packages only from the target's root. That last one is what
# makes FetchContent's find_package(fmt) miss the host's libfmt -- which
# would link, then fail at load time as the wrong architecture -- and fall
# through to fetching and building fmt for the target.
# -----------------------------------------------------------------------
macro(pmn_cross_target _pmn_tc_system _pmn_tc_processor _pmn_tc_root)
set(CMAKE_SYSTEM_NAME "${_pmn_tc_system}")
set(CMAKE_SYSTEM_PROCESSOR "${_pmn_tc_processor}")
list(APPEND CMAKE_FIND_ROOT_PATH "${_pmn_tc_root}")
list(REMOVE_DUPLICATES CMAKE_FIND_ROOT_PATH)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
endmacro()
# -----------------------------------------------------------------------
# pmn_cross_emulator(<program>|"<program>;<program>" [<args>...])
#
# Registers the first of the given program names that is installed
# (qemu-aarch64, or "wine;wine64", ...) as CMAKE_CROSSCOMPILING_EMULATOR,
# so ctest and add_custom_command(COMMAND <target>) can run a
# foreign-architecture binary, and so can scripts/run.sh. Silent if none is
# installed -- the build itself doesn't need one. An emulator the user set
# explicitly is left alone.
#
# It's a cache entry rather than a plain variable so that scripts/run.sh,
# which runs long after configure, can read it back out of CMakeCache.txt.
# CMake treats a cached CMAKE_CROSSCOMPILING_EMULATOR exactly like a
# variable set here (it's also what -DCMAKE_CROSSCOMPILING_EMULATOR=...
# creates). Only set once found, so installing qemu later still gets
# picked up on the next configure.
# -----------------------------------------------------------------------
function(pmn_cross_emulator PROGRAM)
if(NOT CMAKE_CROSSCOMPILING_EMULATOR)
find_program(_pmn_tc_emulator_path NAMES ${PROGRAM} NO_CACHE)
if(_pmn_tc_emulator_path)
set(CMAKE_CROSSCOMPILING_EMULATOR "${_pmn_tc_emulator_path}" ${ARGN}
CACHE STRING "Emulator used to run cross-compiled executables (ctest, scripts/run.sh)")
endif()
endif()
endfunction()
# -----------------------------------------------------------------------
# pmn_linux_toolchain(<x64|arm64|arm32> <gcc|clang>)
#
# One implementation behind all six linux-* toolchain files.
#
# Native or cross is decided by comparing the requested architecture with
# the machine CMake is running on (`uname -m`, via HostArch.cmake): the same
# linux-arm64-gcc preset builds natively on ARM64 hardware with the ordinary
# g++, and cross-builds from an x64 machine with aarch64-linux-gnu-g++.
#
# GCC cross: <triplet>-g++ (Debian/Ubuntu: apt install g++-<triplet>)
# Clang cross: clang++ --target=<triplet>, which additionally needs the
# target's libstdc++/libc: the GCC cross package above
# provides them under /usr/<triplet>, or pass
# -DCMAKE_SYSROOT=<sysroot>.
#
# One unusual case falls outside this: a 32-bit userland running on a
# 64-bit ARM kernel reports aarch64 from `uname -m` while its ordinary g++
# already builds 32-bit code. For that, pass -DCMAKE_CXX_COMPILER=g++ (the
# build is then labeled arm32, correctly, but is treated as a cross build).
#
# NOTE for anyone editing the macros in this file: a macro's parameters
# are textual substitutions, so every use inside if() is quoted --
# unquoted, `if(x64 STREQUAL ...)` would treat "x64" as a variable name.
# -----------------------------------------------------------------------
macro(pmn_linux_toolchain _pmn_tc_arch _pmn_tc_family)
if("${_pmn_tc_arch}" STREQUAL "x64")
set(_pmn_tc_processor "x86_64")
set(_pmn_tc_triplet "x86_64-linux-gnu")
set(_pmn_tc_gcc_pkg "g++-x86-64-linux-gnu")
set(_pmn_tc_qemu "qemu-x86_64")
elseif("${_pmn_tc_arch}" STREQUAL "arm64")
set(_pmn_tc_processor "aarch64")
set(_pmn_tc_triplet "aarch64-linux-gnu")
set(_pmn_tc_gcc_pkg "g++-aarch64-linux-gnu")
set(_pmn_tc_qemu "qemu-aarch64")
elseif("${_pmn_tc_arch}" STREQUAL "arm32")
set(_pmn_tc_processor "arm")
set(_pmn_tc_triplet "arm-linux-gnueabihf")
set(_pmn_tc_gcc_pkg "g++-arm-linux-gnueabihf")
set(_pmn_tc_qemu "qemu-arm")
else()
message(FATAL_ERROR "pmn_linux_toolchain: unknown architecture '${_pmn_tc_arch}'")
endif()
pmn_host_arch(_pmn_tc_host_arch)
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" AND "${_pmn_tc_host_arch}" STREQUAL "${_pmn_tc_arch}")
# Native: this machine already IS the target.
if("${_pmn_tc_family}" STREQUAL "gcc")
pmn_use_compilers(g++ gcc "Install GCC, e.g.: sudo apt install g++")
else()
pmn_use_compilers(clang++ clang "Install Clang, e.g.: sudo apt install clang")
endif()
else()
# Cross.
pmn_cross_target(Linux "${_pmn_tc_processor}" "/usr/${_pmn_tc_triplet}")
if("${_pmn_tc_family}" STREQUAL "gcc")
pmn_use_compilers("${_pmn_tc_triplet}-g++" "${_pmn_tc_triplet}-gcc"
"Install the cross compiler, e.g.: sudo apt install ${_pmn_tc_gcc_pkg}")
else()
pmn_use_compilers(clang++ clang
"Install Clang plus the GCC cross package that provides the target's libraries, e.g.: sudo apt install clang ${_pmn_tc_gcc_pkg}")
set(CMAKE_CXX_COMPILER_TARGET "${_pmn_tc_triplet}")
set(CMAKE_C_COMPILER_TARGET "${_pmn_tc_triplet}")
endif()
pmn_cross_emulator("${_pmn_tc_qemu}" -L "/usr/${_pmn_tc_triplet}")
endif()
endmacro()
# -----------------------------------------------------------------------
# pmn_mingw_toolchain(<x64|x86>)
#
# Behind windows-x64-mingw64 and windows-x86-mingw32. On a Windows host
# (an MSYS2 MINGW64/MINGW32 shell, or a standalone MinGW-w64) this only
# picks the compiler by its triplet-prefixed name. On a Linux host it is a
# real cross build using Debian/Ubuntu's mingw-w64 packages, with wine as
# the emulator if installed.
# -----------------------------------------------------------------------
macro(pmn_mingw_toolchain _pmn_tc_arch)
if("${_pmn_tc_arch}" STREQUAL "x64")
set(_pmn_tc_processor "x86_64")
set(_pmn_tc_triplet "x86_64-w64-mingw32")
set(_pmn_tc_pkg "g++-mingw-w64-x86-64")
elseif("${_pmn_tc_arch}" STREQUAL "x86")
set(_pmn_tc_processor "i686")
set(_pmn_tc_triplet "i686-w64-mingw32")
set(_pmn_tc_pkg "g++-mingw-w64-i686")
else()
message(FATAL_ERROR "pmn_mingw_toolchain: unknown architecture '${_pmn_tc_arch}'")
endif()
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
pmn_use_compilers("${_pmn_tc_triplet}-g++" "${_pmn_tc_triplet}-gcc"
"Install MinGW-w64 (e.g. MSYS2's mingw-w64 packages) and put its bin directory on PATH.")
else()
pmn_cross_target(Windows "${_pmn_tc_processor}" "/usr/${_pmn_tc_triplet}")
pmn_use_compilers("${_pmn_tc_triplet}-g++" "${_pmn_tc_triplet}-gcc"
"Install the cross compiler, e.g.: sudo apt install ${_pmn_tc_pkg}")
# Some distributions ship only a wine64 binary, others a wine one.
pmn_cross_emulator("wine;wine64")
endif()
endmacro()
# -----------------------------------------------------------------------
# pmn_msvc_toolchain(<x64|arm64>)
#
# Behind windows-x64-msvc and windows-arm64-msvc. cl.exe has no --target:
# which architecture it produces is decided by WHICH cl.exe comes first on
# PATH, i.e. by the Visual Studio developer environment the shell was
# started from. A toolchain file therefore can't switch architectures for
# MSVC. What it can do is pick cl instead of letting CMake fall through to
# whatever other compiler is on PATH, and fail early with the instruction
# that fixes the usual cause. Whether the cl.exe it found really targets
# the architecture in the DIST_TARGET is then verified by
# Configuration.cmake, from CMAKE_CXX_COMPILER_ARCHITECTURE_ID.
# -----------------------------------------------------------------------
macro(pmn_msvc_toolchain _pmn_tc_arch)
if("${_pmn_tc_arch}" STREQUAL "x64")
pmn_use_compilers(cl cl
"Open an \"x64 Native Tools Command Prompt for VS\" (or a Developer PowerShell for VS defaulted to x64) and run this again.")
else()
pmn_use_compilers(cl cl
"Open a Visual Studio developer prompt configured for ARM64 -- an \"ARM64 Native Tools Command Prompt for VS\", or on an x64 machine an \"x64_arm64 Cross Tools Command Prompt for VS\" (vcvarsall.bat x64_arm64) -- and run this again.")
endif()
endmacro()
# -----------------------------------------------------------------------
# pmn_clang_cl_toolchain(<x64|arm64>)
#
# Behind windows-x64-clang-cl and windows-arm64-clang-cl: LLVM's clang-cl,
# the cl.exe-compatible driver, targeting the MSVC ABI. Unlike cl.exe,
# clang-cl takes --target, so the architecture is pinned here
# (CMAKE_CXX_COMPILER_TARGET -> --target=<triple> on every compile and
# link line, and while CMake identifies the compiler) instead of depending
# on whatever the environment or the machine's own architecture defaults
# to. That is what lets windows-x64-clang-cl mean x64 on an ARM64 Windows
# machine too.
#
# The MSVC STL headers and the Windows SDK libraries still come from a
# Visual Studio / Build Tools install, as they do for any clang-cl build.
# -----------------------------------------------------------------------
macro(pmn_clang_cl_toolchain _pmn_tc_arch)
if("${_pmn_tc_arch}" STREQUAL "x64")
set(_pmn_tc_triple "x86_64-pc-windows-msvc")
elseif("${_pmn_tc_arch}" STREQUAL "arm64")
set(_pmn_tc_triple "aarch64-pc-windows-msvc")
else()
message(FATAL_ERROR "pmn_clang_cl_toolchain: unknown architecture '${_pmn_tc_arch}'")
endif()
pmn_use_compilers(clang-cl clang-cl
"Install LLVM (e.g.: winget install LLVM.LLVM) or Visual Studio's \"C++ Clang tools for Windows\" component, and make sure clang-cl's directory is on PATH.")
set(CMAKE_CXX_COMPILER_TARGET "${_pmn_tc_triple}")
set(CMAKE_C_COMPILER_TARGET "${_pmn_tc_triple}")
endmacro()
# -----------------------------------------------------------------------
# pmn_msys_clang_toolchain()
#
# Behind windows-x64-clang and windows-arm64-clang: Clang with its own
# GNU-style driver (clang++), as in an MSYS2 CLANG64 / CLANGARM64 shell
# -- distinct from clang-cl. Which architecture that clang++ targets is
# the shell environment's default, so nothing is pinned here; the
# environment is what makes clang++ a mingw-ABI compiler for the right
# machine.
# -----------------------------------------------------------------------
macro(pmn_msys_clang_toolchain)
pmn_use_compilers(clang++ clang
"Open an MSYS2 CLANG64 (x64) or CLANGARM64 (ARM64) shell, whose clang++ targets the matching mingw ABI by default.")
endmacro()
# -----------------------------------------------------------------------
# pmn_macos_toolchain(<arm64|x64>)
#
# Behind macos-arm64-clang and macos-x64-clang. Apple's clang is ONE
# multi-architecture compiler: which architecture it emits is a flag
# (-arch), not a different program. So unlike the Linux cross builds there
# is no cross compiler to find, no sysroot to restrict lookups to and no
# emulator to register (an x86_64 binary runs on an Apple Silicon Mac
# through Rosetta 2, and try_run works as usual) -- and CMAKE_CROSSCOMPILING
# correctly stays FALSE. What this pins instead:
#
# CMAKE_OSX_ARCHITECTURES the ONE architecture, always -- even when
# it is the machine's own. That is the only reliable statement of what
# gets built: without it the compiler follows the process that
# launched it, so a cmake running under Rosetta 2 on an Apple Silicon
# Mac would quietly produce x86_64 for a target that says arm64.
# CMAKE_OSX_DEPLOYMENT_TARGET the oldest macOS the result runs on:
# 11.0, the first release that runs on Apple Silicon. Left unset it
# defaults to the SDK's own version, and everything built then
# refuses to start on any macOS older than the build machine's.
#
# Both only take effect if set BEFORE the first project(), which is
# exactly when a toolchain file runs, and both yield to an explicit
# -DCMAKE_OSX_...= on the command line (the label check in
# Configuration.cmake then catches an architecture that no longer matches
# the target). Homebrew's LLVM clang instead of Apple's works the same
# way: -DCMAKE_CXX_COMPILER=$(brew --prefix llvm)/bin/clang++.
# -----------------------------------------------------------------------
macro(pmn_macos_toolchain _pmn_tc_arch)
if("${_pmn_tc_arch}" STREQUAL "arm64")
set(_pmn_tc_osx_arch "arm64")
elseif("${_pmn_tc_arch}" STREQUAL "x64")
set(_pmn_tc_osx_arch "x86_64")
else()
message(FATAL_ERROR "pmn_macos_toolchain: unknown architecture '${_pmn_tc_arch}'")
endif()
if(NOT CMAKE_OSX_ARCHITECTURES)
set(CMAKE_OSX_ARCHITECTURES "${_pmn_tc_osx_arch}")
endif()
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0")
endif()
# /usr/bin/clang++ exists on every Mac, as a stub that only works once
# the Command Line Tools (or Xcode) are installed -- without them CMake
# would fail inside its own compiler check with "xcrun: error: invalid
# active developer path". Say what's actually wrong instead.
if(CMAKE_HOST_APPLE)
execute_process(COMMAND xcode-select -p
RESULT_VARIABLE _pmn_tc_xcode_rc
OUTPUT_QUIET ERROR_QUIET)
if(NOT _pmn_tc_xcode_rc EQUAL 0)
message(FATAL_ERROR
"The '${PMN_TOOLCHAIN_DIST_TARGET}' toolchain needs Apple's compiler, but no "
"developer directory is set (xcode-select -p failed).\n"
"Install the Xcode Command Line Tools: xcode-select --install\n"
"(scripts/setup.sh offers to do this for you.)")
endif()
endif()
pmn_use_compilers(clang++ clang
"Install the Xcode Command Line Tools: xcode-select --install")
endmacro()
+14
View File
@@ -0,0 +1,14 @@
# linux-arm32-clang: Clang building for 32-bit ARM (armhf) Linux.
#
# On 32-bit ARM hardware: a native build with the ordinary clang++.
# Any other Linux machine: a cross build with clang++ --target=arm-linux-gnueabihf
# (sudo apt install clang g++-arm-linux-gnueabihf -- the second package
# supplies the target's libstdc++/libc; or -DCMAKE_SYSROOT=...).
# If qemu-arm is installed it is registered as the
# emulator, so ctest can run the result.
#
# See _common.cmake (pmn_linux_toolchain) for the details and overrides.
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("linux-arm32-clang")
pmn_linux_toolchain(arm32 clang)
+13
View File
@@ -0,0 +1,13 @@
# linux-arm32-gcc: GCC building for 32-bit ARM (armhf) Linux.
#
# On 32-bit ARM hardware: a native build with the ordinary g++.
# Any other Linux machine: a cross build with arm-linux-gnueabihf-g++
# (sudo apt install g++-arm-linux-gnueabihf).
# If qemu-arm is installed it is registered as the
# emulator, so ctest can run the result.
#
# See _common.cmake (pmn_linux_toolchain) for the details and overrides.
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("linux-arm32-gcc")
pmn_linux_toolchain(arm32 gcc)
+14
View File
@@ -0,0 +1,14 @@
# linux-arm64-clang: Clang building for 64-bit ARM (aarch64) Linux.
#
# On ARM64 hardware: a native build with the ordinary clang++.
# Any other Linux machine: a cross build with clang++ --target=aarch64-linux-gnu
# (sudo apt install clang g++-aarch64-linux-gnu -- the second package
# supplies the target's libstdc++/libc; or -DCMAKE_SYSROOT=...).
# If qemu-aarch64 is installed it is registered as the
# emulator, so ctest can run the result.
#
# See _common.cmake (pmn_linux_toolchain) for the details and overrides.
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("linux-arm64-clang")
pmn_linux_toolchain(arm64 clang)
+13
View File
@@ -0,0 +1,13 @@
# linux-arm64-gcc: GCC building for 64-bit ARM (aarch64) Linux.
#
# On ARM64 hardware: a native build with the ordinary g++.
# Any other Linux machine: a cross build with aarch64-linux-gnu-g++
# (sudo apt install g++-aarch64-linux-gnu).
# If qemu-aarch64 is installed it is registered as the
# emulator, so ctest can run the result.
#
# See _common.cmake (pmn_linux_toolchain) for the details and overrides.
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("linux-arm64-gcc")
pmn_linux_toolchain(arm64 gcc)
+14
View File
@@ -0,0 +1,14 @@
# linux-x64-clang: Clang building for 64-bit x86 (x86_64) Linux.
#
# On x64 hardware: a native build with the ordinary clang++.
# Any other Linux machine: a cross build with clang++ --target=x86_64-linux-gnu
# (sudo apt install clang g++-x86-64-linux-gnu -- the second package
# supplies the target's libstdc++/libc; or -DCMAKE_SYSROOT=...).
# If qemu-x86_64 is installed it is registered as the
# emulator, so ctest can run the result.
#
# See _common.cmake (pmn_linux_toolchain) for the details and overrides.
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("linux-x64-clang")
pmn_linux_toolchain(x64 clang)
+13
View File
@@ -0,0 +1,13 @@
# linux-x64-gcc: GCC building for 64-bit x86 (x86_64) Linux.
#
# On x64 hardware: a native build with the ordinary g++.
# Any other Linux machine: a cross build with x86_64-linux-gnu-g++
# (sudo apt install g++-x86-64-linux-gnu).
# If qemu-x86_64 is installed it is registered as the
# emulator, so ctest can run the result.
#
# See _common.cmake (pmn_linux_toolchain) for the details and overrides.
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("linux-x64-gcc")
pmn_linux_toolchain(x64 gcc)
+16
View File
@@ -0,0 +1,16 @@
# macos-arm64-clang: Clang building for Apple Silicon macOS.
#
# Apple Clang from the Xcode Command Line Tools (xcode-select --install),
# or LLVM's own Clang if you point -DCMAKE_CXX_COMPILER at it.
#
# The architecture is pinned with CMAKE_OSX_ARCHITECTURES (arm64) rather
# than left to whichever process launched the compiler, so this means
# arm64 even when cmake itself is an x86_64 build running under Rosetta 2.
# The minimum macOS is 11.0, the first release for Apple Silicon, unless
# you pass -DCMAKE_OSX_DEPLOYMENT_TARGET=... yourself.
#
# See _common.cmake (pmn_macos_toolchain).
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("macos-arm64-clang")
pmn_macos_toolchain(arm64)
+16
View File
@@ -0,0 +1,16 @@
# macos-x64-clang: Clang building for Intel macOS.
#
# Apple Clang from the Xcode Command Line Tools (xcode-select --install),
# or LLVM's own Clang if you point -DCMAKE_CXX_COMPILER at it.
#
# The architecture is pinned with CMAKE_OSX_ARCHITECTURES (x86_64) rather
# than left to whichever process launched the compiler, so this means x64
# on an Apple Silicon Mac too -- where the result runs through Rosetta 2.
# The minimum macOS is 11.0 unless you pass
# -DCMAKE_OSX_DEPLOYMENT_TARGET=... yourself.
#
# See _common.cmake (pmn_macos_toolchain).
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("macos-x64-clang")
pmn_macos_toolchain(x64)
@@ -0,0 +1,13 @@
# windows-arm64-clang-cl: LLVM's clang-cl (the cl.exe-compatible driver)
# building for ARM64 Windows with the MSVC ABI.
#
# The target is pinned to aarch64-pc-windows-msvc (--target=), so this means
# ARM64 regardless of the machine's own architecture (an x64 machine
# cross-compiles). The MSVC STL headers and Windows SDK libraries, including
# their ARM64 variants, still come from a Visual Studio / Build Tools install.
#
# See _common.cmake (pmn_clang_cl_toolchain).
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("windows-arm64-clang-cl")
pmn_clang_cl_toolchain(arm64)
@@ -0,0 +1,12 @@
# windows-arm64-clang: Clang with its own GNU-style driver (clang++) building
# for ARM64 Windows, as in an MSYS2 CLANGARM64 shell. Not clang-cl -- see
# windows-arm64-clang-cl for that.
#
# Nothing is pinned: the CLANGARM64 environment is what makes clang++ target
# ARM64 with the mingw ABI. Configuration.cmake checks that afterwards.
#
# See _common.cmake (pmn_msys_clang_toolchain).
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("windows-arm64-clang")
pmn_msys_clang_toolchain()
+13
View File
@@ -0,0 +1,13 @@
# windows-arm64-msvc: Microsoft's cl.exe building for ARM64 Windows.
#
# Run from an "ARM64 Native Tools Command Prompt for VS" (on an ARM64
# machine) or an "x64_arm64 Cross Tools Command Prompt for VS" (on an x64
# machine cross-building for ARM64). cl.exe cannot be pointed at an
# architecture from here, the developer environment decides it.
# Configuration.cmake verifies the cl.exe it got really targets ARM64.
#
# See _common.cmake (pmn_msvc_toolchain).
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("windows-arm64-msvc")
pmn_msvc_toolchain(arm64)
@@ -0,0 +1,12 @@
# windows-x64-clang-cl: LLVM's clang-cl (the cl.exe-compatible driver)
# building for x64 Windows with the MSVC ABI.
#
# The target is pinned to x86_64-pc-windows-msvc (--target=), so this means
# x64 regardless of the machine's own architecture. The MSVC STL headers and
# Windows SDK libraries still come from a Visual Studio / Build Tools install.
#
# See _common.cmake (pmn_clang_cl_toolchain).
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("windows-x64-clang-cl")
pmn_clang_cl_toolchain(x64)
+12
View File
@@ -0,0 +1,12 @@
# windows-x64-clang: Clang with its own GNU-style driver (clang++) building
# for x64 Windows, as in an MSYS2 CLANG64 shell. Not clang-cl -- see
# windows-x64-clang-cl for that.
#
# Nothing is pinned: the CLANG64 environment is what makes clang++ target
# x64 with the mingw ABI. Configuration.cmake checks that afterwards.
#
# See _common.cmake (pmn_msys_clang_toolchain).
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("windows-x64-clang")
pmn_msys_clang_toolchain()
@@ -0,0 +1,13 @@
# windows-x64-mingw64: MinGW-w64 GCC building for 64-bit Windows.
#
# Windows host: x86_64-w64-mingw32-g++ from an MSYS2 MINGW64 shell (or any
# MinGW-w64 on PATH).
# Linux host: a cross build (sudo apt install g++-mingw-w64-x86-64).
# If wine is installed it is registered as the emulator, so
# ctest can run the result.
#
# See _common.cmake (pmn_mingw_toolchain) for the details and overrides.
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("windows-x64-mingw64")
pmn_mingw_toolchain(x64)
+12
View File
@@ -0,0 +1,12 @@
# windows-x64-msvc: Microsoft's cl.exe building for x64 Windows.
#
# Run from an "x64 Native Tools Command Prompt for VS" (or any shell where
# vcvars64.bat has been run): cl.exe cannot be pointed at an architecture
# from here, the developer environment decides it. Configuration.cmake
# verifies the cl.exe it got really targets x64.
#
# See _common.cmake (pmn_msvc_toolchain).
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("windows-x64-msvc")
pmn_msvc_toolchain(x64)
@@ -0,0 +1,13 @@
# windows-x86-mingw32: MinGW-w64 GCC building for 32-bit Windows.
#
# Windows host: i686-w64-mingw32-g++ from an MSYS2 MINGW32 shell (or any
# MinGW-w64 on PATH).
# Linux host: a cross build (sudo apt install g++-mingw-w64-i686).
# If wine is installed it is registered as the emulator, so
# ctest can run the result.
#
# See _common.cmake (pmn_mingw_toolchain) for the details and overrides.
include("${CMAKE_CURRENT_LIST_DIR}/_common.cmake")
pmn_toolchain_target("windows-x86-mingw32")
pmn_mingw_toolchain(x86)
+81
View File
@@ -0,0 +1,81 @@
# Shared helpers for scripts/*.sh. Sourced, never run directly (hence the
# leading underscore and no executable bit); every script sources it after
# it has cd'd to the project root, because both helpers below run
# `cmake -P cmake/<something>.cmake` relative to that.
#
# There is deliberately no Windows twin of this file: scripts/*.bat each keep
# their own copy of these two small wrappers as labels (:read_config and
# :resolve_build_dir), because sharing labels between .bat files is more
# fragile than repeating them. What the wrappers *ask* is answered by the
# same cmake scripts either way, so no logic is duplicated -- only the glue.
# Reads one value out of Configuration.cmake -- this project's single
# source of truth -- via cmake/PrintConfig.cmake, instead of hardcoding a
# second copy of a default that can silently drift out of sync. See that
# file for exactly what this strips off of its output.
_pmn_read_config() {
cmake "-DPRINT_VARS=$1" -P cmake/PrintConfig.cmake | sed -E 's/^-- [^=]*=//'
}
# _pmn_resolve_build_dir [<name>]
#
# Prints the build directory to use. With a <name> (a preset, or "default")
# that is simply BUILD_OUTPUT_DIR/<name>; without one it is discovered: the
# directory scripts/build.sh built last, else the only one that exists. If
# neither works, the reason goes to stderr and the return status is 1.
#
# The logic itself is cmake/BuildDir.cmake's -- see it for the details and
# the one-line answer protocol parsed here.
_pmn_resolve_build_dir() {
local answer
# run.sh never needed cmake before it had to ask which build directory to
# use, so say what's wrong instead of letting the shell's own "command not
# found" and an empty answer speak for it.
if ! command -v cmake >/dev/null 2>&1; then
echo "error: cmake not found on PATH -- it is needed to work out which build directory to use." >&2
echo " Put it on PATH, or set BUILD_DIR to the directory explicitly, e.g. BUILD_DIR=out/build/<preset>." >&2
return 1
fi
answer="$(cmake "-DPMN_NAME=${1:-}" -P cmake/BuildDir.cmake | sed -E 's/^-- //')"
case "$answer" in
BUILD_DIR=*)
printf '%s\n' "${answer#BUILD_DIR=}"
;;
ERROR=*)
echo "error: ${answer#ERROR=}" >&2
return 1
;;
*)
echo "error: cmake/BuildDir.cmake did not answer as expected: ${answer}" >&2
return 1
;;
esac
}
# _pmn_record_build_dir <name>
#
# Remembers <name> as the build directory built last, so the next
# `scripts/run.sh` / `install.sh` / `docs.sh` without a --preset uses it.
_pmn_record_build_dir() {
cmake "-DPMN_NAME=$1" -DPMN_RECORD=ON -P cmake/BuildDir.cmake >/dev/null
}
# On macOS, Homebrew's llvm formula -- the one that ships clang-tidy -- is
# keg-only: installed, but deliberately not linked into PATH, so a script
# looking for `clang-tidy` can't find it. Add its bin directory to the END of
# PATH (never the front: Apple's clang must stay the one builds use). A
# no-op everywhere else, and when llvm isn't installed.
_pmn_extend_tool_path() {
[ "$(uname -s)" = Darwin ] || return 0
local d
for d in /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin; do
if [ -d "$d" ]; then
case ":$PATH:" in
*":$d:"*) ;;
*) PATH="$PATH:$d" ;;
esac
return 0
fi
done
return 0
}
+581
View File
@@ -0,0 +1,581 @@
<#
.SYNOPSIS
Interactive installer for the tools this project builds with. Windows;
called by scripts\setup.bat (Linux and macOS have scripts/_tools.sh).
.DESCRIPTION
Checks each tool, and for every one that is missing asks -- ONE TOOL AT
A TIME -- whether to install it, showing the exact command first. Nothing
is installed without an explicit yes (or -Yes / -All).
Required Git, CMake 3.25+, and a C++ compiler: at least one of
Visual Studio Build Tools (MSVC) or MinGW-w64 (via MSYS2)
Optional LLVM (clang-cl, Clang, clang-format, clang-tidy), and the
other MSYS2 toolchains (32-bit MinGW, CLANG64)
Installs go through winget (Windows Package Manager, part of the App
Installer that ships with Windows 10 1809+ and Windows 11). Two things
winget does not do for you, and this script does:
* put CMake and LLVM on PATH -- their installers leave it to you, and
every guide that runs `winget install Kitware.CMake` ends with "now
add C:\Program Files\CMake\bin to PATH by hand". A tool found in its
usual folder but not on PATH is added to this session's PATH (which
is handed back to setup.bat through -PathOut so the rest of setup
can use it) and, if you agree, to your user PATH permanently.
* update a fresh MSYS2, whose package databases are stale (twice, as
MSYS2 itself recommends -- updating its runtime asks for a restart)
before installing MinGW-w64 into it.
Written for Windows PowerShell 5.1 as much as for PowerShell 7: no ??,
no ternary, no && / ||, nested Join-Path (5.1 has no -AdditionalChildPath),
and plain ASCII only (5.1 reads a BOM-less file as the ANSI code page).
.PARAMETER Yes
Install the REQUIRED tools without asking. For scripts and CI. Without a
terminal and without this, nothing is asked and nothing is installed.
.PARAMETER All
Also install the optional tools without asking (implies -Yes).
.PARAMETER Check
Only report what is and is not installed; change nothing. Exits 1 if a
required tool is missing.
.PARAMETER PathOut
File to write the directories that were added to this session's PATH to,
one per line, so the calling batch file can add them to its own.
.NOTES
Exit status: 0 if every required tool is present at the end, 1 if not.
#>
[CmdletBinding()]
param(
[switch]$Yes,
[switch]$All,
[switch]$Check,
[string]$PathOut = ''
)
if ($All) { $Yes = $true }
# Oldest CMake this project supports (cmake_minimum_required in
# CMakeLists.txt, and the presets file's cmakeMinimumRequired).
$script:MinCmake = New-Object System.Version 3, 25
# The Visual Studio Build Tools package. 'Microsoft.VisualStudio.BuildTools'
# (no year) is the newest release; the 2022 one is the long-established,
# documented path, so it is the default -- change the id here to prefer
# the newer one.
$script:MsvcWingetId = 'Microsoft.VisualStudio.2022.BuildTools'
$script:MsvcOverride = '--passive --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended'
# Answers that carry over from one prompt to the next.
$script:AnsAll = $false # "a": yes to everything still to come
$script:AnsSkipOptional = $false # "s": no to every OPTIONAL tool still to come
$script:AnsQuit = $false # "q": stop asking altogether
# Directories added to this session's PATH, for setup.bat to pick up.
$script:PathAdds = New-Object System.Collections.ArrayList
# ---------------------------------------------------------------------------
# Small helpers
# ---------------------------------------------------------------------------
function Say([string]$Text = '') { Write-Host $Text }
function Note([string]$Text) { Write-Host $Text -ForegroundColor DarkGray }
function Good([string]$Text) { Write-Host $Text -ForegroundColor Green }
function Warn([string]$Text) { Write-Host $Text -ForegroundColor Yellow }
function Bad([string]$Text) { Write-Host $Text -ForegroundColor Red }
# Nested Join-Path: Windows PowerShell 5.1's Join-Path takes only two parts.
function Join-Parts {
$p = [string]$args[0]
for ($i = 1; $i -lt $args.Count; $i++) { $p = Join-Path $p ([string]$args[$i]) }
return $p
}
function Get-ProgramFiles { return $env:ProgramFiles }
function Get-ProgramFilesX86 { return ${env:ProgramFiles(x86)} }
# MSYS2's install folder: C:\msys64 unless MSYS2_ROOT says otherwise.
function Get-Msys2Root {
if ($env:MSYS2_ROOT) { return $env:MSYS2_ROOT }
$drive = $env:SystemDrive
if (-not $drive) { $drive = 'C:' }
return (Join-Path ($drive + '\') 'msys64')
}
# The first executable called $Name on PATH, or one in $FallbackDirs (the
# folder the installer puts it in even when it doesn't touch PATH), or $null.
function Find-Exe([string]$Name, [string[]]$FallbackDirs) {
$cmd = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1
if ($cmd) { return $cmd.Source }
foreach ($dir in $FallbackDirs) {
if (-not $dir) { continue }
foreach ($ext in @('.exe', '')) {
$candidate = Join-Path $dir ($Name + $ext)
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
}
}
return $null
}
function New-Status([string]$State, [string]$Detail = '') {
return [pscustomobject]@{ State = $State; Detail = $Detail }
}
# ---------------------------------------------------------------------------
# PATH (session and user)
#
# Wrapped in functions so a test can replace the two that touch the real
# registry.
# ---------------------------------------------------------------------------
function Get-UserPathValue { return [Environment]::GetEnvironmentVariable('Path', 'User') }
function Set-UserPathValue([string]$Value) { [Environment]::SetEnvironmentVariable('Path', $Value, 'User') }
function Get-MachinePathValue { return [Environment]::GetEnvironmentVariable('Path', 'Machine') }
function Split-PathList([string]$List) {
if (-not $List) { return @() }
$sep = [System.IO.Path]::PathSeparator
$items = @()
foreach ($part in $List.Split($sep)) {
$trimmed = $part.Trim().TrimEnd('\', '/')
if ($trimmed) { $items += $trimmed }
}
return $items
}
function Test-InList([string]$List, [string]$Dir) {
$want = $Dir.TrimEnd('\', '/')
foreach ($item in (Split-PathList $List)) {
if ($item -ieq $want) { return $true }
}
return $false
}
function Test-OnSessionPath([string]$Dir) { return (Test-InList $env:Path $Dir) }
function Test-OnPersistentPath([string]$Dir) {
return ((Test-InList (Get-UserPathValue) $Dir) -or (Test-InList (Get-MachinePathValue) $Dir))
}
function Add-ToSessionPath([string]$Dir) {
if (Test-OnSessionPath $Dir) { return }
$sep = [System.IO.Path]::PathSeparator
$current = $env:Path
if (-not $current) { $current = '' }
$env:Path = $current.TrimEnd($sep) + $sep + $Dir
[void]$script:PathAdds.Add($Dir)
}
function Add-ToUserPath([string]$Dir) {
$current = Get-UserPathValue
$sep = [System.IO.Path]::PathSeparator
if ($current) { $new = $current.TrimEnd($sep) + $sep + $Dir } else { $new = $Dir }
Set-UserPathValue $new
}
# ---------------------------------------------------------------------------
# Detection
# ---------------------------------------------------------------------------
function Get-WingetStatus {
if (Get-Command winget -ErrorAction SilentlyContinue) { return (New-Status 'ok' 'winget') }
return (New-Status 'missing')
}
function Get-GitStatus {
$exe = Find-Exe 'git' @((Join-Parts (Get-ProgramFiles) 'Git' 'cmd'))
if (-not $exe) { return (New-Status 'missing') }
$ver = ''
try { $ver = (& $exe --version 2>$null | Select-Object -First 1) -replace '^git version ', '' } catch { }
return (New-Status 'ok' $ver)
}
function Get-CmakeStatus {
$exe = Find-Exe 'cmake' @((Join-Parts (Get-ProgramFiles) 'CMake' 'bin'))
if (-not $exe) { return (New-Status 'missing') }
$ver = ''
try {
$first = & $exe --version 2>$null | Select-Object -First 1
if ($first -match '(\d+\.\d+(\.\d+)?)') { $ver = $Matches[1] }
} catch { }
if (-not $ver) { return (New-Status 'ok' '') }
$parsed = $null
if ([System.Version]::TryParse($ver, [ref]$parsed) -and $parsed -lt $script:MinCmake) {
return (New-Status 'old' $ver)
}
return (New-Status 'ok' $ver)
}
# MSVC: the Visual Studio Installer ships vswhere.exe, which knows whether
# the C++ x86/x64 tools are installed in ANY Visual Studio or Build Tools.
function Get-MsvcStatus {
$pf86 = Get-ProgramFilesX86
if (-not $pf86) { return (New-Status 'missing') }
$vswhere = Join-Parts $pf86 'Microsoft Visual Studio' 'Installer' 'vswhere.exe'
if (-not (Test-Path -LiteralPath $vswhere -PathType Leaf)) { return (New-Status 'missing') }
$where = $null
try {
$where = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null | Select-Object -First 1
} catch { }
if ($where) { return (New-Status 'ok' ([string]$where)) }
return (New-Status 'missing')
}
function Get-LlvmStatus {
$exe = Find-Exe 'clang-cl' @((Join-Parts (Get-ProgramFiles) 'LLVM' 'bin'))
if (-not $exe) { return (New-Status 'missing') }
return (New-Status 'ok' (Split-Path -Parent $exe))
}
function Get-Msys2Status {
$bash = Join-Parts (Get-Msys2Root) 'usr' 'bin' 'bash.exe'
if (Test-Path -LiteralPath $bash -PathType Leaf) { return (New-Status 'ok' (Get-Msys2Root)) }
return (New-Status 'missing')
}
function Get-MingwStatus([string]$Subdir) {
$gpp = Join-Parts (Get-Msys2Root) $Subdir 'bin' 'g++.exe'
if (Test-Path -LiteralPath $gpp -PathType Leaf) { return (New-Status 'ok' (Split-Path -Parent $gpp)) }
return (New-Status 'missing')
}
function Get-Clang64Status {
$clang = Join-Parts (Get-Msys2Root) 'clang64' 'bin' 'clang++.exe'
if (Test-Path -LiteralPath $clang -PathType Leaf) { return (New-Status 'ok' (Split-Path -Parent $clang)) }
return (New-Status 'missing')
}
# ---------------------------------------------------------------------------
# Asking
# ---------------------------------------------------------------------------
function Test-CanAsk { return (-not [Console]::IsInputRedirected) }
# One line from the user, or $null at end of input. A function of its own so
# a test can stand in for the console.
function Read-Answer { return [Console]::ReadLine() }
# Would an OPTIONAL tool be offered (asked about, or installed by -All) now?
function Test-OptionalWanted {
if ($script:AnsQuit -or $script:AnsSkipOptional) { return $false }
if ($All -or $script:AnsAll) { return $true }
return (Test-CanAsk)
}
# Returns $true for "install it". Sets $script:Ans* for answers that outlast
# one prompt.
function Confirm-Install([string]$Kind, [string]$Question) {
if ($script:AnsQuit) { return $false }
if ($script:AnsAll) { return $true }
if ($Kind -eq 'optional' -and $script:AnsSkipOptional) { return $false }
# Flags answer for you...
if ($Kind -eq 'required' -and $Yes) { return $true }
if ($All) { return $true }
# ...and with no terminal there is nobody to ask.
if (-not (Test-CanAsk)) {
Note ' (not asking: no terminal. Re-run interactively, or pass --yes / --all.)'
return $false
}
if ($Kind -eq 'required') { $prompt = " $Question [Y/n/a/q] " } else { $prompt = " $Question [y/N/a/s/q] " }
while ($true) {
Write-Host $prompt -NoNewline
$answer = Read-Answer
if ($null -eq $answer) {
# End of input (Ctrl-Z): stop asking rather than loop on it.
Write-Host ''
$script:AnsQuit = $true
return $false
}
switch ($answer.Trim().ToLowerInvariant()) {
{ $_ -in @('y', 'yes') } { return $true }
{ $_ -in @('n', 'no') } { return $false }
'' { return ($Kind -eq 'required') }
{ $_ -in @('a', 'all') } { $script:AnsAll = $true; return $true }
{ $_ -in @('s', 'skip') } { $script:AnsSkipOptional = $true; return $false }
{ $_ -in @('q', 'quit') } { $script:AnsQuit = $true; return $false }
default {
if ($Kind -eq 'required') {
Note ' y = install n = not this one a = install everything left q = stop asking'
} else {
Note ' y = install n = not this one a = install everything left s = skip the rest of the optional tools q = stop asking'
}
}
}
}
}
# ---------------------------------------------------------------------------
# Installing
# ---------------------------------------------------------------------------
function Invoke-Winget([string]$Id, [string]$Override = '') {
$winget = @('install', '--id', $Id, '-e', '--source', 'winget',
'--accept-package-agreements', '--accept-source-agreements')
$shown = 'winget ' + ($winget -join ' ')
if ($Override) {
$winget += @('--override', $Override)
$shown += ' --override "' + $Override + '"'
}
Write-Host (' $ ' + $shown) -ForegroundColor DarkGray
& winget @winget
return ($LASTEXITCODE -eq 0)
}
# MSYS2's own advice for a fresh install: its package databases are stale, so
# update the base system first, twice (updating the runtime asks for a
# restart of the shell), then install into it.
function Invoke-Msys2Packages([string[]]$Packages) {
$bash = Join-Parts (Get-Msys2Root) 'usr' 'bin' 'bash.exe'
if (-not (Test-Path -LiteralPath $bash -PathType Leaf)) {
Bad ' MSYS2 is not installed at the expected place -- install it first.'
return $false
}
Write-Host (' $ ' + $bash + ' -lc "pacman --noconfirm -Syuu" (twice; a fresh MSYS2 needs it)') -ForegroundColor DarkGray
& $bash -lc 'pacman --noconfirm -Syuu'
& $bash -lc 'pacman --noconfirm -Syuu'
$list = $Packages -join ' '
Write-Host (' $ ' + $bash + ' -lc "pacman -S --needed --noconfirm ' + $list + '"') -ForegroundColor DarkGray
& $bash -lc ('pacman -S --needed --noconfirm ' + $list)
return ($LASTEXITCODE -eq 0)
}
# ---------------------------------------------------------------------------
# The catalog
#
# Ordered as they are asked. Each row: a stable Id, whether it is Required or
# optional (msvc is decided at run time -- see below), a label, why it
# matters, how to tell if it is there, how to install it, and the folder
# that has to be on PATH for it to be usable (or $null).
# ---------------------------------------------------------------------------
function Get-Catalog {
$pf = Get-ProgramFiles
$msys = Get-Msys2Root
return @(
@{ Id = 'git'; Kind = 'required'; Label = 'Git';
Why = 'FetchContent clones dependencies (fmt) with it';
Status = { Get-GitStatus }; Install = { Invoke-Winget 'Git.Git' };
PathDir = (Join-Parts $pf 'Git' 'cmd'); PathDefault = $true },
@{ Id = 'cmake'; Kind = 'required'; Label = 'CMake 3.25 or newer';
Why = 'configures and drives every build';
Status = { Get-CmakeStatus }; Install = { Invoke-Winget 'Kitware.CMake' };
PathDir = (Join-Parts $pf 'CMake' 'bin'); PathDefault = $true },
# A compiler: at least one of MSVC or MinGW-w64 is required. Which
# kind msvc is depends on whether one is already there (see Main).
@{ Id = 'msvc'; Kind = 'compiler'; Label = 'Visual Studio Build Tools (MSVC)';
Why = 'the windows-*-msvc presets, and the headers/libraries the windows-*-clang-cl ones need';
Status = { Get-MsvcStatus };
Install = { Invoke-Winget $script:MsvcWingetId $script:MsvcOverride };
PathDir = $null },
@{ Id = 'llvm'; Kind = 'optional'; Label = 'LLVM (clang-cl, Clang, clang-format, clang-tidy)';
Why = 'the windows-*-clang-cl presets, scripts\refactor.bat and the pre-commit hook';
Status = { Get-LlvmStatus }; Install = { Invoke-Winget 'LLVM.LLVM' };
PathDir = (Join-Parts $pf 'LLVM' 'bin'); PathDefault = $true },
@{ Id = 'msys2'; Kind = 'optional'; Label = 'MSYS2';
Why = 'the home of MinGW-w64 on Windows (needed for the windows-*-mingw* presets)';
Status = { Get-Msys2Status }; Install = { Invoke-Winget 'MSYS2.MSYS2' };
PathDir = $null },
@{ Id = 'mingw64'; Kind = 'optional'; Label = 'MinGW-w64 GCC, 64-bit';
Why = 'the windows-x64-mingw64 preset'; Needs = 'msys2';
Status = { Get-MingwStatus 'mingw64' };
Install = { Invoke-Msys2Packages @('mingw-w64-x86_64-gcc') };
PathDir = (Join-Parts $msys 'mingw64' 'bin'); PathDefault = $false },
@{ Id = 'mingw32'; Kind = 'optional'; Label = 'MinGW-w64 GCC, 32-bit';
Why = 'the windows-x86-mingw32 preset'; Needs = 'msys2';
Status = { Get-MingwStatus 'mingw32' };
Install = { Invoke-Msys2Packages @('mingw-w64-i686-gcc') };
PathDir = (Join-Parts $msys 'mingw32' 'bin'); PathDefault = $false },
@{ Id = 'clang64'; Kind = 'optional'; Label = 'MSYS2 CLANG64 (GNU-style Clang)';
Why = 'the windows-x64-clang preset'; Needs = 'msys2';
Status = { Get-Clang64Status };
Install = { Invoke-Msys2Packages @('mingw-w64-clang-x86_64-clang') };
PathDir = (Join-Parts $msys 'clang64' 'bin'); PathDefault = $false }
)
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
function Show-ManualHelp {
Say ' Install these yourself instead:'
Say ' Git https://git-scm.com/download/win'
Say ' CMake https://cmake.org/download/ (tick "Add CMake to the system PATH")'
Say ' MSVC https://visualstudio.microsoft.com/visual-cpp-build-tools/ (workload: Desktop development with C++)'
Say ' LLVM https://github.com/llvm/llvm-project/releases'
Say ' MSYS2 https://www.msys2.org/'
}
function Invoke-Main {
$catalog = Get-Catalog
$wingetStatus = Get-WingetStatus
$haveWinget = ($wingetStatus.State -eq 'ok')
Say ('==> Checking the tools this project builds with (Windows, winget: ' + $(if ($haveWinget) { 'found' } else { 'NOT found' }) + ')')
# ---- report ---------------------------------------------------------
$status = @{}
foreach ($row in $catalog) { $status[$row.Id] = (& $row.Status) }
$haveCompiler = ($status['msvc'].State -eq 'ok') -or ($status['mingw64'].State -eq 'ok') -or ($status['mingw32'].State -eq 'ok')
foreach ($row in $catalog) {
$st = $status[$row.Id]
$kind = $row.Kind
if ($kind -eq 'compiler') { if ($haveCompiler) { $kind = 'optional' } else { $kind = 'required' } }
$row.EffKind = $kind
switch ($st.State) {
'ok' {
if ($st.Detail) { Write-Host (' [ ok ] ' + $row.Label.PadRight(50) + ' ') -NoNewline -ForegroundColor Green; Note $st.Detail }
else { Good (' [ ok ] ' + $row.Label) }
}
'old' { Warn (' [old ] ' + $row.Label.PadRight(50) + ' ' + $st.Detail + ' (needs ' + $script:MinCmake.ToString() + '+)') }
default {
if ($kind -eq 'required') { Bad (' [ !! ] ' + $row.Label.PadRight(50) + ' missing (required)') }
else { Note (' [ -- ] ' + $row.Label.PadRight(50) + ' missing (optional)') }
}
}
}
if (-not $haveCompiler) {
Warn ' A C++ compiler is required: Visual Studio Build Tools, or MinGW-w64 through MSYS2 (either one).'
}
# ---- -Check: report only --------------------------------------------
if ($Check) {
$missing = ($status['git'].State -ne 'ok') -or ($status['cmake'].State -ne 'ok') -or (-not $haveCompiler)
if ($missing) { return 1 }
return 0
}
$anythingToInstall = $false
foreach ($row in $catalog) { if ($status[$row.Id].State -ne 'ok') { $anythingToInstall = $true } }
if ($anythingToInstall -and -not $haveWinget) {
Say ''
Warn ' winget (Windows Package Manager) was not found, so this script cannot install anything.'
Say ' It comes with the App Installer: https://aka.ms/getwinget (or the Microsoft Store).'
Show-ManualHelp
}
# ---- offer installs, one at a time ----------------------------------
$skipped = @()
$failed = @()
foreach ($row in $catalog) {
if ($script:AnsQuit) { break }
$st = & $row.Status # re-check: an earlier install may have provided it
if ($st.State -eq 'ok') { continue }
if (-not $haveWinget) { continue }
# MinGW rows live inside MSYS2.
if ($row.Needs) {
$needRow = $catalog | Where-Object { $_.Id -eq $row.Needs } | Select-Object -First 1
$needed = & $needRow.Status
if ($needed.State -ne 'ok') { continue }
}
# After the msvc row, "a compiler exists" may have changed the kind
# of the rows still to come.
$kind = $row.EffKind
if ($kind -eq 'required' -and $row.Id -eq 'msvc') {
$now = ((Get-MingwStatus 'mingw64').State -eq 'ok') -or ((Get-MingwStatus 'mingw32').State -eq 'ok')
if ($now) { $kind = 'optional' }
}
if ($kind -eq 'optional' -and -not (Test-OptionalWanted)) {
$skipped += $row.Label
continue
}
Say ''
Write-Host (' ' + $row.Label) -NoNewline -ForegroundColor White
Say (' -- ' + $row.Why)
if ($st.State -eq 'old') { Note (' (installed: ' + $st.Detail + ', but too old)') }
if (Confirm-Install $kind ('Install ' + $row.Label + '?')) {
$ok = & $row.Install
$after = & $row.Status
if ($after.State -eq 'ok') {
Good ' installed'
} elseif ($after.State -eq 'old') {
Warn (' CMake ' + $after.Detail + ' is older than the ' + $script:MinCmake.ToString() + ' this project needs.')
$failed += $row.Label
} elseif (-not $ok) {
Bad ' install failed -- see the output above.'
$failed += $row.Label
} else {
Warn (' the install finished, but ' + $row.Label + ' still is not found -- open a new terminal, or check the output above.')
$failed += $row.Label
}
} elseif ($kind -eq 'optional') {
$skipped += $row.Label
}
}
if ($skipped.Count -gt 0) {
Say ''
Note (' Optional, not installed: ' + ($skipped -join ', '))
Note ' (scripts\setup.bat, run interactively, offers each one; --all installs them all.)'
}
# ---- PATH ------------------------------------------------------------
# A tool that is installed but whose folder is not on PATH is unusable
# from a plain terminal -- and, for CMake, from the rest of setup.bat.
foreach ($row in $catalog) {
if (-not $row.PathDir) { continue }
$st = & $row.Status
if ($st.State -ne 'ok') { continue }
if (-not (Test-Path -LiteralPath $row.PathDir -PathType Container)) { continue }
if (Test-OnSessionPath $row.PathDir) { continue }
# This session, always: what follows in setup needs it.
Add-ToSessionPath $row.PathDir
if (Test-OnPersistentPath $row.PathDir) { continue }
Say ''
Say (' ' + $row.Label + ' is installed, but its folder is not on your PATH:')
Say (' ' + $row.PathDir)
if ($row.PathDefault) { $kind = 'required' } else { $kind = 'optional' }
if (-not $row.PathDefault) {
Note ' (Adding MSYS2 toolchain folders puts their g++/gcc on PATH for every program; the MinGW presets use'
Note ' the prefixed names, so you can also leave this out and use an MSYS2 shell instead.)'
}
if (Confirm-Install $kind 'Add it to your user PATH (new terminals will see it)?') {
Add-ToUserPath $row.PathDir
Good ' added -- open a new terminal to use it there.'
}
}
# ---- hand this session's PATH additions back to setup.bat ------------
if ($PathOut -and $script:PathAdds.Count -gt 0) {
Set-Content -LiteralPath $PathOut -Value ($script:PathAdds.ToArray()) -Encoding ASCII
}
# ---- verdict ---------------------------------------------------------
$exit = 0
$needGit = (Get-GitStatus).State -ne 'ok'
$needCmake = (Get-CmakeStatus).State -ne 'ok'
$needCompiler = -not (((Get-MsvcStatus).State -eq 'ok') -or ((Get-MingwStatus 'mingw64').State -eq 'ok') -or ((Get-MingwStatus 'mingw32').State -eq 'ok'))
if ($needGit) { Bad ' still missing (required): Git'; $exit = 1 }
if ($needCmake) { Bad ' still missing (required): CMake 3.25 or newer'; $exit = 1 }
if ($needCompiler) { Bad ' still missing (required): a C++ compiler (Visual Studio Build Tools, or MinGW-w64 via MSYS2)'; $exit = 1 }
if ($failed.Count -gt 0 -and $exit -eq 0) { Warn (' These did not install: ' + ($failed -join ', ')) }
return $exit
}
# Run only when executed, not when dot-sourced (a test dot-sources this file
# to replace the functions that touch the registry).
if ($MyInvocation.InvocationName -ne '.') {
$code = Invoke-Main
exit $code
}
+626
View File
@@ -0,0 +1,626 @@
# Interactive installer for the tools this project builds with. Sourced by
# scripts/setup.sh (Linux and macOS -- Windows has scripts/_tools.ps1); never
# run directly, hence the leading underscore and no executable bit.
#
# What it does: works out what OS and package manager it's on, checks each
# tool in a catalog, and for every one that's missing asks -- ONE TOOL AT A
# TIME -- whether to install it, showing the exact command first. Nothing is
# installed without an explicit yes (or --yes / --all, see below).
#
# Written for bash 3.2, which is what macOS ships as /bin/bash: no
# associative arrays, no mapfile, no ${var,,}, and every array that can be
# empty is expanded with the ${arr[@]+"${arr[@]}"} idiom.
#
# Package managers: apt (Debian/Ubuntu), dnf (Fedora/RHEL), pacman (Arch) on
# Linux; Homebrew on macOS. Anything else is told what the tools are and
# left to install them itself. The cross-compilers, MinGW and emulators
# are offered on apt systems only: that is the package layout
# cmake/toolchains/ was written and tested against (a cross toolchain's
# target files under /usr/<triplet>), and offering a package that would
# then not work is worse than not offering it.
#
# Entry point: _pmn_tools_run
# Environment in (set by setup.sh from its flags):
# PMN_TOOLS_YES=1 install REQUIRED tools without asking
# PMN_TOOLS_ALL=1 also install OPTIONAL tools without asking
# PMN_TOOLS_CHECK=1 only report; change nothing
# Returns 0 if every required tool is present at the end, 1 if not.
#
# Without a terminal on stdin and without --yes/--all it does not prompt (a
# CI job must never hang on a question nobody can answer): it reports and
# leaves everything alone.
# Oldest CMake this project supports (cmake_minimum_required in
# CMakeLists.txt, and the presets file's cmakeMinimumRequired).
PMN_CMAKE_MIN_MAJOR=3
PMN_CMAKE_MIN_MINOR=25
PMN_TOOLS_YES="${PMN_TOOLS_YES:-0}"
PMN_TOOLS_ALL="${PMN_TOOLS_ALL:-0}"
PMN_TOOLS_CHECK="${PMN_TOOLS_CHECK:-0}"
# Answers that carry over from one prompt to the next.
_PMN_ANS_ALL=0 # "a": yes to everything still to come
_PMN_ANS_SKIP_OPT=0 # "s": no to every OPTIONAL tool still to come
_PMN_ANS_QUIT=0 # "q": stop asking altogether
_PMN_APT_UPDATED=0 # apt-get update runs once, before the first apt install
if [ -t 1 ]; then
_PMN_BOLD=$'\033[1m'; _PMN_DIM=$'\033[2m'; _PMN_GREEN=$'\033[32m'
_PMN_YELLOW=$'\033[33m'; _PMN_RED=$'\033[31m'; _PMN_RESET=$'\033[0m'
else
_PMN_BOLD=''; _PMN_DIM=''; _PMN_GREEN=''; _PMN_YELLOW=''; _PMN_RED=''; _PMN_RESET=''
fi
# ---------------------------------------------------------------------------
# Detection
# ---------------------------------------------------------------------------
_pmn_have() { command -v "$1" >/dev/null 2>&1; }
# The two directories Homebrew's llvm formula is keg-only in (Apple Silicon,
# Intel). Keg-only: installed, but deliberately not linked into PATH, which
# is where clang-tidy comes from.
_pmn_brew_llvm_bin() {
local d
for d in /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin; do
if [ -d "$d" ]; then printf '%s\n' "$d"; return 0; fi
done
return 1
}
# Sets PMN_OS (linux|macos|other), PMN_ARCH (x64|arm64|arm32|""), PMN_PM
# (apt|dnf|pacman|brew|none) and PMN_SUDO ("" or "sudo").
_pmn_tools_detect() {
local machine
case "$(uname -s)" in
Linux) PMN_OS=linux ;;
Darwin) PMN_OS=macos ;;
*) PMN_OS=other ;;
esac
machine="$(uname -m)"
# An x86_64 shell under Rosetta 2 on an Apple Silicon Mac still reports
# x86_64 -- ask about the machine (see cmake/HostArch.cmake).
if [ "$PMN_OS" = macos ] && [ "$machine" = x86_64 ] \
&& [ "$(sysctl -in hw.optional.arm64 2>/dev/null || true)" = 1 ]; then
machine=arm64
fi
case "$machine" in
x86_64|amd64) PMN_ARCH=x64 ;;
aarch64|arm64) PMN_ARCH=arm64 ;;
arm*) PMN_ARCH=arm32 ;;
*) PMN_ARCH="" ;;
esac
PMN_PM=none
if [ "$PMN_OS" = macos ]; then
# Homebrew is often installed but not on PATH in a fresh shell.
if ! _pmn_have brew; then
local b
for b in /opt/homebrew/bin/brew /usr/local/bin/brew; do
if [ -x "$b" ]; then eval "$("$b" shellenv)"; break; fi
done
fi
if _pmn_have brew; then PMN_PM=brew; fi
elif [ "$PMN_OS" = linux ]; then
if _pmn_have apt-get; then PMN_PM=apt
elif _pmn_have dnf; then PMN_PM=dnf
elif _pmn_have pacman; then PMN_PM=pacman
fi
fi
# Root is only a question for Linux's system package managers. Homebrew
# refuses to run as root and never needs it -- and decided HERE, before
# a fresh Homebrew exists, this would otherwise be computed against
# PMN_PM=none.
PMN_SUDO=""
if [ "$PMN_OS" = linux ] && [ "$(id -u)" != 0 ]; then
if _pmn_have sudo; then PMN_SUDO=sudo; else PMN_SUDO="?"; fi
fi
}
# "3.28.3" for the installed cmake, or "" if there isn't one.
_pmn_cmake_version() {
_pmn_have cmake || return 0
cmake --version 2>/dev/null | sed -n '1s/^[^0-9]*\([0-9][0-9.]*\).*/\1/p'
}
# 0 if the installed cmake is at least the minimum.
_pmn_cmake_new_enough() {
local v major minor
v="$(_pmn_cmake_version)"
[ -n "$v" ] || return 1
major="${v%%.*}"; minor="${v#*.}"; minor="${minor%%.*}"
if [ "$major" -gt "$PMN_CMAKE_MIN_MAJOR" ]; then return 0; fi
[ "$major" -eq "$PMN_CMAKE_MIN_MAJOR" ] && [ "$minor" -ge "$PMN_CMAKE_MIN_MINOR" ]
}
# macOS: /usr/bin/git, /usr/bin/clang and friends exist on every Mac as
# stubs that pop up an installer dialog when run without the Command Line
# Tools -- `command -v` finds them regardless. The developer directory is
# what says whether real tools sit behind them.
_pmn_clt_installed() {
local dir
dir="$(xcode-select -p 2>/dev/null)" || return 1
[ -x "$dir/usr/bin/clang" ]
}
# _pmn_tool_status <id>: prints "ok <detail>", "old <detail>" or "missing".
_pmn_tool_status() {
local v p
case "$1" in
clt)
if _pmn_clt_installed; then echo "ok $(xcode-select -p)"; else echo missing; fi ;;
brew)
if _pmn_have brew; then echo "ok $(command -v brew)"; else echo missing; fi ;;
git)
if _pmn_have git; then echo "ok $(git --version | sed 's/^git version //')"; else echo missing; fi ;;
cmake)
v="$(_pmn_cmake_version)"
if [ -z "$v" ]; then echo missing
elif _pmn_cmake_new_enough; then echo "ok $v"
else echo "old $v"; fi ;;
cxx)
if _pmn_have g++; then echo "ok g++ $(g++ -dumpversion 2>/dev/null)"
elif _pmn_have clang++; then echo "ok clang++"
elif _pmn_have c++; then echo "ok c++"
else echo missing; fi ;;
clang)
if _pmn_have clang++; then echo "ok $(clang++ --version 2>/dev/null | sed -n '1s/.*version \([0-9][0-9.]*\).*/\1/p')"; else echo missing; fi ;;
clang-format|clang-tidy)
if _pmn_have "$1"; then echo "ok $(command -v "$1")"
elif p="$(_pmn_brew_llvm_bin)" && [ -x "$p/$1" ]; then echo "ok $p/$1"
else echo missing; fi ;;
xc-arm64) if _pmn_have aarch64-linux-gnu-g++; then echo "ok"; else echo missing; fi ;;
xc-arm32) if _pmn_have arm-linux-gnueabihf-g++; then echo "ok"; else echo missing; fi ;;
xc-x64) if _pmn_have x86_64-linux-gnu-g++; then echo "ok"; else echo missing; fi ;;
mingw64) if _pmn_have x86_64-w64-mingw32-g++; then echo "ok"; else echo missing; fi ;;
mingw32) if _pmn_have i686-w64-mingw32-g++; then echo "ok"; else echo missing; fi ;;
qemu) if _pmn_have qemu-aarch64 || _pmn_have qemu-arm || _pmn_have qemu-x86_64; then echo "ok"; else echo missing; fi ;;
wine) if _pmn_have wine || _pmn_have wine64; then echo "ok"; else echo missing; fi ;;
ninja) if _pmn_have ninja; then echo "ok $(ninja --version 2>/dev/null)"; else echo missing; fi ;;
*) echo missing ;;
esac
}
# ---------------------------------------------------------------------------
# The catalog
#
# id | kind | label | why | apt | dnf | pacman | brew
#
# "-" means "not offered through that package manager". Rows are filtered per
# platform by _pmn_tools_rows below, and listed in the order they're asked.
# ---------------------------------------------------------------------------
_pmn_row_cat() {
case "$1" in
git) echo "git|required|Git|FetchContent clones dependencies (fmt) with it|git|git|git|git" ;;
cmake) echo "cmake|required|CMake $PMN_CMAKE_MIN_MAJOR.$PMN_CMAKE_MIN_MINOR or newer|configures and drives every build|cmake|cmake|cmake|cmake" ;;
cxx) echo "cxx|required|C++ compiler (GCC)|compiles the project|g++|gcc-c++|gcc|-" ;;
clang) echo "clang|optional|Clang|the linux-*-clang presets (GCC alone is enough otherwise)|clang|clang|clang|-" ;;
clang-format) echo "clang-format|optional|clang-format|scripts/refactor.sh and the pre-commit hook|clang-format|clang-tools-extra|clang|clang-format" ;;
clang-tidy) echo "clang-tidy|optional|clang-tidy|scripts/refactor.sh and the pre-commit hook|clang-tidy|clang-tools-extra|clang|llvm" ;;
xc-arm64) echo "xc-arm64|optional|AArch64 cross compiler|the linux-arm64-* presets, built from this machine|g++-aarch64-linux-gnu|-|-|-" ;;
xc-arm32) echo "xc-arm32|optional|32-bit ARM cross compiler|the linux-arm32-* presets, built from this machine|g++-arm-linux-gnueabihf|-|-|-" ;;
xc-x64) echo "xc-x64|optional|x86-64 cross compiler|the linux-x64-* presets, built from this machine|g++-x86-64-linux-gnu|-|-|-" ;;
mingw64) echo "mingw64|optional|MinGW-w64 (64-bit Windows)|the windows-x64-mingw64 preset, cross-built from Linux|g++-mingw-w64-x86-64|-|-|-" ;;
mingw32) echo "mingw32|optional|MinGW-w64 (32-bit Windows)|the windows-x86-mingw32 preset, cross-built from Linux|g++-mingw-w64-i686|-|-|-" ;;
qemu) echo "qemu|optional|qemu-user|runs ARM cross builds (scripts/run.sh, ctest)|qemu-user|-|-|-" ;;
wine) echo "wine|optional|wine|runs MinGW cross builds (scripts/run.sh, ctest)|wine|-|-|-" ;;
ninja) echo "ninja|required|Ninja|the build generator (no pinned download exists for this machine)|ninja-build|ninja-build|ninja|ninja" ;;
clt) echo "clt|required|Xcode Command Line Tools|Apple Clang, git and make: the compiler for the macos-* presets|-|-|-|-" ;;
brew) echo "brew|required|Homebrew|installs CMake and the other tools below|-|-|-|-" ;;
esac
}
# The ids to consider on this platform, in the order they are asked.
_pmn_tools_ids() {
if [ "$PMN_OS" = macos ]; then
echo "clt brew cmake clang-format clang-tidy"
return
fi
local ids="git cmake cxx clang clang-format clang-tidy"
# Cross toolchains: apt only (see the file header), and only for
# architectures this machine can build FOR without being them.
if [ "$PMN_PM" = apt ]; then
case "$PMN_ARCH" in
x64) ids="$ids xc-arm64 xc-arm32 mingw64 mingw32 qemu wine" ;;
arm64) ids="$ids xc-x64 xc-arm32 mingw64 mingw32 qemu" ;;
esac
fi
echo "$ids"
}
# Field <n> (1-based) of a catalog row. A `read` into named variables rather
# than word-splitting the row: the rows contain "*" (linux-*-clang), and an
# unquoted expansion would be subject to pathname globbing.
_pmn_field() {
local f1 f2 f3 f4 f5 f6 f7 f8
IFS='|' read -r f1 f2 f3 f4 f5 f6 f7 f8 <<< "$1"
case "$2" in
1) printf '%s\n' "$f1" ;; 2) printf '%s\n' "$f2" ;; 3) printf '%s\n' "$f3" ;;
4) printf '%s\n' "$f4" ;; 5) printf '%s\n' "$f5" ;; 6) printf '%s\n' "$f6" ;;
7) printf '%s\n' "$f7" ;; 8) printf '%s\n' "$f8" ;;
esac
}
# The package(s) that provide a row's tool through the current package
# manager, or "" if it isn't offered there.
_pmn_row_packages() {
local row="$1" col
case "$PMN_PM" in
apt) col=5 ;; dnf) col=6 ;; pacman) col=7 ;; brew) col=8 ;; *) return 0 ;;
esac
local p
p="$(_pmn_field "$row" "$col")"
[ "$p" = "-" ] && p=""
printf '%s\n' "$p"
}
# ---------------------------------------------------------------------------
# Asking
# ---------------------------------------------------------------------------
_pmn_say() { printf '%s\n' "$*"; }
_pmn_note() { printf '%s\n' "${_PMN_DIM}$*${_PMN_RESET}"; }
# Would an OPTIONAL tool be offered (asked about, or installed by --all) at
# this point? If not, the caller skips it silently and mentions it once at
# the end, instead of printing a "left alone" block for each.
_pmn_optional_wanted() {
if [ "$_PMN_ANS_QUIT" = 1 ] || [ "$_PMN_ANS_SKIP_OPT" = 1 ]; then return 1; fi
if [ "$PMN_TOOLS_ALL" = 1 ] || [ "$_PMN_ANS_ALL" = 1 ]; then return 0; fi
[ -t 0 ]
}
# _pmn_confirm <required|optional> <question>
#
# Returns 0 for "install it", 1 for "leave it". Sets _PMN_ANS_* for answers
# that outlast one prompt (see their comments up top).
_pmn_confirm() {
local kind="$1" question="$2" prompt ans
if [ "$_PMN_ANS_QUIT" = 1 ]; then return 1; fi
if [ "$_PMN_ANS_ALL" = 1 ]; then return 0; fi
if [ "$kind" = optional ] && [ "$_PMN_ANS_SKIP_OPT" = 1 ]; then return 1; fi
# Flags answer for you...
if [ "$kind" = required ] && [ "$PMN_TOOLS_YES" = 1 ]; then return 0; fi
if [ "$PMN_TOOLS_ALL" = 1 ]; then return 0; fi
# ...and with no terminal there is nobody to ask.
if [ ! -t 0 ]; then
_pmn_note " (not asking: no terminal. Re-run interactively, or pass --yes / --all.)"
return 1
fi
if [ "$kind" = required ]; then
prompt=" ${question} [Y/n/a/q] "
else
prompt=" ${question} [y/N/a/s/q] "
fi
while :; do
if ! IFS= read -r -p "$prompt" ans; then
# EOF (Ctrl-D): stop asking rather than loop on it.
echo
_PMN_ANS_QUIT=1
return 1
fi
case "$ans" in
y|Y|yes|YES|Yes) return 0 ;;
n|N|no|NO|No) return 1 ;;
"") if [ "$kind" = required ]; then return 0; else return 1; fi ;;
a|A|all) _PMN_ANS_ALL=1; return 0 ;;
s|S|skip) _PMN_ANS_SKIP_OPT=1; return 1 ;;
q|Q|quit) _PMN_ANS_QUIT=1; return 1 ;;
*)
if [ "$kind" = required ]; then
_pmn_note " y = install n = not this one a = install everything left q = stop asking"
else
_pmn_note " y = install n = not this one a = install everything left s = skip the rest of the optional tools q = stop asking"
fi
;;
esac
done
}
# ---------------------------------------------------------------------------
# Installing
# ---------------------------------------------------------------------------
# _pmn_run_shown <command...>: print the command, then run it.
_pmn_run_shown() {
printf ' %s$ %s%s\n' "${_PMN_DIM}" "$*" "${_PMN_RESET}"
"$@"
}
# Sudo, when the package manager needs it. A machine that is neither root
# nor has sudo can't install system packages; say so once, plainly.
_pmn_pkg_install() {
local pkgs="$1" rc=0
if [ "$PMN_SUDO" = "?" ] && [ "$PMN_PM" != brew ]; then
_pmn_say " ${_PMN_RED}cannot install:${_PMN_RESET} this needs root, and sudo isn't available."
_pmn_say " Run this script as root, or install by hand: $pkgs"
return 1
fi
# shellcheck disable=SC2086 # $pkgs is a deliberate word list
case "$PMN_PM" in
apt)
if [ "$_PMN_APT_UPDATED" = 0 ]; then
_pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} apt-get update || true
_PMN_APT_UPDATED=1
fi
_pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} apt-get install -y $pkgs || rc=$?
;;
dnf)
_pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} dnf install -y $pkgs || rc=$?
;;
pacman)
_pmn_run_shown ${PMN_SUDO:+"$PMN_SUDO"} pacman -S --needed --noconfirm $pkgs || rc=$?
;;
brew)
_pmn_run_shown brew install $pkgs || rc=$?
;;
*)
return 1 ;;
esac
return "$rc"
}
# macOS: `xcode-select --install` returns immediately after opening a GUI
# installer, so wait for the developer directory to appear. Enter re-checks
# right away; otherwise it looks every 5 seconds.
_pmn_install_clt() {
_pmn_run_shown xcode-select --install || true
_pmn_say " A dialog should have opened: click Install and wait for it to finish."
_pmn_say " Waiting for the tools (Enter checks now; Ctrl-C to stop waiting) ..."
local waited=0
until _pmn_clt_installed; do
# Not `read -t 5` with stdin at EOF: that returns at once and would
# spin. sleep is the wait; read only makes Enter an early re-check.
if [ -t 0 ]; then
IFS= read -r -t 5 _ 2>/dev/null || true
else
sleep 5
fi
waited=$((waited + 5))
if [ "$waited" -ge 3600 ]; then
_pmn_say " Gave up waiting after an hour -- re-run this script once the install has finished."
return 1
fi
done
return 0
}
# Homebrew's own documented installer. It asks for your password and for a
# RETURN of its own -- those prompts are Homebrew's, and are left alone
# unless --yes was given (then NONINTERACTIVE=1 tells it not to ask).
_pmn_install_brew() {
if ! _pmn_have curl; then
_pmn_say " curl not found -- see https://brew.sh for manual installation steps."
return 1
fi
printf ' %s$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"%s\n' "${_PMN_DIM}" "${_PMN_RESET}"
local rc=0
if [ "$PMN_TOOLS_YES" = 1 ] || [ "$PMN_TOOLS_ALL" = 1 ]; then
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || rc=$?
else
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || rc=$?
fi
# A fresh install isn't on PATH in this shell yet.
local b
for b in /opt/homebrew/bin/brew /usr/local/bin/brew; do
if [ -x "$b" ]; then eval "$("$b" shellenv)"; PMN_PM=brew; break; fi
done
return "$rc"
}
# _pmn_install_row <row>: install one catalog row; returns its status.
_pmn_install_row() {
local row="$1" id pkgs
id="$(_pmn_field "$row" 1)"
case "$id" in
clt) _pmn_install_clt; return $? ;;
brew) _pmn_install_brew; return $? ;;
esac
pkgs="$(_pmn_row_packages "$row")"
if [ -z "$pkgs" ]; then
_pmn_say " ${_PMN_YELLOW}no package known for this on ${PMN_PM}${_PMN_RESET} -- install it yourself."
return 1
fi
_pmn_pkg_install "$pkgs"
}
# What to tell someone whose package manager's CMake is too old (Ubuntu 22.04
# ships 3.22, Debian 11 ships 3.18). Not run automatically: which of these is
# right depends on the machine, and each one changes more than a package.
_pmn_cmake_too_old_help() {
_pmn_say " ${_PMN_YELLOW}CMake $1 is older than the ${PMN_CMAKE_MIN_MAJOR}.${PMN_CMAKE_MIN_MINOR} this project needs.${_PMN_RESET} Any one of:"
_pmn_say " pipx install cmake # Kitware's own wheels (pipx from your package manager)"
_pmn_say " sudo snap install cmake --classic # if you use snap"
_pmn_say " https://cmake.org/download/ # official binary tarball"
}
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
# Rows are collected into an array first, then looped over -- rather than
# reading a here-doc in the loop -- so a command run inside the loop (a
# package manager, sudo) can never swallow the rest of the list from stdin.
_pmn_tools_run() {
local rows=() id row kind label why status detail pkgs missing_required=0
local failed=""
local pm_desc
_pmn_tools_detect
case "$PMN_OS" in
linux) pm_desc="Linux ${PMN_ARCH:-?}, package manager: ${PMN_PM}" ;;
macos) pm_desc="macOS ${PMN_ARCH:-?}, package manager: ${PMN_PM}" ;;
*) pm_desc="$(uname -s) (not supported by this installer)" ;;
esac
_pmn_say "==> Checking the tools this project builds with (${pm_desc})"
if [ "$PMN_OS" = other ]; then
_pmn_say " This installer knows Linux and macOS; on Windows use scripts\\setup.bat."
return 0
fi
if [ "$PMN_OS" = linux ] && [ "$PMN_PM" = none ]; then
_pmn_say " ${_PMN_YELLOW}No supported package manager found${_PMN_RESET} (apt, dnf and pacman are)."
_pmn_say " Install these yourself: a C++ compiler, git, CMake ${PMN_CMAKE_MIN_MAJOR}.${PMN_CMAKE_MIN_MINOR}+ (and clang-format/clang-tidy, optionally)."
fi
for id in $(_pmn_tools_ids); do
row="$(_pmn_row_cat "$id")"
[ -n "$row" ] && rows+=("$row")
done
# ---- report ----------------------------------------------------------
for row in ${rows[@]+"${rows[@]}"}; do
id="$(_pmn_field "$row" 1)"; kind="$(_pmn_field "$row" 2)"; label="$(_pmn_field "$row" 3)"
status="$(_pmn_tool_status "$id")"
detail="${status#* }"; [ "$detail" = "$status" ] && detail=""
case "${status%% *}" in
ok)
if [ -n "$detail" ]; then
printf ' %s[ ok ]%s %-30s %s%s%s\n' "$_PMN_GREEN" "$_PMN_RESET" "$label" "$_PMN_DIM" "$detail" "$_PMN_RESET"
else
printf ' %s[ ok ]%s %s\n' "$_PMN_GREEN" "$_PMN_RESET" "$label"
fi ;;
old)
printf ' %s[old ]%s %-30s %s (needs %s.%s+)\n' "$_PMN_YELLOW" "$_PMN_RESET" "$label" "$detail" "$PMN_CMAKE_MIN_MAJOR" "$PMN_CMAKE_MIN_MINOR" ;;
*)
if [ "$kind" = required ]; then
printf ' %s[ !! ]%s %-30s missing (required)\n' "$_PMN_RED" "$_PMN_RESET" "$label"
else
printf ' %s[ -- ]%s %-30s missing (optional)\n' "$_PMN_DIM" "$_PMN_RESET" "$label"
fi ;;
esac
done
if [ "$PMN_TOOLS_CHECK" = 1 ]; then
for row in ${rows[@]+"${rows[@]}"}; do
id="$(_pmn_field "$row" 1)"
if [ "$(_pmn_field "$row" 2)" = required ] && [ "$(_pmn_tool_status "$id" | cut -d' ' -f1)" != ok ]; then
# brew is only "required" for what it installs; not a failure on its own if nothing needs it
[ "$id" = brew ] && continue
missing_required=1
fi
done
return "$missing_required"
fi
# ---- offer installs, one at a time -----------------------------------
local offered=0 skipped_optional=""
for row in ${rows[@]+"${rows[@]}"}; do
id="$(_pmn_field "$row" 1)"; kind="$(_pmn_field "$row" 2)"; label="$(_pmn_field "$row" 3)"
why="$(_pmn_field "$row" 4)"
# Re-check now: an earlier install may already have provided this
# one (dnf's clang-tools-extra is both clang-format and clang-tidy).
status="$(_pmn_tool_status "$id")"
if [ "${status%% *}" = ok ]; then continue; fi
if [ "$_PMN_ANS_QUIT" = 1 ]; then break; fi
# Everything through Homebrew needs Homebrew first.
if [ "$PMN_OS" = macos ] && [ "$id" != clt ] && [ "$id" != brew ] && [ "$PMN_PM" != brew ]; then
_pmn_say
_pmn_say " ${_PMN_BOLD}${label}${_PMN_RESET} -- ${why}"
_pmn_note " skipped: needs Homebrew, which isn't installed"
failed="$failed $id"
continue
fi
if [ "$PMN_OS" = linux ] && [ "$id" != brew ]; then
pkgs="$(_pmn_row_packages "$row")"
if [ "$PMN_PM" = none ] || [ -z "$pkgs" ]; then
continue # nothing this package manager can do; already said so above
fi
fi
if [ "$kind" = optional ] && ! _pmn_optional_wanted; then
skipped_optional="${skipped_optional:+$skipped_optional, }$label"
continue
fi
offered=$((offered + 1))
_pmn_say
_pmn_say " ${_PMN_BOLD}${label}${_PMN_RESET} -- ${why}"
if [ "${status%% *}" = old ]; then
_pmn_note " (installed: ${status#* }, but too old)"
fi
if _pmn_confirm "$kind" "Install ${label}?"; then
if _pmn_install_row "$row"; then
status="$(_pmn_tool_status "$id")"
case "${status%% *}" in
ok) _pmn_say " ${_PMN_GREEN}installed${_PMN_RESET}" ;;
old) _pmn_cmake_too_old_help "${status#* }"; failed="$failed $id" ;;
*) _pmn_say " ${_PMN_YELLOW}the install finished, but ${label} still isn't on PATH${_PMN_RESET} -- open a new terminal, or check the output above."
failed="$failed $id" ;;
esac
else
_pmn_say " ${_PMN_RED}install failed${_PMN_RESET} -- see the output above."
failed="$failed $id"
fi
else
# Declined (or quit): nothing to echo back to someone who just
# typed it, but an optional one belongs in the closing summary.
if [ "$kind" = optional ]; then
skipped_optional="${skipped_optional:+$skipped_optional, }$label"
fi
fi
done
if [ "$offered" -eq 0 ] && [ -z "$skipped_optional" ] && [ "$_PMN_ANS_QUIT" != 1 ]; then
_pmn_say " Nothing to install."
fi
if [ -n "$skipped_optional" ]; then
_pmn_say
_pmn_note " Optional, not installed: ${skipped_optional}"
_pmn_note " (scripts/setup.sh, run interactively, offers each one; --all installs them all.)"
fi
# ---- final verdict ---------------------------------------------------
for row in ${rows[@]+"${rows[@]}"}; do
id="$(_pmn_field "$row" 1)"
if [ "$(_pmn_field "$row" 2)" = required ] && [ "$id" != brew ] \
&& [ "$(_pmn_tool_status "$id" | cut -d' ' -f1)" != ok ]; then
missing_required=1
_pmn_say " ${_PMN_RED}still missing (required):${_PMN_RESET} $(_pmn_field "$row" 3)"
fi
done
return "$missing_required"
}
# _pmn_tools_offer <id>: offer to install ONE catalog tool -- for callers that
# find out mid-way that they need it (setup.sh, when there is no pinned Ninja
# for this machine). Same prompt, same command display, same rules.
_pmn_tools_offer() {
local id="$1" row status
_pmn_tools_detect
row="$(_pmn_row_cat "$id")"
[ -n "$row" ] || return 1
status="$(_pmn_tool_status "$id")"
[ "${status%% *}" = ok ] && return 0
if [ -z "$(_pmn_row_packages "$row")" ] || [ "$PMN_PM" = none ]; then
_pmn_say " Install $(_pmn_field "$row" 3) yourself: $(_pmn_field "$row" 4)."
return 1
fi
_pmn_say
_pmn_say " ${_PMN_BOLD}$(_pmn_field "$row" 3)${_PMN_RESET} -- $(_pmn_field "$row" 4)"
if _pmn_confirm "$(_pmn_field "$row" 2)" "Install $(_pmn_field "$row" 3)?"; then
if ! _pmn_install_row "$row"; then
_pmn_say " ${_PMN_RED}install failed${_PMN_RESET} -- see the output above."
return 1
fi
if [ "$(_pmn_tool_status "$id" | cut -d' ' -f1)" = ok ]; then
_pmn_say " ${_PMN_GREEN}installed${_PMN_RESET}"
return 0
fi
_pmn_say " ${_PMN_YELLOW}the install finished, but $(_pmn_field "$row" 3) still isn't on PATH${_PMN_RESET} -- open a new terminal, or check the output above."
return 1
fi
return 1
}
+358
View File
@@ -0,0 +1,358 @@
@echo off
REM Configure (if needed) and build the project, preferably with Ninja.
REM
REM Usage (from the project root):
REM scripts\build.bat [--preset ^<name^>] [BUILD_TYPE] [CMAKE_ARGS...]
REM scripts\build.bat --list-presets
REM scripts\build.bat --help
REM
REM --preset <name> Use a CMakePresets.json preset. A preset selects that
REM target's toolchain file (cmake\toolchains\<name>.cmake),
REM which picks the compiler and declares the DIST_TARGET
REM together, instead of you having to pass a matching
REM pair by hand:
REM scripts\build.bat --preset windows-x64-mingw64
REM Each preset builds in its own directory,
REM out\build\<name>, so switching between presets never
REM invalidates -- or deletes -- another one's build.
REM Run --list-presets to see every preset for your OS.
REM Must come first if given at all; --preset=<name>
REM also works. Given with no value (or a value that
REM looks like another flag), this fails fast with that
REM same preset list shown right there. A value that IS
REM given but isn't a real preset name is instead
REM caught by cmake itself failing to configure, at
REM which point this script shows the preset list too
REM -- see the "configure" step below.
REM BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel
REM (default: Configuration.cmake's DEFAULT_BUILD_TYPE)
REM CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.:
REM scripts\build.bat --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
REM scripts\build.bat -DBUILD_TESTS=ON
REM (BUILD_TYPE omitted -- recognized by not
REM starting with "-")
REM
REM Picking a DIST_TARGET: three ways; the first two set the compiler and the
REM label together, and this script rejects mixing --preset with a
REM -DDIST_TARGET rather than leaving it ambiguous which one wins.
REM 1. --preset <name> The recommended way; see --list-presets.
REM 2. -DCMAKE_TOOLCHAIN_FILE=cmake\toolchains\<name>.cmake
REM (no --preset) What a preset does, by hand, as one of
REM CMAKE_ARGS above.
REM 3. -DDIST_TARGET=<value> Sets the label alone, as one of CMAKE_ARGS
REM (no --preset) above. Without also passing a matching
REM compiler (or, better, the toolchain file
REM above), DIST_TARGET is auto-detected from
REM whatever compiler CMake finds on its own; if
REM the two disagree, Configuration.cmake
REM FATAL_ERRORs rather than silently building
REM the wrong toolchain and mislabeling the
REM result -- see that file and the README.
REM Passing --preset together with an explicit -DDIST_TARGET=... in
REM CMAKE_ARGS is rejected by this script before cmake even runs: the
REM preset's toolchain already declares DIST_TARGET, so a second value
REM alongside it is always either redundant or contradictory -- never
REM useful.
REM
REM cmd.exe splits %1, %2, ... (and SHIFT) on space, comma, semicolon, AND
REM "=" -- type "-DFOO=BAR" as an argument and it arrives as TWO separate
REM parameters, %1=-DFOO %2=BAR, silently, with no way to tell after the
REM fact. This script never reads a "="-laden argument (or --preset's own
REM value) through %1 or SHIFT; everything is peeled off of the raw,
REM unsplit %* instead using FOR /F (which only ever splits on space/tab),
REM so "=" survives intact -- see the parsing block below. (bash's
REM "$@"/shift has no such bug -- see build.sh for that half of this same
REM feature.)
REM
REM Build directory: out\build\<preset> for --preset <preset>; without a
REM preset, out\build\default (the compiler is auto-detected there, so keep
REM to one compiler per directory or use presets). Set BUILD_DIR to use an
REM exact directory of your own instead. scripts\run.bat, install.bat and
REM docs.bat use the directory built last, or --preset <name> to pick another.
setlocal enabledelayedexpansion
REM -h/--help is checked here, off the raw %1, before anything else --
REM including whether cmake is even on PATH below -- since help shouldn't
REM depend on that.
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
goto after_help
:show_help
echo Usage: scripts\build.bat [--preset ^<name^>] [BUILD_TYPE] [CMAKE_ARGS...]
echo scripts\build.bat --list-presets
echo scripts\build.bat --help
echo.
echo --preset ^<name^> Use a CMakePresets.json preset: it selects the target's
echo toolchain file ^(compiler and DIST_TARGET together^) and
echo builds in its own directory, out\build\^<name^>. Must come
echo first if given at all; --preset=^<name^> also works. Run
echo --list-presets to see every preset for your OS.
echo BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel
echo ^(default: Configuration.cmake's DEFAULT_BUILD_TYPE^)
echo CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.
echo -DDIST_TARGET=^<value^> or -DBUILD_TESTS=ON.
echo.
echo Don't combine --preset with an explicit -DDIST_TARGET=... in CMAKE_ARGS --
echo the preset's toolchain already declares one; see the comment at the top of
echo this script ^(scripts\build.bat^) and Configuration.cmake's "Target selection"
echo section for why picking a DIST_TARGET is one or the other, not both.
echo.
echo Examples:
echo scripts\build.bat --preset linux-x64-clang
echo scripts\build.bat --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
echo scripts\build.bat -DBUILD_TESTS=ON
exit /b 0
:after_help
cd /d "%~dp0.."
REM An explicit BUILD_DIR from the environment beats the one-directory-per-
REM preset layout resolved below, and is then also not recorded as "the
REM build made last" (run/install/docs would otherwise be sent to a
REM directory the layout doesn't know about).
set "_pmn_record_build=1"
if not "%BUILD_DIR%"=="" set "_pmn_record_build=0"
where cmake >nul 2>nul
if errorlevel 1 (
echo error: cmake not found on PATH.
exit /b 1
)
REM See the header comment above for why this doesn't use %1/SHIFT.
set "ALL_ARGS=%*"
set "PRESET="
set "BUILD_TYPE="
set "EXTRA_ARGS="
if not defined ALL_ARGS goto args_done
for /f "tokens=1,* delims= " %%A in ("%ALL_ARGS%") do (
set "_FIRST=%%A"
set "_REST=%%B"
)
if /i "%_FIRST%"=="--list-presets" (
cmake --list-presets
exit /b !ERRORLEVEL!
)
if /i "%_FIRST%"=="--preset" goto preset_space_form
if /i "%_FIRST:~0,9%"=="--preset=" goto preset_equals_form
goto no_preset
:preset_space_form
if not defined _REST (
echo error: --preset requires a value, e.g. --preset windows-x64-mingw64
echo.
cmake --list-presets
exit /b 1
)
for /f "tokens=1,* delims= " %%A in ("%_REST%") do (
set "PRESET=%%A"
set "_REST=%%B"
)
if "%PRESET:~0,1%"=="-" (
echo error: --preset requires a value, e.g. --preset windows-x64-mingw64
echo.
cmake --list-presets
exit /b 1
)
goto have_preset
:preset_equals_form
set "PRESET=%_FIRST:~9%"
if not defined PRESET (
echo error: --preset= requires a value, e.g. --preset=windows-x64-mingw64
echo.
cmake --list-presets
exit /b 1
)
goto have_preset
:have_preset
set "ALL_ARGS=%_REST%"
if not defined ALL_ARGS goto args_done
for /f "tokens=1,* delims= " %%A in ("%ALL_ARGS%") do (
set "_FIRST=%%A"
set "_REST=%%B"
)
:no_preset
if "%_FIRST:~0,1%"=="-" (
set "EXTRA_ARGS=%ALL_ARGS%"
) else (
set "BUILD_TYPE=%_FIRST%"
set "EXTRA_ARGS=%_REST%"
)
:args_done
if "%BUILD_TYPE%"=="" call :read_config DEFAULT_BUILD_TYPE BUILD_TYPE
if "%TOOLCACHE_DIR%"=="" call :read_config TOOLCACHE_DIR TOOLCACHE_DIR
REM One build directory per preset (out\build\<preset>; "default" when no
REM preset was given), so switching presets never has to reuse -- or wipe -- a
REM directory configured for another compiler. cmake\BuildDir.cmake owns the
REM layout (Configuration.cmake's BUILD_OUTPUT_DIR); goto rather than an
REM if-block for the reason given further down about nesting.
set "_pmn_name=default"
if defined PRESET set "_pmn_name=%PRESET%"
if not "%BUILD_DIR%"=="" goto build_dir_known
call :resolve_build_dir "%_pmn_name%" BUILD_DIR
if errorlevel 1 exit /b 1
:build_dir_known
REM Combining --preset with an explicit -DDIST_TARGET=... in CMAKE_ARGS is
REM ambiguous (the preset already sets one) -- caught here, before cmake
REM ever runs, rather than leaving it to whichever of the two CMake
REM happens to prefer. findstr's /C: does a literal (non-regex) substring
REM search, which sidesteps cmd.exe's own "=" splitting quirk (see the
REM header comment) entirely -- there's no argument-parsing involved here,
REM just a plain string search over EXTRA_ARGS as already assembled.
if defined PRESET (
echo %EXTRA_ARGS%| findstr /C:"-DDIST_TARGET=" >nul
if not errorlevel 1 (
echo error: both --preset %PRESET% and an explicit -DDIST_TARGET=... were given.
echo A preset's toolchain already declares DIST_TARGET ^(and picks the compiler^) -- pick one:
echo scripts\build.bat --preset %PRESET%
echo scripts\build.bat -DCMAKE_TOOLCHAIN_FILE=cmake\toolchains\^<target^>.cmake
echo See the comment at the top of this script, or Configuration.cmake's
echo "Target selection" section, for why these two aren't combined.
exit /b 1
)
)
REM Prefer a pinned ninja (fetched by scripts\setup.bat -- see
REM cmake\FetchNinja.cmake) over a system one, same "always the pinned
REM copy" preference as Doxygen. Needed whether or not --preset is used:
REM every preset sets its generator to Ninja but doesn't hardcode which
REM ninja binary to use (that would just be TOOLCACHE_DIR's default
REM duplicated a third time -- see Configuration.cmake) --
REM CMAKE_MAKE_PROGRAM supplies it either way; only -G Ninja itself is
REM skipped below when a preset already sets the generator.
REM
REM Deliberately goto-based, and deliberately `pushd` + a bare wildcard
REM rather than `for /d %%D in ("path\*")` -- both tested directly; a
REM path-prefixed wildcard didn't match here, this form does, and it also
REM sidesteps nesting a FOR loop inside an if/else block, which
REM separately corrupts cmd.exe's paren-matching for the enclosing block
REM (see scripts\refactor.bat for that one).
set "NINJA_BIN="
if not exist "%TOOLCACHE_DIR%" goto check_system_ninja
pushd "%TOOLCACHE_DIR%"
for /d %%D in (ninja-*) do if exist "%%D\ninja.exe" if not defined NINJA_BIN set "NINJA_BIN=%%~fD\ninja.exe"
popd
if defined NINJA_BIN goto have_pinned_ninja
:check_system_ninja
where ninja >nul 2>nul
if errorlevel 1 goto no_ninja
echo ==^> Using system ninja
if not defined PRESET set "GENERATOR_ARGS=-G Ninja"
goto configure
:have_pinned_ninja
echo ==^> Using pinned ninja: %NINJA_BIN%
if defined PRESET (
set "GENERATOR_ARGS=-DCMAKE_MAKE_PROGRAM=%NINJA_BIN%"
) else (
set "GENERATOR_ARGS=-G Ninja -DCMAKE_MAKE_PROGRAM=%NINJA_BIN%"
)
goto configure
:no_ninja
if defined PRESET (
echo warning: ninja not found ^(no pinned copy, none on PATH^) -- the '%PRESET%'
echo preset requires it; configure will likely fail. Run
echo scripts\setup.bat to fetch a pinned ninja, or install ninja
echo yourself.
) else (
echo warning: ninja not found ^(no pinned copy, none on PATH^) -- falling back to
echo CMake's default generator for this platform. Run scripts\setup.bat
echo to fetch a pinned ninja, or install ninja yourself, for a faster
echo and more consistent build. Continuing without it.
)
set "GENERATOR_ARGS="
:configure
echo ==^> Configuring (%BUILD_TYPE%) into %BUILD_DIR%\
if defined PRESET echo Preset: %PRESET%
if defined EXTRA_ARGS echo Extra CMake arguments: %EXTRA_ARGS%
if defined PRESET (
cmake --preset %PRESET% -S . -B "%BUILD_DIR%" %GENERATOR_ARGS% -DCMAKE_BUILD_TYPE=%BUILD_TYPE% %EXTRA_ARGS%
) else (
cmake %GENERATOR_ARGS% -S . -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=%BUILD_TYPE% %EXTRA_ARGS%
)
if errorlevel 1 (
if defined PRESET (
echo.
echo error: configuring with preset '%PRESET%' failed -- see the cmake output above.
echo If '%PRESET%' isn't the preset name you meant, here is what --list-presets
echo shows for this OS:
echo.
cmake --list-presets
)
exit /b 1
)
REM Configure succeeded: this is now the build the other scripts default to.
if "%_pmn_record_build%"=="1" cmake "-DPMN_NAME=%_pmn_name%" -DPMN_RECORD=ON -P cmake\BuildDir.cmake >nul
echo ==^> Building
cmake --build "%BUILD_DIR%"
if errorlevel 1 exit /b 1
echo ==^> Done. Run a module with: scripts\run.bat ^<target-name^>
exit /b 0
REM ---------------------------------------------------------------------
REM :read_config <ConfigurationVarName> <LocalVarName>
REM
REM Reads one value out of Configuration.cmake -- this project's single
REM source of truth -- via cmake\PrintConfig.cmake, instead of hardcoding
REM a second copy of a default that can silently drift out of sync, and
REM stores it into %2 in the CALLER's scope. See cmake\PrintConfig.cmake
REM for exactly what's being stripped off of its output below.
REM ---------------------------------------------------------------------
:read_config
setlocal
set "_pmn_line="
for /f "usebackq delims=" %%V in (`cmake "-DPRINT_VARS=%~1" -P cmake\PrintConfig.cmake`) do set "_pmn_line=%%V"
for /f "tokens=1,* delims==" %%A in ("%_pmn_line%") do set "_pmn_value=%%B"
endlocal & set "%~2=%_pmn_value%"
goto :eof
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks cmake\BuildDir.cmake which build directory to use -- %1 is a preset
REM name, or "default"; empty means "discover one" (the build made last, or
REM the only one) -- and stores it into %2 in the CALLER's scope, with
REM backslashes. Same shape as :read_config above, and the same
REM `cmake -P` one-line-answer protocol: "-- BUILD_DIR=<path>" or
REM "-- ERROR=<text>" (see that file). On failure it prints the reason to
REM stderr, unless a third argument "quiet" is given, and returns errorlevel 1.
REM ---------------------------------------------------------------------
:resolve_build_dir
setlocal
set "_pmn_key="
set "_pmn_val="
for /f "usebackq tokens=1,* delims==" %%A in (`cmake "-DPMN_NAME=%~1" -P cmake\BuildDir.cmake`) do (
set "_pmn_key=%%A"
set "_pmn_val=%%B"
)
if "%_pmn_key%"=="-- BUILD_DIR" if defined _pmn_val goto resolve_ok
if /i "%~3"=="quiet" goto resolve_failed
if defined _pmn_val (
echo error: %_pmn_val% 1>&2
) else (
echo error: cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+280
View File
@@ -0,0 +1,280 @@
#!/usr/bin/env bash
# Configure (if needed) and build the project, preferably with Ninja.
#
# Usage (from the project root):
# scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
# scripts/build.sh --list-presets
# scripts/build.sh --help
#
# --preset <name> Use a CMakePresets.json preset. A preset selects that
# target's toolchain file (cmake/toolchains/<name>.cmake),
# which picks the compiler and declares the DIST_TARGET
# together, instead of you having to pass a matching
# pair by hand:
# scripts/build.sh --preset windows-x64-mingw64
# Each preset builds in its own directory,
# out/build/<name>, so switching between presets never
# invalidates -- or deletes -- another one's build.
# Run --list-presets to see every preset for your OS.
# Must come first if given at all; --preset=<name> also
# works. Given with no value, or a value that isn't one
# of --list-presets' own, this fails fast with that
# same preset list shown right there, rather than
# letting a typo fall through to a less obvious `cmake`
# error further down.
# BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
# (default: Configuration.cmake's DEFAULT_BUILD_TYPE)
# CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.:
# scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
# scripts/build.sh -DBUILD_TESTS=ON
# (BUILD_TYPE omitted -- recognized by not
# starting with "-")
#
# Picking a DIST_TARGET: three ways; the first two set the compiler and the
# label together, and this script rejects mixing --preset with a
# -DDIST_TARGET rather than leaving it ambiguous which one wins.
# 1. --preset <name> The recommended way; see --list-presets.
# 2. -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<name>.cmake
# (no --preset) What a preset does, by hand, as one of
# CMAKE_ARGS above.
# 3. -DDIST_TARGET=<value> Sets the label alone, as one of CMAKE_ARGS
# (no --preset) above. Without also passing a matching
# compiler (or, better, the toolchain file
# above), DIST_TARGET is auto-detected from
# whatever compiler CMake finds on its own; if
# the two disagree, Configuration.cmake
# FATAL_ERRORs rather than silently building
# the wrong toolchain and mislabeling the
# result -- see that file and the README.
# Passing --preset together with an explicit -DDIST_TARGET=... in
# CMAKE_ARGS is rejected by this script before cmake even runs: the
# preset's toolchain already declares DIST_TARGET, so a second value
# alongside it is always either redundant (same value) or contradictory
# (different value) -- never useful.
#
# Build directory: out/build/<preset> for --preset <preset>; without a
# preset, out/build/default (the compiler is auto-detected there, so keep
# to one compiler per directory or use presets). Set BUILD_DIR to use an
# exact directory of your own instead. scripts/run.sh, install.sh and
# docs.sh use the directory built last, or --preset <name> to pick another.
set -euo pipefail
shopt -s nullglob
usage() {
cat <<'EOF'
Usage: scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
scripts/build.sh --list-presets
scripts/build.sh --help
--preset <name> Use a CMakePresets.json preset: it selects the target's
toolchain file (compiler and DIST_TARGET together) and
builds in its own directory, out/build/<name>. Must come
first if given at all; --preset=<name> also works. Run
--list-presets to see every preset for your OS.
BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
(default: Configuration.cmake's DEFAULT_BUILD_TYPE)
CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.
-DDIST_TARGET=<value> or -DBUILD_TESTS=ON.
Don't combine --preset with an explicit -DDIST_TARGET=... in CMAKE_ARGS --
the preset's toolchain already declares one; see the comment at the top of
this script (scripts/build.sh) and Configuration.cmake's "Target selection"
section for why picking a DIST_TARGET is one or the other, not both.
Examples:
scripts/build.sh --preset linux-x64-clang
scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
scripts/build.sh -DBUILD_TESTS=ON
EOF
}
case "${1:-}" in
-h|--help)
usage
exit 0
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
command -v cmake >/dev/null 2>&1 || { echo "error: cmake not found on PATH." >&2; exit 1; }
if [ "${1:-}" = "--list-presets" ]; then
exec cmake --list-presets
fi
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir.
source "$SCRIPT_DIR/_common.sh"
# Prints CMake's own preset listing (already filtered to this OS by each
# preset's "condition" in CMakePresets.json) to stderr -- used to show the
# actual options right alongside a preset-related error below, instead of
# just telling the user to go run --list-presets separately.
_pmn_show_presets() {
echo "" >&2
cmake --list-presets >&2
}
# Membership test against that same listing -- $1 matches a real preset
# name for this OS only if it appears there as a quoted entry (anchored on
# both quote characters so e.g. "windows-x64-mingw32" can't false-match a
# check for "windows-x64-mingw3").
_pmn_known_preset() {
cmake --list-presets 2>/dev/null | grep -qF "\"$1\""
}
_pmn_has_explicit_dist_target() {
local arg
for arg in "$@"; do
case "$arg" in
-DDIST_TARGET=*) return 0 ;;
esac
done
return 1
}
# --preset must come first if given at all -- see Configuration.cmake's
# and CMakePresets.json's own comments for why this is the recommended
# way to pick a non-default DIST_TARGET: its toolchain file sets the
# compiler and the label together, instead of you having to pass both by
# hand.
PRESET=""
case "${1:-}" in
--preset)
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
echo "error: --preset requires a value, e.g. --preset windows-x64-mingw64" >&2
_pmn_show_presets
exit 1
fi
PRESET="$2"
shift 2
;;
--preset=*)
PRESET="${1#--preset=}"
shift
if [ -z "$PRESET" ]; then
echo "error: --preset= requires a value, e.g. --preset=windows-x64-mingw64" >&2
_pmn_show_presets
exit 1
fi
;;
esac
if [ -n "$PRESET" ] && ! _pmn_known_preset "$PRESET"; then
echo "error: '${PRESET}' is not a preset --list-presets knows about for this OS." >&2
_pmn_show_presets
exit 1
fi
# One build directory per preset (out/build/<preset>; "default" when no
# preset was given), so switching presets never has to reuse -- or wipe -- a
# directory configured for another compiler. cmake/BuildDir.cmake owns the
# layout (Configuration.cmake's BUILD_OUTPUT_DIR); an explicit BUILD_DIR
# from the environment beats it, and is then also not recorded as "the one
# built last" (run/install/docs would otherwise be sent to a directory the
# layout doesn't know about).
_pmn_record_build=1
if [ -n "${BUILD_DIR:-}" ]; then
_pmn_record_build=0
else
BUILD_DIR="$(_pmn_resolve_build_dir "${PRESET:-default}")"
fi
# If the first remaining argument doesn't start with "-", it's BUILD_TYPE
# and everything after it is CMAKE_ARGS; otherwise there's no BUILD_TYPE
# override and every argument is CMAKE_ARGS. Plain bash "$@"/shift, no
# special-casing needed: unlike Windows cmd.exe (see build.bat), bash
# never splits a "-DFOO=BAR"-style argument on its own "=".
if [ "$#" -gt 0 ] && [[ "$1" != -* ]]; then
BUILD_TYPE="$1"
shift
else
BUILD_TYPE="$(_pmn_read_config DEFAULT_BUILD_TYPE)"
fi
CMAKE_ARGS=("$@")
# ${arr[@]+"${arr[@]}"} rather than a plain "${arr[@]}" wherever an array can be
# empty: under `set -u`, bash before 4.4 -- which includes the 3.2 that macOS
# still ships as /bin/bash -- treats expanding an EMPTY array as an unset
# variable and aborts with "unbound variable".
if [ -n "$PRESET" ] && _pmn_has_explicit_dist_target ${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}; then
echo "error: both --preset ${PRESET} and an explicit -DDIST_TARGET=... were given." >&2
echo " A preset's toolchain already declares DIST_TARGET (and picks the compiler) -- pick one:" >&2
echo " scripts/build.sh --preset ${PRESET}" >&2
echo " scripts/build.sh -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/<target>.cmake" >&2
echo " See the comment at the top of this script, or Configuration.cmake's" >&2
echo " \"Target selection\" section, for why these two aren't combined." >&2
exit 1
fi
# Prefer a pinned ninja (fetched by scripts/setup.sh -- see
# cmake/FetchNinja.cmake) over a system one, same "always the pinned copy"
# preference as Doxygen. Needed whether or not --preset is used: every
# preset sets its generator to Ninja but doesn't hardcode which ninja
# binary to use (that would just be TOOLCACHE_DIR's default duplicated a
# third time -- see Configuration.cmake) -- CMAKE_MAKE_PROGRAM supplies it
# either way; only -G Ninja itself is skipped below when a preset already
# sets the generator.
TOOLCACHE_DIR="${TOOLCACHE_DIR:-$(_pmn_read_config TOOLCACHE_DIR)}"
NINJA_BIN=""
for _candidate in "${TOOLCACHE_DIR}"/ninja-*/ninja; do
if [ -x "$_candidate" ]; then
# Must be absolute: CMake invokes CMAKE_MAKE_PROGRAM from several
# different working directories during its own internal
# try-compile steps, where a relative path doesn't resolve.
NINJA_BIN="$(cd "$(dirname "$_candidate")" && pwd)/$(basename "$_candidate")"
break
fi
done
GENERATOR_ARGS=()
if [ -n "$NINJA_BIN" ]; then
echo "==> Using pinned ninja: ${NINJA_BIN}"
if [ -z "$PRESET" ]; then
GENERATOR_ARGS=(-G Ninja "-DCMAKE_MAKE_PROGRAM=${NINJA_BIN}")
else
GENERATOR_ARGS=("-DCMAKE_MAKE_PROGRAM=${NINJA_BIN}")
fi
elif command -v ninja >/dev/null 2>&1; then
echo "==> Using system ninja: $(command -v ninja)"
if [ -z "$PRESET" ]; then
GENERATOR_ARGS=(-G Ninja)
fi
else
if [ -n "$PRESET" ]; then
echo "warning: ninja not found (no pinned copy, none on PATH) -- the '${PRESET}'" >&2
echo " preset requires it; configure will likely fail. Run" >&2
echo " scripts/setup.sh to fetch a pinned ninja, or install ninja" >&2
echo " yourself." >&2
else
echo "warning: ninja not found (no pinned copy, none on PATH) -- falling back to" >&2
echo " CMake's default generator for this platform. Run scripts/setup.sh" >&2
echo " to fetch a pinned ninja, or install ninja yourself, for a faster" >&2
echo " and more consistent build. Continuing without it." >&2
fi
fi
CONFIGURE_ARGS=(-S . -B "${BUILD_DIR}")
if [ -n "$PRESET" ]; then
CONFIGURE_ARGS=(--preset "${PRESET}" "${CONFIGURE_ARGS[@]}")
fi
echo "==> Configuring (${BUILD_TYPE}) into ${BUILD_DIR}/"
if [ -n "$PRESET" ]; then
echo " Preset: ${PRESET}"
fi
if [ "${#CMAKE_ARGS[@]}" -gt 0 ]; then
echo " Extra CMake arguments: ${CMAKE_ARGS[*]}"
fi
cmake "${CONFIGURE_ARGS[@]}" ${GENERATOR_ARGS[@]+"${GENERATOR_ARGS[@]}"} -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" ${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}
# Configure succeeded: this is now the build the other scripts default to.
if [ "$_pmn_record_build" = 1 ]; then
_pmn_record_build_dir "${PRESET:-default}"
fi
echo "==> Building"
cmake --build "${BUILD_DIR}"
echo "==> Done. Run a module with: scripts/run.sh <target-name>"
+136
View File
@@ -0,0 +1,136 @@
@echo off
REM Build Doxygen documentation for every module (see cmake\Documentation.cmake)
REM and print the path to the landing page. A no-op with a clear message if
REM Doxygen isn't installed or ENABLE_DOXYGEN=OFF.
REM
REM Output goes to out\docs\ (DOCS_OUTPUT_DIR in Configuration.cmake) --
REM note this is a sibling of out\build, not nested inside it, and shared by
REM every preset: documentation doesn't depend on which compiler built the
REM code. The docs targets just need SOME configured build directory to run
REM from, so this uses the one built last unless you pick with --preset. If
REM you've overridden DOCS_OUTPUT_DIR at configure time, also set DOCS_DIR here
REM to match so the printed path is correct (docs still land in the right
REM place either way -- this only affects the message below).
REM
REM Usage (from the project root):
REM scripts\docs.bat REM every module
REM scripts\docs.bat core_lib REM just one module's docs (docs_<target>)
REM scripts\docs.bat --preset windows-x64-clang-cl REM ... from that preset's build directory
REM scripts\docs.bat --help
REM
REM --preset must come first if given at all; --preset=<name> also works. Set
REM BUILD_DIR to use an exact build directory instead.
setlocal
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
goto after_help
:show_help
echo Usage: scripts\docs.bat [--preset ^<name^>] [module-target-name]
echo.
echo Builds Doxygen documentation for every module ^(see cmake\Documentation.cmake^)
echo and prints the path to the landing page. A no-op with a clear message if
echo Doxygen isn't installed or ENABLE_DOXYGEN=OFF.
echo.
echo --preset ^<name^> Run from that preset's build directory
echo ^(out\build\^<name^>^). Default: the build
echo scripts\build.bat made last. Must come first if
echo given at all; --preset=^<name^> also works.
echo ^(no argument^) Build docs for every module.
echo module-target-name Build docs for just that one module ^(its own
echo docs_^<name^> target^), e.g.:
echo scripts\docs.bat core_lib
echo.
echo Output goes to out\docs\ ^(DOCS_OUTPUT_DIR in Configuration.cmake^), shared
echo by every preset -- see the comment at the top of this script if you've
echo overridden that.
exit /b 0
:after_help
cd /d "%~dp0.."
where cmake >nul 2>nul
if errorlevel 1 (
echo error: cmake not found on PATH.
exit /b 1
)
REM --preset must come first if given at all, same as scripts\build.bat.
REM cmd.exe splits "--preset=<name>" into "--preset" and "<name>" on its own
REM (see build.bat's note on "="), so both spellings arrive here identically
REM and this one branch handles both.
set "PRESET="
if /i not "%~1"=="--preset" goto preset_done
if "%~2"=="" (
echo error: --preset requires a value, e.g. --preset windows-x64-clang-cl 1>&2
exit /b 1
)
set "PRESET=%~2"
shift
shift
:preset_done
REM Suffix for the error hints below that say to run build.bat first.
set "_pmn_hint="
if defined PRESET set "_pmn_hint= --preset %PRESET%"
REM BUILD_DIR from the environment beats everything; otherwise --preset, else
REM whichever build was made last (see cmake\BuildDir.cmake). Explains itself
REM and stops if there's nothing to pick.
if not "%BUILD_DIR%"=="" goto build_dir_known
call :resolve_build_dir "%PRESET%" BUILD_DIR
if errorlevel 1 exit /b 1
:build_dir_known
if "%DOCS_DIR%"=="" set "DOCS_DIR=out\docs"
if not exist "%BUILD_DIR%" (
echo error: "%BUILD_DIR%" not found. Run scripts\build.bat%_pmn_hint% first.
exit /b 1
)
echo ==^> Using build directory %BUILD_DIR%
if "%~1"=="" (
cmake --build "%BUILD_DIR%" --target docs
if exist "%DOCS_DIR%\index.html" (
echo ==^> Open: %DOCS_DIR%\index.html
)
) else (
cmake --build "%BUILD_DIR%" --target docs_%1
echo ==^> Open: %DOCS_DIR%\%1\html\index.html
)
exit /b %ERRORLEVEL%
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks cmake\BuildDir.cmake which build directory to use -- %1 is a preset
REM name, or "default"; empty means "discover one" (the build made last, or
REM the only one) -- and stores it into %2 in the CALLER's scope, with
REM backslashes. Same shape as build.bat's, and the same `cmake -P`
REM one-line-answer protocol: "-- BUILD_DIR=<path>" or "-- ERROR=<text>"
REM (see that file). On failure it prints the reason to stderr, unless a
REM third argument "quiet" is given, and returns errorlevel 1.
REM ---------------------------------------------------------------------
:resolve_build_dir
setlocal
set "_pmn_key="
set "_pmn_val="
for /f "usebackq tokens=1,* delims==" %%A in (`cmake "-DPMN_NAME=%~1" -P cmake\BuildDir.cmake`) do (
set "_pmn_key=%%A"
set "_pmn_val=%%B"
)
if "%_pmn_key%"=="-- BUILD_DIR" if defined _pmn_val goto resolve_ok
if /i "%~3"=="quiet" goto resolve_failed
if defined _pmn_val (
echo error: %_pmn_val% 1>&2
) else (
echo error: cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Build Doxygen documentation for every module (see cmake/Documentation.cmake)
# and print the path to the landing page. A no-op with a clear message if
# Doxygen isn't installed or ENABLE_DOXYGEN=OFF.
#
# Output goes to out/docs/ (DOCS_OUTPUT_DIR in Configuration.cmake) --
# note this is a sibling of out/build, not nested inside it, and shared by
# every preset: documentation doesn't depend on which compiler built the
# code. The docs targets just need SOME configured build directory to run
# from, so this uses the one built last unless you pick with --preset. If
# you've overridden DOCS_OUTPUT_DIR at configure time, also set DOCS_DIR here
# to match so the printed path is correct (docs still land in the right
# place either way -- this only affects the message below).
#
# Usage (from the project root):
# scripts/docs.sh # every module
# scripts/docs.sh core_lib # just one module's docs (docs_<target>)
# scripts/docs.sh --preset linux-x64-gcc # ... from that preset's build directory
# scripts/docs.sh --help
#
# --preset must come first if given at all; --preset=<name> also works. Set
# BUILD_DIR to use an exact build directory instead.
set -euo pipefail
case "${1:-}" in
-h|--help)
cat <<'HELP'
Usage: scripts/docs.sh [--preset <name>] [module-target-name]
Builds Doxygen documentation for every module (see cmake/Documentation.cmake)
and prints the path to the landing page. A no-op with a clear message if
Doxygen isn't installed or ENABLE_DOXYGEN=OFF.
--preset <name> Run from that preset's build directory
(out/build/<name>). Default: the build
scripts/build.sh made last. Must come first if
given at all; --preset=<name> also works.
(no argument) Build docs for every module.
module-target-name Build docs for just that one module (its own
docs_<name> target), e.g.:
scripts/docs.sh core_lib
Output goes to out/docs/ (DOCS_OUTPUT_DIR in Configuration.cmake), shared
by every preset -- see the comment at the top of this script if you've
overridden that.
HELP
exit 0
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir.
source "$SCRIPT_DIR/_common.sh"
# --preset must come first if given at all, same as scripts/build.sh.
PRESET=""
case "${1:-}" in
--preset)
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
echo "error: --preset requires a value, e.g. --preset linux-x64-gcc" >&2
exit 1
fi
PRESET="$2"
shift 2
;;
--preset=*)
PRESET="${1#--preset=}"
shift
if [ -z "$PRESET" ]; then
echo "error: --preset= requires a value, e.g. --preset=linux-x64-gcc" >&2
exit 1
fi
;;
esac
if [ -z "${BUILD_DIR:-}" ]; then
# Explains itself and stops if there's nothing to pick.
BUILD_DIR="$(_pmn_resolve_build_dir "$PRESET")"
fi
DOCS_DIR="${DOCS_DIR:-out/docs}"
if [ ! -d "$BUILD_DIR" ]; then
echo "error: '${BUILD_DIR}' not found. Run scripts/build.sh${PRESET:+ --preset $PRESET} first." >&2
exit 1
fi
echo "==> Using build directory ${BUILD_DIR}"
if [ $# -ge 1 ]; then
TARGET="docs_$1"
cmake --build "${BUILD_DIR}" --target "${TARGET}"
echo "==> Open: ${DOCS_DIR}/$1/html/index.html"
else
cmake --build "${BUILD_DIR}" --target docs
if [ -f "${DOCS_DIR}/index.html" ]; then
echo "==> Open: ${DOCS_DIR}/index.html"
fi
fi
+151
View File
@@ -0,0 +1,151 @@
@echo off
REM Install this project's own build artifacts to
REM out\install\<platform>-<arch>-<compiler>\ (INSTALL_OUTPUT_DIR in
REM Configuration.cmake; see CMakeLists.txt's install() DESTINATION paths) --
REM the same name as the out\build\<preset> directory they came from, since
REM a preset is named after its target.
REM
REM Always passes --component to `cmake --install`. Without it, any
REM vendored/fetched dependency's own install() rules (see
REM cmake\Dependencies.cmake) would also run, typically dumping files under
REM CMAKE_INSTALL_PREFIX -- COMPONENT is what keeps `cmake --install`
REM scoped to just this project's own targets.
REM
REM Usage (from the project root):
REM scripts\install.bat [--preset <name>] [BUILD_TYPE]
REM scripts\install.bat --help
REM
REM --preset <name> Install the build made with that preset
REM (out\build\<name>). Without it: the build
REM scripts\build.bat made last, or the only one that
REM exists. Must come first if given at all;
REM --preset=<name> also works.
REM BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
REM (default: Configuration.cmake's DEFAULT_BUILD_TYPE) --
REM only relevant for a Ninja Multi-Config build.
REM
REM Set BUILD_DIR to install from an exact build directory instead.
setlocal enabledelayedexpansion
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
goto after_help
:show_help
echo Usage: scripts\install.bat [--preset ^<name^>] [BUILD_TYPE]
echo.
echo Installs this project's own build artifacts to
echo out\install\^<platform^>-^<arch^>-^<compiler^>\ ^(INSTALL_OUTPUT_DIR in
echo Configuration.cmake^), scoped to just this project via `cmake --install
echo --component` -- see the comment at the top of this script.
echo.
echo --preset ^<name^> Install the build made with that preset
echo ^(out\build\^<name^>^). Default: the build
echo scripts\build.bat made last. Must come first if given
echo at all; --preset=^<name^> also works.
echo BUILD_TYPE Debug^|Release^|RelWithDebInfo^|MinSizeRel
echo ^(default: Configuration.cmake's DEFAULT_BUILD_TYPE^) --
echo only relevant for a Ninja Multi-Config build.
exit /b 0
:after_help
cd /d "%~dp0.."
where cmake >nul 2>nul
if errorlevel 1 (
echo error: cmake not found on PATH.
exit /b 1
)
REM --preset must come first if given at all, same as scripts\build.bat.
REM cmd.exe splits "--preset=<name>" into "--preset" and "<name>" on its own
REM (see build.bat's note on "="), so both spellings arrive here identically
REM and this one branch handles both.
set "PRESET="
if /i not "%~1"=="--preset" goto preset_done
if "%~2"=="" (
echo error: --preset requires a value, e.g. --preset windows-x64-clang-cl 1>&2
exit /b 1
)
set "PRESET=%~2"
shift
shift
:preset_done
REM Suffix for the error hints below that say to run build.bat first.
set "_pmn_hint="
if defined PRESET set "_pmn_hint= --preset %PRESET%"
REM BUILD_DIR from the environment beats everything; otherwise --preset, else
REM whichever build was made last (see cmake\BuildDir.cmake). Explains itself
REM and stops if there's nothing to pick.
if not "%BUILD_DIR%"=="" goto build_dir_known
call :resolve_build_dir "%PRESET%" BUILD_DIR
if errorlevel 1 exit /b 1
:build_dir_known
REM A plain %~1 is safe here (unlike build.bat's arguments): a build type
REM like "Debug" never contains "=", so cmd.exe's positional-parameter
REM splitting on it never comes into play.
set "BUILD_TYPE=%~1"
if "%BUILD_TYPE%"=="" call :read_config DEFAULT_BUILD_TYPE BUILD_TYPE
call :read_config PROJECT_CODE_NAME COMPONENT
if not exist "%BUILD_DIR%" (
echo error: "%BUILD_DIR%" not found. Run scripts\build.bat%_pmn_hint% first.
exit /b 1
)
echo ==^> Installing (component: %COMPONENT%) from %BUILD_DIR%
cmake --install "%BUILD_DIR%" --config %BUILD_TYPE% --component %COMPONENT%
exit /b %ERRORLEVEL%
REM ---------------------------------------------------------------------
REM :read_config <ConfigurationVarName> <LocalVarName>
REM
REM Reads one value out of Configuration.cmake -- this project's single
REM source of truth -- via cmake\PrintConfig.cmake, instead of hardcoding
REM a second copy of a default that can silently drift out of sync, and
REM stores it into %2 in the CALLER's scope. See cmake\PrintConfig.cmake
REM for exactly what's being stripped off of its output below.
REM ---------------------------------------------------------------------
:read_config
setlocal
set "_pmn_line="
for /f "usebackq delims=" %%V in (`cmake "-DPRINT_VARS=%~1" -P cmake\PrintConfig.cmake`) do set "_pmn_line=%%V"
for /f "tokens=1,* delims==" %%A in ("%_pmn_line%") do set "_pmn_value=%%B"
endlocal & set "%~2=%_pmn_value%"
goto :eof
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks cmake\BuildDir.cmake which build directory to use -- %1 is a preset
REM name, or "default"; empty means "discover one" (the build made last, or
REM the only one) -- and stores it into %2 in the CALLER's scope, with
REM backslashes. Same shape as build.bat's, and the same `cmake -P`
REM one-line-answer protocol: "-- BUILD_DIR=<path>" or "-- ERROR=<text>"
REM (see that file). On failure it prints the reason to stderr, unless a
REM third argument "quiet" is given, and returns errorlevel 1.
REM ---------------------------------------------------------------------
:resolve_build_dir
setlocal
set "_pmn_key="
set "_pmn_val="
for /f "usebackq tokens=1,* delims==" %%A in (`cmake "-DPMN_NAME=%~1" -P cmake\BuildDir.cmake`) do (
set "_pmn_key=%%A"
set "_pmn_val=%%B"
)
if "%_pmn_key%"=="-- BUILD_DIR" if defined _pmn_val goto resolve_ok
if /i "%~3"=="quiet" goto resolve_failed
if defined _pmn_val (
echo error: %_pmn_val% 1>&2
) else (
echo error: cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Install this project's own build artifacts to
# out/install/<platform>-<arch>-<compiler>/ (INSTALL_OUTPUT_DIR in
# Configuration.cmake; see CMakeLists.txt's install() DESTINATION paths) --
# the same name as the out/build/<preset> directory they came from, since
# a preset is named after its target.
#
# Always passes --component to `cmake --install`. Without it, any
# vendored/fetched dependency's own install() rules (see
# cmake/Dependencies.cmake) would also run, typically dumping files under
# CMAKE_INSTALL_PREFIX (e.g. /usr/local) -- COMPONENT is what keeps
# `cmake --install` scoped to just this project's own targets.
#
# Usage (from the project root):
# scripts/install.sh [--preset <name>] [BUILD_TYPE]
# scripts/install.sh --help
#
# --preset <name> Install the build made with that preset
# (out/build/<name>). Without it: the build
# scripts/build.sh made last, or the only one that
# exists. Must come first if given at all;
# --preset=<name> also works.
# BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
# (default: Configuration.cmake's DEFAULT_BUILD_TYPE) --
# only relevant for a Ninja Multi-Config build.
#
# Set BUILD_DIR to install from an exact build directory instead.
set -euo pipefail
case "${1:-}" in
-h|--help)
cat <<'HELP'
Usage: scripts/install.sh [--preset <name>] [BUILD_TYPE]
Installs this project's own build artifacts to
out/install/<platform>-<arch>-<compiler>/ (INSTALL_OUTPUT_DIR in
Configuration.cmake), scoped to just this project via `cmake --install
--component` -- see the comment at the top of this script.
--preset <name> Install the build made with that preset (out/build/<name>).
Default: the build scripts/build.sh made last. Must come
first if given at all; --preset=<name> also works.
BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
(default: Configuration.cmake's DEFAULT_BUILD_TYPE) --
only relevant for a Ninja Multi-Config build.
HELP
exit 0
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
command -v cmake >/dev/null 2>&1 || { echo "error: cmake not found on PATH." >&2; exit 1; }
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir. Reading
# values out of Configuration.cmake this way -- its single source of truth
# -- is the bug fix that replaced COMPONENT being a literal "demo" here,
# with a comment warning it had to be kept in sync with PROJECT_CODE_NAME by
# hand.
source "$SCRIPT_DIR/_common.sh"
# --preset must come first if given at all, same as scripts/build.sh.
PRESET=""
case "${1:-}" in
--preset)
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
echo "error: --preset requires a value, e.g. --preset linux-x64-gcc" >&2
exit 1
fi
PRESET="$2"
shift 2
;;
--preset=*)
PRESET="${1#--preset=}"
shift
if [ -z "$PRESET" ]; then
echo "error: --preset= requires a value, e.g. --preset=linux-x64-gcc" >&2
exit 1
fi
;;
esac
if [ -z "${BUILD_DIR:-}" ]; then
# Explains itself and stops if there's nothing to pick.
BUILD_DIR="$(_pmn_resolve_build_dir "$PRESET")"
fi
BUILD_TYPE="${1:-$(_pmn_read_config DEFAULT_BUILD_TYPE)}"
COMPONENT="$(_pmn_read_config PROJECT_CODE_NAME)"
if [ ! -d "$BUILD_DIR" ]; then
echo "error: '${BUILD_DIR}' not found. Run scripts/build.sh${PRESET:+ --preset $PRESET} first." >&2
exit 1
fi
echo "==> Installing (component: ${COMPONENT}) from ${BUILD_DIR}"
cmake --install "${BUILD_DIR}" --config "${BUILD_TYPE}" --component "${COMPONENT}"
+104
View File
@@ -0,0 +1,104 @@
@echo off
REM Applies clang-format and clang-tidy's --fix across this project's own
REM C/C++ code. The pre-commit hook only CHECKS; this is the "just fix it"
REM counterpart -- review the result with `git diff` afterward, same as any
REM auto-formatter.
REM
REM Defaults to source\ and c-api\, this template's own-code roots -- NEVER
REM walks out\, .cache\, vendor\, or .git\ by default, so it can't
REM accidentally spend minutes reformatting/analyzing a fetched dependency's
REM entire source tree the way scanning the whole repo from "." would.
REM
REM Usage, from the project root: scripts\refactor.bat [optional path]
REM scripts\refactor.bat every file under source\ and c-api\
REM scripts\refactor.bat source\Core just one file or directory -- taken
REM as given, with no exclusion filter,
REM so pick a path that doesn't overlap
REM with out\/.cache\/vendor\/.git\
REM scripts\refactor.bat --help
setlocal enabledelayedexpansion
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
goto after_help
:show_help
echo Usage: scripts\refactor.bat [scope]
echo.
echo Applies clang-format and clang-tidy's --fix across this project's own
echo C/C++ code -- review the result with `git diff` afterward. See the
echo comment at the top of this script for the default scope and why it
echo never walks out\, .cache\, vendor\, or .git\.
echo.
echo ^(no argument^) Every file under source\ and c-api\ ^(whichever exist^).
echo scope Just one file or directory, e.g.:
echo scripts\refactor.bat source\Core
exit /b 0
:after_help
cd /d "%~dp0.."
where clang-format >nul 2>nul
set "HAVE_FORMAT=0"
if not errorlevel 1 set "HAVE_FORMAT=1"
where clang-tidy >nul 2>nul
set "HAVE_TIDY=0"
if not errorlevel 1 set "HAVE_TIDY=1"
if "%HAVE_FORMAT%"=="1" goto have_a_tool
if "%HAVE_TIDY%"=="1" goto have_a_tool
echo error: neither clang-format nor clang-tidy found on PATH. Run scripts\setup.bat for install guidance.
exit /b 1
:have_a_tool
set "FILELIST=%TEMP%\refactor_files_%RANDOM%.txt"
if exist "%FILELIST%" del "%FILELIST%"
REM Collect the file list. Deliberately NOT using an if/else block here --
REM a for /f or for /r loop nested inside a parenthesized if(...) else(...)
REM block confuses cmd.exe's paren-matching for the enclosing block (this
REM was tested and confirmed, not a guess); goto-based branching sidesteps
REM the problem entirely by never nesting a FOR loop inside another block.
if not "%~1"=="" goto collect_custom
set "SCOPE_DESC=source and c-api"
if exist "source" for /r "source" %%F in (*.c *.h *.cpp *.hpp *.cc *.cxx *.tpp) do echo %%F>>"%FILELIST%"
if exist "c-api" for /r "c-api" %%F in (*.c *.h *.cpp *.hpp *.cc *.cxx *.tpp) do echo %%F>>"%FILELIST%"
goto collected
:collect_custom
set "SCOPE_DESC=%~1"
if not exist "%~1" goto collected
for /r "%~1" %%F in (*.c *.h *.cpp *.hpp *.cc *.cxx *.tpp) do echo %%F>>"%FILELIST%"
:collected
if exist "%FILELIST%" goto have_files
echo No C/C++ files found under: %SCOPE_DESC%
exit /b 0
:have_files
echo ==^> Files in scope: %SCOPE_DESC%
if "%HAVE_FORMAT%"=="1" goto do_format
echo ==^> Skipping clang-format ^(not found on PATH -- see scripts\setup.bat^)
goto after_format
:do_format
echo ==^> Running clang-format -i
for /f "usebackq delims=" %%F in ("%FILELIST%") do clang-format -i "%%F"
:after_format
if "%HAVE_TIDY%"=="1" goto do_tidy
echo ==^> Skipping clang-tidy ^(not found on PATH -- see scripts\setup.bat^)
goto after_tidy
:do_tidy
if exist compile_commands.json goto run_tidy
echo ==^> Skipping clang-tidy ^(no compile_commands.json -- run scripts\build.bat first^)
goto after_tidy
:run_tidy
echo ==^> Running clang-tidy --fix ^(auto-fixable issues only; build once first if this is stale^)
for /f "usebackq delims=" %%F in ("%FILELIST%") do clang-tidy --quiet --fix "%%F"
:after_tidy
del "%FILELIST%" 2>nul
echo ==^> Done. Review the changes: git diff
exit /b 0
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Applies clang-format and clang-tidy's --fix across this project's own
# C/C++ code. The pre-commit hook only CHECKS; this is the "just fix it"
# counterpart -- review the result with `git diff` afterward, same as any
# auto-formatter.
#
# Defaults to source/ and c-api/ (this template's own-code roots) --
# NEVER walks out/, .cache/, vendor/, or .git/ by default, so it can't
# accidentally spend minutes reformatting/analyzing a fetched dependency's
# entire source tree (e.g. out/build/_deps/*-src/) the way scanning the
# whole repo from "." would. Passing an explicit scope still excludes
# those four directories defensively, in case the scope you give overlaps
# with them.
#
# Usage (from the project root):
# scripts/refactor.sh # every file under source/ and c-api/
# scripts/refactor.sh source/Core # just one file or directory
# scripts/refactor.sh --help
set -euo pipefail
case "${1:-}" in
-h|--help)
cat <<'EOF'
Usage: scripts/refactor.sh [scope]
Applies clang-format and clang-tidy's --fix across this project's own
C/C++ code -- review the result with `git diff` afterward. See the
comment at the top of this script for the default scope and why it never
walks out/, .cache/, vendor/, or .git/.
(no argument) Every file under source/ and c-api/ (whichever exist).
scope Just one file or directory, e.g.:
scripts/refactor.sh source/Core
EOF
exit 0
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# _pmn_extend_tool_path: finds Homebrew's keg-only llvm (clang-tidy) on macOS.
source "$SCRIPT_DIR/_common.sh"
_pmn_extend_tool_path
if [ $# -ge 1 ]; then
scopes=("$1")
else
scopes=()
[ -d source ] && scopes+=(source)
[ -d c-api ] && scopes+=(c-api)
fi
if [ "${#scopes[@]}" -eq 0 ]; then
echo "No default scope found (expected a source/ and/or c-api/ directory here)." >&2
exit 1
fi
if ! command -v clang-format >/dev/null 2>&1 && ! command -v clang-tidy >/dev/null 2>&1; then
echo "error: neither clang-format nor clang-tidy found on PATH. Run scripts/setup.sh for install guidance." >&2
exit 1
fi
# A read loop rather than mapfile/readarray: those are bash 4+, and macOS
# ships bash 3.2.
files=()
while IFS= read -r _file; do
files+=("$_file")
done < <(
find "${scopes[@]}" \
\( -path './vendor' -o -path './out' -o -path './.cache' -o -path './.git' \) -prune -o \
-type f \( -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.cxx' -o -name '*.tpp' \) -print \
| sed 's|^\./||' | sort
)
if [ "${#files[@]}" -eq 0 ]; then
echo "No C/C++ files found under: ${scopes[*]}"
exit 0
fi
echo "==> ${#files[@]} file(s) in scope: ${scopes[*]}"
if command -v clang-format >/dev/null 2>&1; then
echo "==> Running clang-format -i"
clang-format -i "${files[@]}"
else
echo "==> Skipping clang-format (not found on PATH -- see scripts/setup.sh)"
fi
if command -v clang-tidy >/dev/null 2>&1; then
if [ ! -f compile_commands.json ]; then
echo "==> Skipping clang-tidy (no compile_commands.json -- run scripts/build.sh first)"
else
echo "==> Running clang-tidy --fix (auto-fixable issues only; build once first if this is stale)"
for f in "${files[@]}"; do
clang-tidy --quiet --fix "$f" || true
done
fi
else
echo "==> Skipping clang-tidy (not found on PATH -- see scripts/setup.sh)"
fi
echo "==> Done. Review the changes: git diff"
+185
View File
@@ -0,0 +1,185 @@
@echo off
REM Run an executable module by its CMake target name (as registered in
REM that module's own CMakeLists.txt), resolved via the manifest that
REM _write_module_manifest() generates at configure time -- so this script
REM never needs updating when modules are added, removed, or renamed.
REM
REM Usage (from the project root):
REM scripts\run.bat [--preset <name>] <target-name> [-- program-args...]
REM
REM --preset <name> Run the build made with that preset (out\build\<name>).
REM Without it: the build scripts\build.bat made last, or
REM the only one that exists. Must come first if given at
REM all; --preset=<name> also works.
REM
REM Examples:
REM scripts\run.bat hello_exe
REM scripts\run.bat --preset windows-x64-clang-cl hello_exe
REM scripts\run.bat farewell_exe -- Ale
REM
REM Set BUILD_DIR to run out of an exact build directory instead.
setlocal enabledelayedexpansion
cd /d "%~dp0.."
REM --preset must come first if given at all, same as scripts\build.bat.
REM cmd.exe splits "--preset=<name>" into "--preset" and "<name>" on its own
REM (see build.bat's note on "="), so both spellings arrive here identically
REM and this one branch handles both.
set "PRESET="
if /i not "%~1"=="--preset" goto preset_done
if "%~2"=="" (
echo error: --preset requires a value, e.g. --preset windows-x64-clang-cl 1>&2
exit /b 1
)
set "PRESET=%~2"
shift
shift
:preset_done
REM Suffix for the error hints below that say to run build.bat first.
set "_pmn_hint="
if defined PRESET set "_pmn_hint= --preset %PRESET%"
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
if "%~1"=="" goto usage
set "TARGET_NAME=%~1"
shift
REM Collect any remaining args to pass through to the program (skipping a
REM leading "--" separator if present).
set "PASSTHROUGH="
set "skip_next_dashes=1"
:collect
if "%~1"=="" goto collected
if "!skip_next_dashes!"=="1" if "%~1"=="--" (
set "skip_next_dashes=0"
shift
goto collect
)
set "skip_next_dashes=0"
set "PASSTHROUGH=!PASSTHROUGH! "%~1""
shift
goto collect
:collected
REM BUILD_DIR from the environment beats everything; otherwise --preset, else
REM whichever build was made last (see cmake\BuildDir.cmake). Explains itself
REM and stops if there's nothing to pick.
if not "%BUILD_DIR%"=="" goto build_dir_known
where cmake >nul 2>nul
if errorlevel 1 (
echo error: cmake not found on PATH -- it is needed to work out which build directory to use. 1>&2
echo Put it on PATH, or set BUILD_DIR to the directory explicitly, e.g. out\build\^<preset^>. 1>&2
exit /b 1
)
call :resolve_build_dir "%PRESET%" BUILD_DIR
if errorlevel 1 exit /b 1
:build_dir_known
set "MANIFEST="
for %%F in ("%BUILD_DIR%\module_manifest-*.txt") do (
if not defined MANIFEST set "MANIFEST=%%F"
)
if not defined MANIFEST (
echo error: no build manifest found in "%BUILD_DIR%". Run scripts\build.bat%_pmn_hint% first. 1>&2
exit /b 1
)
set "BIN_PATH="
for /f "usebackq tokens=1,* delims==" %%K in ("%MANIFEST%") do (
if "%%K"=="%TARGET_NAME%" set "BIN_PATH=%%L"
)
if not defined BIN_PATH (
echo error: no executable target named "%TARGET_NAME%" in "%BUILD_DIR%". 1>&2
echo Available targets: 1>&2
for /f "usebackq tokens=1 delims==" %%T in ("%MANIFEST%") do echo %%T 1>&2
exit /b 1
)
if not exist "%BIN_PATH%" (
echo error: "%BIN_PATH%" was not found. Rebuild with scripts\build.bat%_pmn_hint%. 1>&2
exit /b 1
)
REM Which build this came from -- to stderr, so a program's own stdout stays
REM clean to pipe.
echo ==^> Running %TARGET_NAME% ^(%BUILD_DIR%^) 1>&2
"%BIN_PATH%" !PASSTHROUGH!
exit /b %ERRORLEVEL%
:usage
REM Best-effort here: usage should still print with nothing built yet.
if "%BUILD_DIR%"=="" call :resolve_build_dir "%PRESET%" BUILD_DIR quiet
echo Usage: %~nx0 [--preset ^<name^>] ^<cmake-target-name^> [-- program-args...] 1>&2
echo. 1>&2
echo Available executable targets: 1>&2
set "found=0"
for %%F in ("%BUILD_DIR%\module_manifest-*.txt") do (
set "found=1"
for /f "usebackq tokens=1 delims==" %%T in ("%%F") do echo %%T 1>&2
)
if "!found!"=="0" echo (none found -- did you run scripts\build.bat?) 1>&2
exit /b 1
:show_help
REM Best-effort here: help should still print with nothing built yet.
if "%BUILD_DIR%"=="" call :resolve_build_dir "%PRESET%" BUILD_DIR quiet
echo Usage: %~nx0 [--preset ^<name^>] ^<cmake-target-name^> [-- program-args...]
echo.
echo Runs an executable module by its CMake target name, resolved via the
echo manifest scripts\build.bat generates at configure time -- see the
echo comment at the top of this script.
echo.
echo --preset ^<name^> Run the build made with that preset. Default: the
echo build scripts\build.bat made last.
echo.
echo Available executable targets:
set "found=0"
for %%F in ("%BUILD_DIR%\module_manifest-*.txt") do (
set "found=1"
for /f "usebackq tokens=1 delims==" %%T in ("%%F") do echo %%T
)
if "!found!"=="0" echo (none found -- did you run scripts\build.bat?)
echo.
echo Examples:
echo scripts\run.bat hello_exe
echo scripts\run.bat --preset windows-x64-clang-cl hello_exe
echo scripts\run.bat farewell_exe -- Ale
exit /b 0
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks cmake\BuildDir.cmake which build directory to use -- %1 is a preset
REM name, or "default"; empty means "discover one" (the build made last, or
REM the only one) -- and stores it into %2 in the CALLER's scope, with
REM backslashes. Same shape as build.bat's, and the same `cmake -P`
REM one-line-answer protocol: "-- BUILD_DIR=<path>" or "-- ERROR=<text>"
REM (see that file). On failure it prints the reason to stderr, unless a
REM third argument "quiet" is given, and returns errorlevel 1.
REM ---------------------------------------------------------------------
:resolve_build_dir
setlocal
set "_pmn_key="
set "_pmn_val="
for /f "usebackq tokens=1,* delims==" %%A in (`cmake "-DPMN_NAME=%~1" -P cmake\BuildDir.cmake`) do (
set "_pmn_key=%%A"
set "_pmn_val=%%B"
)
if "%_pmn_key%"=="-- BUILD_DIR" if defined _pmn_val goto resolve_ok
if /i "%~3"=="quiet" goto resolve_failed
if defined _pmn_val (
echo error: %_pmn_val% 1>&2
) else (
echo error: cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
Executable
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env bash
# Run an executable module by its CMake target name (as registered in
# that module's own CMakeLists.txt), resolved via the manifest that
# _write_module_manifest() generates at configure time -- so this script
# never needs updating when modules are added, removed, or renamed.
#
# Usage (from the project root):
# scripts/run.sh [--preset <name>] <target-name> [-- program-args...]
#
# --preset <name> Run the build made with that preset (out/build/<name>).
# Without it: the build scripts/build.sh made last, or
# the only one that exists. Must come first if given at
# all; --preset=<name> also works.
#
# Examples:
# scripts/run.sh hello_exe
# scripts/run.sh --preset linux-x64-clang hello_exe
# scripts/run.sh farewell_exe -- Ale
#
# Set BUILD_DIR to run out of an exact build directory instead.
#
# An executable built for another architecture or OS -- linux-arm64-* on an
# x64 machine, windows-*-mingw* from Linux -- is run under the emulator its
# toolchain registered (qemu-user, wine) if one was installed when the build
# was configured; otherwise it can only run on the target itself.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# _pmn_read_config, _pmn_resolve_build_dir, _pmn_record_build_dir.
source "$SCRIPT_DIR/_common.sh"
# --preset must come first if given at all, same as scripts/build.sh.
PRESET=""
case "${1:-}" in
--preset)
if [ "$#" -lt 2 ] || [[ "$2" == -* ]]; then
echo "error: --preset requires a value, e.g. --preset linux-x64-gcc" >&2
exit 1
fi
PRESET="$2"
shift 2
;;
--preset=*)
PRESET="${1#--preset=}"
shift
if [ -z "$PRESET" ]; then
echo "error: --preset= requires a value, e.g. --preset=linux-x64-gcc" >&2
exit 1
fi
;;
esac
# BUILD_DIR from the environment beats everything; otherwise --preset, else
# whichever build was made last (see cmake/BuildDir.cmake). Help and the
# usage message look it up best-effort -- they should still print on a
# fresh checkout with nothing built yet -- while actually running
# something needs a real answer, and stops with the reason if there isn't
# one.
_pmn_try_build_dir() {
if [ -z "${BUILD_DIR:-}" ]; then
BUILD_DIR="$(_pmn_resolve_build_dir "$PRESET" 2>/dev/null || true)"
fi
}
list_targets() {
local manifest
[ -n "${BUILD_DIR:-}" ] || return 0
for manifest in "${BUILD_DIR}"/module_manifest-*.txt; do
[ -e "$manifest" ] || continue
cut -d= -f1 "$manifest"
done | sort -u
}
usage() {
_pmn_try_build_dir
echo "Usage: $0 [--preset <name>] <cmake-target-name> [-- program-args...]" >&2
echo >&2
echo "Available executable targets:" >&2
local targets
targets="$(list_targets)"
if [ -n "$targets" ]; then
echo "$targets" | sed 's/^/ /' >&2
else
echo " (none found -- did you run scripts/build.sh?)" >&2
fi
exit 1
}
show_help() {
_pmn_try_build_dir
echo "Usage: $0 [--preset <name>] <cmake-target-name> [-- program-args...]"
echo
echo "Runs an executable module by its CMake target name, resolved via the"
echo "manifest scripts/build.sh generates at configure time -- see the"
echo "comment at the top of this script."
echo
echo " --preset <name> Run the build made with that preset. Default: the"
echo " build scripts/build.sh made last."
echo
echo "Available executable targets:"
local targets
targets="$(list_targets)"
if [ -n "$targets" ]; then
echo "$targets" | sed 's/^/ /'
else
echo " (none found -- did you run scripts/build.sh?)"
fi
echo
echo "Examples:"
echo " scripts/run.sh hello_exe"
echo " scripts/run.sh --preset linux-x64-clang hello_exe"
echo " scripts/run.sh farewell_exe -- Ale"
exit 0
}
case "${1:-}" in
-h|--help) show_help ;;
esac
[ $# -ge 1 ] || usage
TARGET_NAME="$1"
shift
if [ "${1:-}" = "--" ]; then
shift
fi
if [ -z "${BUILD_DIR:-}" ]; then
# Explains itself and stops if there's nothing to pick.
BUILD_DIR="$(_pmn_resolve_build_dir "$PRESET")"
fi
MANIFEST=""
for m in "${BUILD_DIR}"/module_manifest-*.txt; do
[ -e "$m" ] || continue
MANIFEST="$m"
break
done
if [ -z "$MANIFEST" ]; then
echo "error: no build manifest found in '${BUILD_DIR}'. Run scripts/build.sh${PRESET:+ --preset $PRESET} first." >&2
exit 1
fi
BIN_PATH="$(grep "^${TARGET_NAME}=" "$MANIFEST" | head -n1 | cut -d= -f2- || true)"
if [ -z "$BIN_PATH" ]; then
echo "error: no executable target named '${TARGET_NAME}' in '${BUILD_DIR}'." >&2
echo "Available targets:" >&2
list_targets | sed 's/^/ /' >&2
exit 1
fi
if [ ! -x "$BIN_PATH" ]; then
echo "error: '${BIN_PATH}' was not found or is not executable. Rebuild with scripts/build.sh${PRESET:+ --preset $PRESET}." >&2
exit 1
fi
# A cross build's toolchain file (cmake/toolchains/) registers an emulator --
# qemu-user for another CPU, wine for Windows -- as
# CMAKE_CROSSCOMPILING_EMULATOR when one is installed, which is also what
# lets ctest run the same binaries. Use it the same way. It's a
# ";"-separated list (the emulator, then its arguments).
RUN_PREFIX=()
if [ -f "${BUILD_DIR}/CMakeCache.txt" ]; then
_pmn_emulator="$(sed -n 's/^CMAKE_CROSSCOMPILING_EMULATOR:[A-Z]*=//p' "${BUILD_DIR}/CMakeCache.txt")"
if [ -n "$_pmn_emulator" ]; then
IFS=';' read -r -a RUN_PREFIX <<< "$_pmn_emulator"
fi
fi
# Which build this came from -- to stderr, so a program's own stdout stays
# clean to pipe.
echo "==> Running ${TARGET_NAME} (${BUILD_DIR})${RUN_PREFIX[0]:+ under ${RUN_PREFIX[0]##*/}}" >&2
# Not `exec`, so a failure to start at all can be explained below.
_pmn_status=0
${RUN_PREFIX[@]+"${RUN_PREFIX[@]}"} "$BIN_PATH" "$@" || _pmn_status=$?
# 126 is the shell's "found it, couldn't execute it" -- which is what running
# a binary built for another architecture or OS looks like.
if [ "$_pmn_status" -eq 126 ]; then
echo "error: '${BIN_PATH}' couldn't be started on this machine. If it is a cross build" >&2
echo " (built for another CPU or OS), run it on the target, or install an" >&2
echo " emulator -- qemu-user for another CPU, wine for Windows -- and reconfigure" >&2
echo " (scripts/build.sh --preset <name>) so the toolchain picks it up." >&2
fi
exit "$_pmn_status"
+219
View File
@@ -0,0 +1,219 @@
@echo off
REM One-time (or re-run any time) environment setup for this template.
REM Windows; on Linux and macOS it is scripts/setup.sh.
REM
REM 1. Checks for the tools this project builds with -- Git, CMake 3.25+ and a
REM C++ compiler (Visual Studio Build Tools, or MinGW-w64 through MSYS2:
REM required), plus LLVM and the other MSYS2 toolchains (optional) -- and
REM offers to install whatever is missing, ONE TOOL AT A TIME, showing the
REM exact winget command before running it. Nothing is installed without
REM your yes. The work is done by scripts\_tools.ps1 (PowerShell: prompts,
REM PATH handling and winget are far more robust there than in batch).
REM 2. Fetches the pinned ninja (cmake\FetchNinja.cmake) -- used by
REM scripts\build.bat if found (see there); a system ninja still
REM works fine too, this just avoids needing one installed at all.
REM 3. Fetches the pinned Doxygen (cmake\FetchDoxygen.cmake) -- NOT
REM required to build the project itself (see cmake\Documentation.cmake),
REM only to run scripts\docs.bat afterward.
REM 4. Activates the pre-commit hook (.githooks\pre-commit) for this
REM clone, if it's a git repository.
REM
REM A failure in step 2 or 3 (no pinned binary for this machine, no network)
REM is reported and setup carries on -- it used to stop there, before the hook
REM was activated.
REM
REM Usage (from the project root):
REM scripts\setup.bat [--yes] [--all] [--check] [--skip-tools]
REM scripts\setup.bat --help
REM
REM No delayed expansion in this file, on purpose: PATH is edited below, and
REM with delayed expansion on, a "!" in any directory name would be eaten.
setlocal
REM %~dp0 is captured first: a plain SHIFT (used to walk the arguments below)
REM shifts %0 as well.
set "_PMN_HERE=%~dp0"
set "_PMN_TOOLS_ARGS="
set "_PMN_SKIP_TOOLS=0"
set "_PMN_CHECK=0"
set "_PMN_TOOLS_RC=0"
:parse_args
if "%~1"=="" goto args_done
if /i "%~1"=="-h" goto show_help
if /i "%~1"=="--help" goto show_help
if /i "%~1"=="-y" goto opt_yes
if /i "%~1"=="--yes" goto opt_yes
if /i "%~1"=="--all" goto opt_all
if /i "%~1"=="--check" goto opt_check
if /i "%~1"=="--skip-tools" goto opt_skip_tools
echo error: unknown option "%~1" 1>&2
echo Run scripts\setup.bat --help for usage. 1>&2
exit /b 1
:opt_yes
set "_PMN_TOOLS_ARGS=%_PMN_TOOLS_ARGS% -Yes"
shift
goto parse_args
:opt_all
set "_PMN_TOOLS_ARGS=%_PMN_TOOLS_ARGS% -All"
shift
goto parse_args
:opt_check
set "_PMN_TOOLS_ARGS=%_PMN_TOOLS_ARGS% -Check"
set "_PMN_CHECK=1"
shift
goto parse_args
:opt_skip_tools
set "_PMN_SKIP_TOOLS=1"
shift
goto parse_args
:show_help
echo Usage: scripts\setup.bat [--yes] [--all] [--check] [--skip-tools]
echo.
echo One-time ^(or re-run any time^) environment setup for this template:
echo 1. Checks the tools the project builds with and offers to install what's
echo missing, one tool at a time ^(Git, CMake and a C++ compiler are
echo required; LLVM and the other MSYS2 toolchains are optional^). Each
echo install shows its exact winget command and waits for your yes.
echo 2. Fetches the pinned ninja ^(cmake\FetchNinja.cmake^).
echo 3. Fetches the pinned Doxygen ^(cmake\FetchDoxygen.cmake^).
echo 4. Activates the pre-commit hook ^(.githooks\pre-commit^) for this clone,
echo if it's a git repository.
echo.
echo Options:
echo -y, --yes Install the REQUIRED tools without asking. For scripts and
echo CI; without a terminal and without this, nothing is asked
echo and nothing is installed.
echo --all Also install the optional tools without asking ^(implies
echo --yes^).
echo --check Only report what is and isn't installed; change nothing,
echo fetch nothing. Exits 1 if a required tool is missing.
echo --skip-tools Skip step 1 and go straight to the pinned downloads.
echo.
echo At each question: y install it n not this one a install everything
echo left s skip the rest of the optional tools q stop asking.
echo.
echo See the comment at the top of this script for more on each step.
exit /b 0
:args_done
cd /d "%_PMN_HERE%.."
REM ---- 1. tools --------------------------------------------------------------
if "%_PMN_SKIP_TOOLS%"=="1" goto tools_done
where powershell >nul 2>nul
if errorlevel 1 goto no_powershell
set "_PMN_PATHFILE=%TEMP%\pmn-path-%RANDOM%%RANDOM%.txt"
powershell -NoProfile -ExecutionPolicy Bypass -File "%_PMN_HERE%_tools.ps1" %_PMN_TOOLS_ARGS% -PathOut "%_PMN_PATHFILE%"
set "_PMN_TOOLS_RC=%ERRORLEVEL%"
REM The installer adds tool folders (CMake, Git, ...) to ITS copy of PATH so
REM it can use them; a child process can't change ours, so it hands the list
REM back in a file and they're appended here, for the steps below.
if exist "%_PMN_PATHFILE%" call :take_path_additions
echo.
goto tools_done
:no_powershell
echo error: powershell.exe not found on PATH -- it runs the tool installer. 1>&2
echo Install the tools yourself and re-run with --skip-tools. 1>&2
exit /b 1
:tools_done
if not "%_PMN_CHECK%"=="1" goto after_check
if "%_PMN_TOOLS_RC%"=="0" goto check_ok
echo ==^> Required tools are missing. Run scripts\setup.bat to install them.
exit /b %_PMN_TOOLS_RC%
:check_ok
echo ==^> All required tools are present.
exit /b 0
:after_check
REM Everything below runs CMake scripts.
where cmake >nul 2>nul
if errorlevel 1 goto no_cmake
goto have_cmake
:no_cmake
echo error: cmake not found on PATH -- nothing below can run without it. 1>&2
if "%_PMN_SKIP_TOOLS%"=="1" goto no_cmake_skip
echo Install CMake ^(3.25 or newer^), open a new terminal, and re-run this script. 1>&2
exit /b 1
:no_cmake_skip
echo Re-run without --skip-tools to be offered an install. 1>&2
exit /b 1
:have_cmake
REM ---- 2. pinned ninja -------------------------------------------------------
echo ==^> Fetching pinned ninja (cmake\FetchNinja.cmake)
cmake -P cmake\FetchNinja.cmake
if errorlevel 1 goto ninja_failed
goto ninja_done
:ninja_failed
echo.
echo No pinned ninja here ^(no upstream binary for this machine, or the
echo download failed -- see above^).
where ninja >nul 2>nul
if errorlevel 1 goto ninja_missing
echo The ninja on PATH will be used instead.
goto ninja_done
:ninja_missing
echo scripts\build.bat needs a ninja: e.g. winget install Ninja-build.Ninja
:ninja_done
REM ---- 3. pinned Doxygen -----------------------------------------------------
echo.
echo ==^> Fetching pinned Doxygen (cmake\FetchDoxygen.cmake)
cmake -P cmake\FetchDoxygen.cmake
if errorlevel 1 goto doxygen_failed
goto doxygen_done
:doxygen_failed
echo.
echo Documentation ^(scripts\docs.bat^) won't be available: there's no pinned
echo Doxygen for this machine, or the download failed -- see above.
echo The project itself builds fine without it.
:doxygen_done
REM ---- 4. pre-commit hook ----------------------------------------------------
echo.
git rev-parse --is-inside-work-tree >nul 2>nul
if errorlevel 1 goto hook_skip
echo ==^> Activating pre-commit hook
git config core.hooksPath .githooks
echo core.hooksPath = .githooks
goto hook_done
:hook_skip
echo ==^> Skipping pre-commit hook activation ^(not a git repository yet^)
echo Once you've run "git init", re-run this script or:
echo git config core.hooksPath .githooks
:hook_done
echo.
if "%_PMN_TOOLS_RC%"=="0" goto setup_ok
echo ==^> Setup finished, but a required tool is still missing -- see above.
exit /b 1
:setup_ok
echo ==^> Setup complete. scripts\build.bat and scripts\docs.bat are ready to use.
exit /b 0
REM ---------------------------------------------------------------------
REM :take_path_additions
REM
REM Appends every directory listed in %_PMN_PATHFILE% to PATH, then deletes
REM the file. One "for /f ... do call" line, deliberately not a parenthesized
REM block: a directory like "C:\Program Files (x86)\..." would close it early.
REM ---------------------------------------------------------------------
:take_path_additions
for /f "usebackq delims=" %%D in ("%_PMN_PATHFILE%") do call :add_path "%%D"
del "%_PMN_PATHFILE%" >nul 2>nul
goto :eof
:add_path
set "PATH=%PATH%;%~1"
goto :eof
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
# One-time (or re-run any time) environment setup for this template. Linux
# and macOS; on Windows it is scripts\setup.bat.
#
# 1. Checks for the tools this project builds with -- git, CMake 3.25+ and a
# C++ compiler (both of which are required), and Clang, clang-format,
# clang-tidy, cross compilers, MinGW and emulators (all optional) -- and
# offers to install whatever is missing, ONE TOOL AT A TIME, showing
# the exact command before running it. Nothing is installed without
# your yes. See scripts/_tools.sh for what it knows and how.
# 2. Fetches the pinned ninja (cmake/FetchNinja.cmake) -- used by
# scripts/build.sh / build.bat if found (see there); a system ninja
# still works fine too, this just avoids needing one installed at all.
# Where upstream ships no ninja binary for your machine, it offers to
# install one through your package manager instead.
# 3. Fetches the pinned Doxygen (cmake/FetchDoxygen.cmake) -- NOT
# required to build the project itself (see cmake/Documentation.cmake),
# only to run scripts/docs.sh / docs.bat afterward.
# 4. Activates the pre-commit hook (.githooks/pre-commit) for this
# clone, if it's a git repository.
#
# A failure in step 2 or 3 (no pinned binary for this machine, no network)
# is reported and setup carries on -- it used to abort here, before the hook
# was activated.
#
# Usage (from the project root):
# scripts/setup.sh [--yes] [--all] [--check] [--skip-tools]
# scripts/setup.sh --help
set -euo pipefail
_usage() {
cat <<'EOF'
Usage: scripts/setup.sh [--yes] [--all] [--check] [--skip-tools]
One-time (or re-run any time) environment setup for this template:
1. Checks the tools the project builds with and offers to install what's
missing, one tool at a time (git, CMake and a C++ compiler are
required; Clang, clang-format/clang-tidy, cross compilers, MinGW and
emulators are optional). Each install shows its exact command and
waits for your yes.
2. Fetches the pinned ninja (cmake/FetchNinja.cmake).
3. Fetches the pinned Doxygen (cmake/FetchDoxygen.cmake).
4. Activates the pre-commit hook (.githooks/pre-commit) for this clone,
if it's a git repository.
Options:
-y, --yes Install the REQUIRED tools without asking. For scripts and
CI; without a terminal and without this, nothing is asked
and nothing is installed.
--all Also install the optional tools without asking (implies
--yes).
--check Only report what is and isn't installed; change nothing,
fetch nothing. Exits 1 if a required tool is missing.
--skip-tools Skip step 1 and go straight to the pinned downloads.
At each question: y install it n not this one a install everything
left s skip the rest of the optional tools q stop asking.
See the comment at the top of this script for more on each step.
EOF
}
PMN_TOOLS_YES=0
PMN_TOOLS_ALL=0
PMN_TOOLS_CHECK=0
SKIP_TOOLS=0
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) _usage; exit 0 ;;
-y|--yes) PMN_TOOLS_YES=1 ;;
--all) PMN_TOOLS_ALL=1; PMN_TOOLS_YES=1 ;;
--check) PMN_TOOLS_CHECK=1 ;;
--skip-tools) SKIP_TOOLS=1 ;;
*)
echo "error: unknown option '$1'" >&2
echo "Run scripts/setup.sh --help for usage." >&2
exit 1
;;
esac
shift
done
export PMN_TOOLS_YES PMN_TOOLS_ALL PMN_TOOLS_CHECK
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
# _pmn_tools_run, _pmn_tools_offer and their helpers.
source "$SCRIPT_DIR/_tools.sh"
# ---- 1. tools --------------------------------------------------------------
_tools_rc=0
if [ "$SKIP_TOOLS" = 0 ]; then
_pmn_tools_run || _tools_rc=$?
echo
fi
if [ "$PMN_TOOLS_CHECK" = 1 ]; then
if [ "$_tools_rc" -ne 0 ]; then
echo "==> Required tools are missing. Run scripts/setup.sh to install them."
else
echo "==> All required tools are present."
fi
exit "$_tools_rc"
fi
# Everything below runs CMake scripts.
if ! command -v cmake >/dev/null 2>&1; then
echo "error: cmake not found on PATH -- nothing below can run without it." >&2
if [ "$SKIP_TOOLS" = 1 ]; then
echo " Re-run without --skip-tools to be offered an install." >&2
else
echo " Install CMake (3.25 or newer), open a new terminal, and re-run this script." >&2
fi
exit 1
fi
# ---- 2. pinned ninja -------------------------------------------------------
echo "==> Fetching pinned ninja (cmake/FetchNinja.cmake)"
if ! cmake -P cmake/FetchNinja.cmake; then
echo
echo " No pinned ninja here (no upstream binary for this machine, or the"
echo " download failed -- see above)."
if command -v ninja >/dev/null 2>&1; then
echo " The ninja on PATH will be used instead: $(command -v ninja)"
elif [ "$SKIP_TOOLS" = 0 ]; then
# A build needs SOME ninja; this is the machine-specific moment to say so.
_pmn_tools_offer ninja || echo " scripts/build.sh needs a ninja: install one and put it on PATH."
else
echo " scripts/build.sh needs a ninja: install one and put it on PATH."
fi
fi
# ---- 3. pinned Doxygen -----------------------------------------------------
echo
echo "==> Fetching pinned Doxygen (cmake/FetchDoxygen.cmake)"
if ! cmake -P cmake/FetchDoxygen.cmake; then
echo
echo " Documentation (scripts/docs.sh) won't be available: there's no pinned"
echo " Doxygen for this machine, or the download failed -- see above."
echo " The project itself builds fine without it."
fi
# ---- 4. pre-commit hook ----------------------------------------------------
echo
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "==> Activating pre-commit hook"
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
echo " core.hooksPath = .githooks"
else
echo "==> Skipping pre-commit hook activation (not a git repository yet)"
echo " Once you've run 'git init', re-run this script or:"
echo " chmod +x .githooks/pre-commit && git config core.hooksPath .githooks"
fi
echo
if [ "$_tools_rc" -ne 0 ]; then
echo "==> Setup finished, but a required tool is still missing -- see above."
exit 1
fi
echo "==> Setup complete. scripts/build.sh and scripts/docs.sh are ready to use."
+36
View File
@@ -0,0 +1,36 @@
# Disable raylib examples/games and include FetchContent
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
include(FetchContent)
# Declare and make raylib available
FetchContent_Declare(
raylib
GIT_REPOSITORY "https://github.com/raysan5/raylib.git"
GIT_TAG "master"
GIT_PROGRESS TRUE
)
# 2. Descargar Pugixml (Lector de XML/SVG)
FetchContent_Declare(
pugixml
GIT_REPOSITORY "https://github.com/zeux/pugixml.git"
GIT_TAG "v1.14" # Versión estable
GIT_PROGRESS TRUE
)
# 1. Save the local PLATFORM variable
set(ORIGINAL_PLATFORM "${PLATFORM}")
# 2. Override the local variable to "Desktop" (This shadows the cache and hides "linux" from raylib)
set(PLATFORM "Desktop")
# 3. Configure raylib
FetchContent_MakeAvailable(raylib pugixml)
# 4. Restore your project's local PLATFORM variable back to "linux"
set(PLATFORM "${ORIGINAL_PLATFORM}")
add_exe_module(pacman OUTPUT_NAME "Pacman")
target_link_libraries(pacman PRIVATE raylib pugixml)
+46
View File
@@ -0,0 +1,46 @@
#include "raylib.h"
int main()
{
InitWindow(800, 600, "Attacks for Bombs 1");
int x = 375;
int y = 275;
int tamaño = 50;
int velocidad = 5;
while (!WindowShouldClose())
{
if (IsKeyDown(KEY_W))
{
y -= velocidad;
}
if (IsKeyDown(KEY_S))
{
y += velocidad;
}
if (IsKeyDown(KEY_A))
{
x -= velocidad;
}
if (IsKeyDown(KEY_D))
{
x += velocidad;
}
BeginDrawing();
ClearBackground(WHITE);
DrawRectangle(x, y, tamaño, tamaño, RED);
EndDrawing();
}
CloseWindow();
return 0;
}