Movimiento de "Cuadrado" veloz en mapa blanco
This commit is contained in:
commit
b695b60153
55 files changed
+9391
No files matched your search
+117
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
@@ -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.
|
||||
@@ -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_
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Executable
+81
@@ -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
|
||||
}
|
||||
Executable
+581
@@ -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
|
||||
}
|
||||
Executable
+626
@@ -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
|
||||
}
|
||||
Executable
+358
@@ -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
|
||||
Executable
+280
@@ -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>"
|
||||
Executable
+136
@@ -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
|
||||
Executable
+99
@@ -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
|
||||
Executable
+151
@@ -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
|
||||
Executable
+97
@@ -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}"
|
||||
Executable
+104
@@ -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
|
||||
Executable
+103
@@ -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"
|
||||
Executable
+185
@@ -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
@@ -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"
|
||||
Executable
+219
@@ -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
|
||||
Executable
+161
@@ -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."
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user