Add SDK v1.0.0

This commit is contained in:
alejo committed 2026-10-24 08:05:45 -03:00
commit 40688938ef
76 files changed
+11805

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
---
# .clang-format
# Enforces the formatting rules from the C/C++ Coding Style Guide (Part I: C++ Style).
# Applies equally to Part II C API files per §15 (Indentation and Braces) — same
# tabs/Allman/line-length rules, just different naming (naming can't be enforced
# by clang-format; see .clang-tidy).
Language: Cpp
Standard: Latest
# --- Indentation (style guide §2) ---
UseTab: Always
TabWidth: 4
IndentWidth: 4
ContinuationIndentWidth: 4
IndentCaseLabels: true
IndentPPDirectives: None
IndentExternBlock: Indent
# Access specifiers (public:/private:) sit unindented, flush with the class
# keyword — only members are indented one level, matching every example in §1/§6.
AccessModifierOffset: -4
# Namespace bodies are indented like any other block (style guide §Namespaces).
NamespaceIndentation: All
# --- Braces: Allman style everywhere (style guide §3) ---
BreakBeforeBraces: Allman
Cpp11BracedListStyle: true
# InlineOnly (not Empty or None): out-of-class function definitions always
# get full Allman expansion regardless of length, same as before -- this
# only additionally allows a function *defined inside a class body* to
# stay a compact one-liner if it's short enough to fit. That's specifically
# for the trivial-one-liner exception to "avoid inline member definitions"
# in Class Member Organization (style guide §6) -- a plain getter like
# `int getValue() const { return m_value; }` stays inline-and-compact;
# anything defined out-of-class still always gets the full brace expansion.
AllowShortFunctionsOnASingleLine: InlineOnly
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AllowShortBlocksOnASingleLine: Never
AllowShortEnumsOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
# No trailing "// namespace X" / "// class X" comments on closing braces
# (style guide §Namespaces / §6).
FixNamespaceComments: false
# --- Pointers and references attach to the type (style guide §4) ---
PointerAlignment: Left
ReferenceAlignment: Pointer
DerivePointerAlignment: false
# --- Line length (style guide §8) ---
ColumnLimit: 100
# --- Spacing (style guide §5) ---
SpaceBeforeParens: ControlStatements
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpacesInAngles: false
# --- Include ordering (style guide §7: main header, C headers, C++ std,
# third-party, project headers) ---
SortIncludes: true
SortUsingDeclarations: true
IncludeBlocks: Regroup
IncludeIsMainRegex: '(Test)?$'
IncludeCategories:
# 1. Corresponding header is auto-detected and always sorted first.
# 2. C system headers wrapped for C++ (<cstdio>, <cstdint>, ...).
- Regex: '^<(cassert|cctype|cerrno|cfenv|cfloat|cinttypes|climits|clocale|cmath|csetjmp|csignal|cstdarg|cstddef|cstdint|cstdio|cstdlib|cstring|ctime|cuchar|cwchar|cwctype)>$'
Priority: 1
CaseSensitive: true
# 3. C++ standard library headers (no dot, no slash — <vector>, <memory>, ...).
- Regex: '^<[a-z_]+>$'
Priority: 2
CaseSensitive: true
# 4. Third-party / external library headers (<fmt/format.h>, <boost/...>, ...).
- Regex: '^<.*>$'
Priority: 3
CaseSensitive: true
# 5. Project headers, quoted ("NetworkManager.hpp", ...).
- Regex: '^".*"$'
Priority: 4
CaseSensitive: true
# --- Misc ---
AlignTrailingComments: true
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
# Parameters/arguments: never one-per-line (style guide §8). When a
# signature/call doesn't fit on one line, keep everything together on a
# single continuation line if it fits there (AllowAll...OnNextLine), and
# fall back to packing multiple per line (BinPack...) rather than ever
# breaking to one per line. This can't force a genuinely long signature
# to fit within ColumnLimit without wrapping at all -- doing that would
# require ColumnLimit: 0 project-wide, which would also stop comments and
# ordinary expressions from ever wrapping. See §8 for the full rule.
#
# COMPATIBILITY NOTE: clang-format 20 changed BinPackParameters (and the
# equivalent for call arguments) from a boolean to an enum, and later
# versions deprecated it again in favor of a nested PackParameters/
# PackArguments option. The boolean form below is correct for clang-format
# through 19 (still the most common version in practice, e.g. Ubuntu
# 24.04 LTS ships 18). On clang-format 20 or newer, replace `true` with
# `BinPack` on both lines if you find these are being silently ignored.
BinPackArguments: true
BinPackParameters: true
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
BreakConstructorInitializers: BeforeColon
ConstructorInitializerIndentWidth: 4
PenaltyReturnTypeOnItsOwnLine: 1000
+161
View File
@@ -0,0 +1,161 @@
---
# .clang-tidy
# Enforces naming conventions (style guide §1) and a subset of the Language
# Feature Do's and Don'ts (style guide §10) for C++ code. Place at the repo
# root for Part I (C++) files. For Part II (C API) files, use the
# directory-scoped override in c-api/.clang-tidy instead — clang-tidy picks
# the closest .clang-tidy file up the directory tree, so nesting one there
# gives that subtree different naming rules without affecting the rest of
# the project.
#
# readability-redundant-inline-specifier is a narrow, complementary aid for
# the "avoid inline member definitions" rule in §6 (Class Member
# Organization): it only catches an explicit `inline` keyword that's
# already redundant (e.g. on a function defined in the class body, which
# is implicitly inline regardless). It does not catch — and nothing in
# this file catches — an inline definition that omits the keyword
# entirely, which is the actual, common case the style guide rule is
# about; that one stays a code-review judgment call.
Checks: >
-*,
readability-identifier-naming,
readability-redundant-inline-specifier,
modernize-use-nullptr,
modernize-use-override,
modernize-use-equals-default,
modernize-use-equals-delete,
modernize-loop-convert,
modernize-concat-nested-namespaces,
modernize-use-default-member-init,
modernize-avoid-c-arrays,
modernize-use-nodiscard,
cppcoreguidelines-special-member-functions,
cppcoreguidelines-owning-memory,
cppcoreguidelines-no-malloc,
cppcoreguidelines-pro-type-member-init,
cppcoreguidelines-pro-type-cstyle-cast,
cppcoreguidelines-macro-usage,
performance-unnecessary-value-param,
performance-unnecessary-copy-initialization,
bugprone-exception-escape,
bugprone-use-after-move
WarningsAsErrors: ''
HeaderFilterRegex: '.*\.hpp$'
FormatStyle: file
CheckOptions:
# --- Naming Conventions — style guide §1 ---
- key: readability-identifier-naming.NamespaceCase
value: lower_case
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.StructCase
value: CamelCase
- key: readability-identifier-naming.EnumCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: camelBack
- key: readability-identifier-naming.MethodCase
value: camelBack
- key: readability-identifier-naming.VariableCase
value: camelBack
- key: readability-identifier-naming.LocalVariableCase
value: camelBack
- key: readability-identifier-naming.ParameterCase
value: camelBack
# A plain local `const` (not `static`, not `constexpr`) stays under the
# Local Variables rule (camelCase) per the style guide's own note that
# only *global/static* const/constexpr values are "constants" for this
# rule's purposes -- without this, it silently inherits the general
# ConstantCase (PascalCase) fallback below instead, which would flag
# totally ordinary code like `const std::string name = ...;` inside a
# function. StaticConstantCase/GlobalConstantCase/ConstexprVariableCase
# below are unaffected and still require PascalCase, matching the guide.
- key: readability-identifier-naming.LocalConstantCase
value: camelBack
# Private/protected member variables: camelCase with an m_ prefix.
- key: readability-identifier-naming.PrivateMemberCase
value: camelBack
- key: readability-identifier-naming.PrivateMemberPrefix
value: m_
- key: readability-identifier-naming.ProtectedMemberCase
value: camelBack
- key: readability-identifier-naming.ProtectedMemberPrefix
value: m_
# Public members (e.g. plain data structs) — camelCase, no prefix.
- key: readability-identifier-naming.PublicMemberCase
value: camelBack
# Static class data members — camelCase with an s_ prefix (distinct from
# instance members above). "ClassMember" is clang-tidy's category for
# static (non-const) data members specifically.
- key: readability-identifier-naming.ClassMemberCase
value: camelBack
- key: readability-identifier-naming.ClassMemberPrefix
value: s_
# Global (namespace/file-scope, mutable) variables — camelCase with a g_
# prefix, to flag genuinely global mutable state at every use site.
- key: readability-identifier-naming.GlobalVariableCase
value: camelBack
- key: readability-identifier-naming.GlobalVariablePrefix
value: g_
# Static variables — function-local `static` or file-scope `static` —
# camelCase with an s_ prefix, for the same reason as globals above.
- key: readability-identifier-naming.StaticVariableCase
value: camelBack
- key: readability-identifier-naming.StaticVariablePrefix
value: s_
# Constants and enum values — PascalCase (style guide §1 table).
- key: readability-identifier-naming.ConstantCase
value: CamelCase
- key: readability-identifier-naming.GlobalConstantCase
value: CamelCase
- key: readability-identifier-naming.StaticConstantCase
value: CamelCase
- key: readability-identifier-naming.ClassConstantCase
value: CamelCase
- key: readability-identifier-naming.ConstexprVariableCase
value: CamelCase
# Template parameters — PascalCase (e.g. ValueType).
- key: readability-identifier-naming.TemplateParameterCase
value: CamelCase
- key: readability-identifier-naming.TypeTemplateParameterCase
value: CamelCase
# Type aliases / typedefs — PascalCase, matching class naming.
- key: readability-identifier-naming.TypeAliasCase
value: CamelCase
- key: readability-identifier-naming.TypedefCase
value: CamelCase
# Macros — ALL_CAPS_SNAKE_CASE (the one deliberate exception in §1).
- key: readability-identifier-naming.MacroDefinitionCase
value: UPPER_CASE
# --- cppcoreguidelines-owning-memory — style guide §10, Memory and Ownership ---
# Flags raw new/delete and legacy C allocators used for ownership instead of
# std::unique_ptr / std::shared_ptr.
- key: cppcoreguidelines-owning-memory.LegacyResourceProducers
value: 'malloc;calloc;realloc;strdup;strndup'
- key: cppcoreguidelines-owning-memory.LegacyResourceConsumers
value: 'free;realloc'
# --- cppcoreguidelines-macro-usage — style guide §10, prefer constexpr over #define ---
# Allow include-guard-style and project-prefixed macros; flag everything else
# that could instead be a constexpr constant.
- key: cppcoreguidelines-macro-usage.AllowedRegexp
value: '^[A-Z0-9_]+_H(PP)?$'
- key: cppcoreguidelines-macro-usage.CheckCapsOnly
value: false
+34
View File
@@ -0,0 +1,34 @@
# .editorconfig
# Baseline whitespace/indentation rules for editors that don't run
# clang-format live (or before it runs). See .clang-format for the
# authoritative, full formatting rules from the style guide.
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Windows scripts are CRLF (see .gitattributes). Without this, the global
# end_of_line = lf above would have editors rewrite them as LF.
[*.{bat,cmd,ps1}]
end_of_line = crlf
# Style guide §2 / §15 — tabs everywhere, C++ and C API alike.
[*.{c,h,cpp,hpp,cc,cxx,tpp}]
indent_style = tab
indent_size = 4
tab_width = 4
max_line_length = 100
# Config/build files typically use spaces by convention — not covered by the
# C/C++ style guide, kept here only so this file has a sane fallback for
# CMakeLists.txt, YAML, etc. in the same repo.
[*.{yml,yaml,cmake,json}]
indent_style = space
indent_size = 2
[CMakeLists.txt]
indent_style = space
indent_size = 2
+29
View File
@@ -0,0 +1,29 @@
# .gitattributes
#
# Forces LF line endings for source files regardless of OS or each
# contributor's core.autocrlf setting. Without this, the same file can have
# CRLF on a Windows checkout and LF on Linux/Mac, which makes clang-format
# and the pre-commit hook's diff check report false formatting differences
# that have nothing to do with actual style.
* text=auto eol=lf
# Be explicit for the style guide's own file types, on top of the general
# rule above.
*.c text eol=lf
*.h text eol=lf
*.cpp text eol=lf
*.hpp text eol=lf
*.cc text eol=lf
*.cxx text eol=lf
*.tpp text eol=lf
# Windows-specific scripts keep CRLF (cmd.exe is fussy about LF-only batch
# files; PowerShell scripts follow the same convention).
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Never touch binaries.
*.png binary
*.jpg binary
*.ico binary
+441
View File
@@ -0,0 +1,441 @@
## 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/sysroots/
.cache/tools/
.cache/wine/
compile_commands.json
out/
out/build/
out/dist/
out/docs/
out/install/
# <<< Configuration.cmake generated paths -- END <<<
+33
View File
@@ -0,0 +1,33 @@
# =============================================================================
# CMakeLists.txt -- the project's entry point
#
# Deliberately tiny, and the project's: nothing in it changes when the template
# does. Two directories hold everything else:
#
# project/ this project's settings -- its name, version, languages, C++
# standard, output layout (project/Configuration.cmake)
# breeze/ the template: the build system, toolchains, scripts and docs.
# Replace it wholesale to update the template.
#
# source/ (modules), vendor/ (vendored code) and out/ (everything generated)
# stay at the root, as does c-api/ if you use it.
# =============================================================================
cmake_minimum_required(VERSION 3.25)
# project() takes its arguments from the settings, so they are read first. The
# file holds settings only (no compiler needed), which is what lets the scripts
# read the same file without a configure.
include("${CMAKE_CURRENT_SOURCE_DIR}/project/Configuration.cmake")
project(
${PROJECT_TITLE}
VERSION ${PROJECT_VERSION_NUMBER}
DESCRIPTION "${PROJECT_SUMMARY}"
LANGUAGES ${PROJECT_LANGUAGES}
)
# Everything else -- target selection, output paths, module discovery, install
# rules -- is the template's. Add project-wide CMake of your own below this line
# (or above it, if it has to come before the template's), not in breeze/.
include("${CMAKE_CURRENT_SOURCE_DIR}/breeze/cmake/Breeze.cmake")
+11
View File
@@ -0,0 +1,11 @@
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 25,
"patch": 0
},
"include": [
"breeze/CMakePresets.json"
]
}
+14
View File
@@ -0,0 +1,14 @@
# DemoApp
The project's own README: say what it is here. Doxygen uses this file as the documentation's main page.
The build system is the Breeze template, in [`breeze/`](breeze/README.md); this project's settings are
in [`project/Configuration.cmake`](project/Configuration.cmake).
```sh
breeze/scripts/setup.sh # once per clone; asks before installing anything
breeze/scripts/build.sh # configure and build (Release)
breeze/scripts/run.sh hello_exe World # run a module
```
On Windows use `breeze\scripts\setup.bat`, `breeze\scripts\build.bat` and `breeze\scripts\run.bat`.
+166
View File
@@ -0,0 +1,166 @@
{
"version": 6,
"cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 },
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}"
},
{
"name": "base-linux",
"hidden": true,
"inherits": "base",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" }
},
{
"name": "base-macos",
"hidden": true,
"inherits": "base",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }
},
{
"name": "base-windows",
"hidden": true,
"inherits": "base",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
},
{
"name": "base-mingw",
"hidden": true,
"inherits": "base",
"condition": { "type": "inList", "string": "${hostSystemName}", "list": ["Windows", "Linux"] }
},
{
"name": "linux-x64-gcc",
"inherits": "base-linux",
"displayName": "Linux x64 (GCC)",
"description": "GCC for x64 Linux: native on x64 hardware; on an ARM64 machine a cross build with x86_64-linux-gnu-g++",
"toolchainFile": "${sourceDir}/breeze/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}/breeze/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}/breeze/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}/breeze/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 breeze/cmake/toolchains/_common.cmake",
"toolchainFile": "${sourceDir}/breeze/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=<the triple the cross g++ was built for, normally arm-linux-gnueabihf>) -- see linux-arm32-gcc",
"toolchainFile": "${sourceDir}/breeze/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}/breeze/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}/breeze/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}/breeze/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}/breeze/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}/breeze/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}/breeze/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}/breeze/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}/breeze/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}/breeze/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}/breeze/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" }
]
}
+720
View File
@@ -0,0 +1,720 @@
# CMakeTemplate
A C++20 project template for CMake 3.25+ that builds the same way on Linux, macOS and Windows.
- **Adding code means adding a folder.** A module is a directory under `source/` whose
`CMakeLists.txt` is one or two lines long. The root `CMakeLists.txt` is never edited.
- **The template is one folder.** `breeze/` holds everything that makes it work: the build system,
toolchains, scripts and these docs. `project/` holds this project's own settings, and `source/`,
`vendor/` and `out/` are yours, at the root. Updating the template means replacing `breeze/`.
- **Sixteen targets, one preset each.** Linux (x64, arm64, arm32 × GCC, Clang), macOS (arm64, x64),
Windows (x64, arm64 × MSVC, clang-cl, Clang; x64 and x86 MinGW-w64). Every target has its own
toolchain file and its own build directory, so switching targets never rebuilds anything.
- **One setup script** checks your machine and offers to install what is missing, one tool at a
time, nothing without your yes.
- **A working demo** to copy from: the shared library `core_lib` (with a piece of it, `core_farewell`, in an
objects module of its own), the executables `hello_exe` and
`farewell_exe`, a vendored header-only dependency (`tiny_ansi`), a vendored CMake dependency
(`tiny_case`) and `fmt`, fetched from Git.
**Part 1, using the template:** [Requirements](#requirements) · [Quick start](#quick-start) ·
[Choosing a target](#choosing-a-target) · [Everyday commands](#everyday-commands) ·
[Adding code](#adding-code) · [Settings](#settings) · [Making it yours](#making-it-yours) ·
[Updating the template](#updating-the-template) · [Troubleshooting](#troubleshooting)
**Part 2, how it works:** [Layout](#repository-layout) · [The settings file](#the-settings-file) ·
[Modules](#modules) · [Dependencies](#dependencies) · [Targets and toolchains](#targets-and-toolchains) ·
[Setup internals](#setup-internals) · [Script internals](#script-internals) ·
[Style and tooling](#style-and-tooling) · [Embedding](#embedding) ·
[Verification status](#verification-status)
---
# Part 1: Using the template
## Requirements
- **Required:** Git, CMake 3.25 or newer, and a C++20 compiler (GCC, Clang, MSVC or MinGW-w64).
- **Optional, each only for what uses it:** Clang, clang-format and clang-tidy (`refactor` and the
pre-commit hook), cross compilers, MinGW-w64, `qemu-user` and `wine` (to run cross-built
programs), Doxygen.
- **Fetched for you** into `.cache/tools/`: Ninja 1.13.2 and Doxygen 1.16.1, pinned so every machine
builds with the same versions. A Ninja already on `PATH` is the fallback; the project builds
without Doxygen.
- Setup installs tools through apt, dnf and pacman (Linux), Homebrew (macOS) and winget (Windows).
On anything else it reports what is missing and leaves the installing to you.
## Quick start
```sh
breeze/scripts/setup.sh # once per clone; asks before installing anything
breeze/scripts/build.sh # configure and build (Release)
breeze/scripts/run.sh hello_exe World # Hello, World! (from demo_core)
```
On Windows use `breeze\scripts\setup.bat`, `breeze\scripts\build.bat` and `breeze\scripts\run.bat hello_exe World`, from
`cmd` or PowerShell.
To build for a specific target, name its preset: `breeze/scripts/build.sh --preset linux-x64-clang`. `run`
then follows the build you made last, or takes `--preset` too.
## Choosing a target
A target is a preset named `<os>-<arch>-<compiler>`. `breeze/scripts/build.sh --list-presets` shows the ones
your OS offers. Without `--preset` the first compiler CMake finds is used and the build goes to
`out/build/default/`.
| Presets | Build | Needs |
|---|---|---|
| `linux-x64-gcc` `linux-x64-clang` `linux-arm64-gcc` `linux-arm64-clang` `linux-arm32-gcc` `linux-arm32-clang` | Linux on x86-64, AArch64, 32-bit ARM | On matching hardware, just the compiler. Otherwise a cross compiler for that architecture; the Clang presets also need Clang. |
| `macos-arm64-clang` `macos-x64-clang` | macOS on Apple Silicon, Intel | Xcode Command Line Tools. Either Mac builds both; x64 programs run on Apple Silicon under Rosetta 2. |
| `windows-x64-msvc` `windows-arm64-msvc` | Windows, MSVC | Visual Studio or Build Tools with the C++ workload. ARM64 also needs the ARM64 build tools component; from an x64 machine that is a cross build (`breeze\scripts\setup.bat` offers to add it). |
| `windows-x64-clang-cl` `windows-arm64-clang-cl` | Windows, clang-cl (MSVC ABI) | The above plus clang-cl: LLVM, or Visual Studio's Clang component. |
| `windows-x64-clang` `windows-arm64-clang` | Windows, MSYS2 Clang (MinGW ABI) | MSYS2's CLANG64 (x64) or CLANGARM64 (ARM64) Clang package. MSYS2 has CLANGARM64 only on ARM64 Windows, so `windows-arm64-clang` needs such a machine; from x64 use the ARM64 MSVC or clang-cl presets. |
| `windows-x64-mingw64` `windows-x86-mingw32` | Windows, MinGW-w64 GCC | MSYS2 with MinGW-w64 GCC. Also cross-builds from Linux. |
**Cross builds.** A Linux preset for another architecture compiles with the matching
`<triplet>-g++` (`aarch64-linux-gnu`, `arm-linux-gnueabihf`, `x86_64-linux-gnu`); the MinGW presets
also cross-build from Linux. Setup installs these where your package manager has them (see
[What setup can install](#what-setup-can-install)). If `qemu-user` (Linux targets) or `wine`
(Windows targets) is installed when you configure, `breeze/scripts/run` and `ctest` use it to run the
result.
**Windows ARM64 from an x64 machine** is a cross build too: `windows-arm64-msvc` and
`windows-arm64-clang-cl` compile with Visual Studio's x64-hosted ARM64 tools (`vcvarsall x64_arm64`),
which need the ARM64 build tools component, and the programs cannot run on the build machine.
**Windows environments.** MSVC and clang-cl only work inside a Visual Studio developer environment,
and MSYS2's compilers only with their folder on `PATH`. `breeze\scripts\build.bat` enters the right one for
the preset by itself, for that run only, so no "Native Tools Command Prompt" or MSYS2 shell is
needed; `PMN_NO_ENV=1` turns it off ([details](#windows-environments)). Plain `cmake --preset ...`
and IDEs don't do this: start them from the matching prompt.
## Everyday commands
Every script exists as `.sh` and `.bat` with the same options, answers `-h` / `--help`, and works
from any directory in the repository. `--preset NAME` (or `--preset=NAME`) must come first.
| Script | Usage | Does |
|---|---|---|
| `setup` | `[--yes] [--all] [--everything] [--check] [--skip-tools]` | Checks and installs tools, fetches pinned Ninja and Doxygen, activates the pre-commit hook. |
| `build` | `[--preset NAME] [BUILD_TYPE] [CMAKE_ARGS…]`<br>`--list-presets` | Configures and builds into `out/build/<preset>`. `BUILD_TYPE` is `Debug`, `Release` (default), `RelWithDebInfo` or `MinSizeRel`; further arguments go to CMake as they are, e.g. `-DBUILD_TESTS=ON`. |
| `run` | `[--preset NAME] TARGET [ARGS…]` | Runs an executable by CMake target name; without a target it lists them. Cross-built programs run under qemu or wine when available. |
| `install` | `[--preset NAME] [BUILD_TYPE]` | Installs this project's targets to `out/install/<preset>/{bin,lib}`. |
| `docs` | `[--preset NAME] [MODULE]` | Builds the Doxygen docs of every module, or one, into `out/docs/`. |
| `refactor` | `[PATH]` | Runs clang-format, then clang-tidy `--fix` (which needs a previous build for its compile database), over `source/` and `c-api/`, or just `PATH`. Review with `git diff`. |
`run`, `install` and `docs` act on the build made last unless you pass `--preset`; `BUILD_DIR`
selects an exact directory instead. The build type is not part of the directory name, so changing it
within a preset reconfigures that directory.
**Setup flags.** `--yes` installs the required tools without asking (for CI), `--all` the optional
ones too, `--everything` also the third-party extras (today: Arch's prebuilt 32-bit ARM toolchain
from the AUR), `--check` only reports and exits 1 if a required tool is missing, `--skip-tools` goes
straight to the downloads. At each question: `y` installs, `n` skips that tool, `a` accepts everything
left, `s` skips the remaining optional tools, `q` stops asking. With no terminal and no flag nothing
is installed.
**CMake directly.**
```sh
cmake --preset linux-x64-gcc
cmake --build --preset linux-x64-gcc
ctest --test-dir out/build/linux-x64-gcc # tests need -DBUILD_TESTS=ON
```
IDEs with preset support (CLion, VS Code, Visual Studio) list the presets for your OS; personal ones
go in `CMakeUserPresets.json`. A bare `-DDIST_TARGET=…` only labels a build, it does not choose a
compiler, and a label that disagrees with the compiler stops the configure: pick targets with
presets.
## Adding code
Create a directory with a `CMakeLists.txt`; the next build finds it.
```cmake
# source/MyLib/CMakeLists.txt library: sources in src/, headers in include/
add_lib_module(my_lib TYPE SHARED DEPENDS fmt::fmt)
# source/MyPiece/CMakeLists.txt objects: compiled, no library of its own (a piece of one)
add_objects_module(my_piece) # taken in by naming it: add_lib_module(my_lib DEPENDS my_piece)
# source/MyApp/CMakeLists.txt executable: *.cpp in the module
add_exe_module(my_app DEPENDS my_lib)
# source/tests/my_test/CMakeLists.txt built with -DBUILD_TESTS=ON, registered with CTest
add_test_module(my_test DEPENDS my_lib)
# source/examples/my_example/CMakeLists.txt built with -DBUILD_EXAMPLES=ON
add_example_module(my_example DEPENDS my_lib)
```
Options: `OUTPUT_NAME <file name>` (for an objects module, which has no file, only the name `MODULE_NAME`
carries), `DEPENDS <targets>`, `EXTRA_SOURCES <files>`, `NO_DOCS`. Third-party
code comes in three ways, called from the CMakeLists of the module that needs it:
```cmake
add_vendor_header_only(tiny_ansi) # vendor/tiny_ansi/include, header-only
add_vendor_subdirectory(tiny_case) # vendor/tiny_case/CMakeLists.txt
add_dependency(fmt # an installed copy if new enough, else fetched
GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 12.1.0
FIND_PACKAGE_ARGS 12 NAMES fmt)
```
What one module declares, every module can use: `source/CoreFarewell` depends on the `fmt::fmt` that
`source/Core` declares, whether fmt was fetched or an installed copy was found.
Every module also gets Doxygen docs (`NO_DOCS` opts out; a `README.md` in the module's directory
becomes its main page).
## Settings
Pass options to `build` as CMake arguments, e.g. `breeze/scripts/build.sh -DBUILD_TESTS=ON`.
| Option | Default | Effect |
|---|---|---|
| `BUILD_TESTS`, `BUILD_EXAMPLES` | OFF | Build the modules under `source/tests` and `source/examples`. |
| `INSTALL_TESTS`, `INSTALL_EXAMPLES` | OFF | Also install them (implies the matching `BUILD_` option). |
| `BUILD_SHARED_LIBS` | OFF | Library type for modules that don't say `TYPE`. |
| `ENABLE_DOXYGEN` | ON | Per-module documentation; `-DDOXYGEN_<TAG>=…` sets any Doxyfile tag. |
| `PMN_MINGW_STATIC_RUNTIME` | OFF | MinGW: link the GCC runtime into executables instead of shipping its DLLs. Unsafe when an executable uses a shared module that passes STL types across its interface. |
The project's own settings (its name, version, languages and C++ standard, `PROJECT_CODE_NAME`,
`DEFAULT_BUILD_TYPE`, `OUT_DIR` and the directories under it) live in `project/Configuration.cmake`;
the cache variables among them can also be overridden with `-D`.
## Making it yours
1. In `project/Configuration.cmake` set `PROJECT_TITLE`, `PROJECT_VERSION_NUMBER`, `PROJECT_SUMMARY`,
`PROJECT_LANGUAGES` and `PROJECT_CXX_STANDARD` (the arguments of `project()` and the C++ standard),
`PROJECT_CODE_NAME` (it becomes the `PROJECT_NAME_CODE` compile definition and the install
component) and `DEFAULT_BUILD_TYPE`.
2. Delete the demo modules in `source/` and `vendor/` and add your own.
3. Replace the root `README.md` with the project's own (Doxygen uses it as the main page).
4. Run `breeze/scripts/setup`.
Nothing in `breeze/` needs an edit. The root `CMakeLists.txt` and `CMakePresets.json` are yours to
extend (project-wide CMake of your own, or presets that `inherit` from the template's) and need
nothing to get started.
## Updating the template
The template is the `breeze/` folder and nothing else; no file of the project's lives in it. To
update, replace the folder with the new version's and run `breeze/scripts/setup` again (it points
git's `core.hooksPath` at `breeze/githooks` and re-checks your tools).
| The project's (an update never touches them) | The template's (replaced as a whole) |
|---|---|
| `project/`, `source/`, `vendor/`, `c-api/`; the root `CMakeLists.txt`, `CMakePresets.json` and `README.md` | `breeze/` |
Four kinds of file sit at the root, outside `breeze/`, because the tools that read them look there: the
dot files (`.clang-format`, `.clang-tidy` and `c-api/.clang-tidy`, `.editorconfig`, `.gitattributes`)
and `.gitignore`, whose marked block every configure rewrites. A new template version may change
them: compare them with the new copies. A new version may also read a setting your
`project/Configuration.cmake` doesn't have yet; the configure then stops and names it, and the new
version's own `project/Configuration.cmake` shows its default.
**With the `breeze` CLI** the update is `breeze sdk update` (what is available), `breeze sdk upgrade` (the newest
version of your major version) or `breeze sdk migrate <version>`: it replaces `breeze/` for you (keeping the old one in
`.breeze/backup/`), adds the settings the new version has, and moves a project in the old flat layout to this one. The
SDK's version is `breeze/VERSION`, one line, and a release of the template is a tag (`v1.2.0`) whose `breeze/VERSION`
says the same.
**Moving a project that has the old flat layout** (`cmake/`, `scripts/` and `Configuration.cmake` at the
root): put in `breeze/`, `project/` and the new root `CMakeLists.txt`, `CMakePresets.json` and
`README.md`, and delete the old `cmake/`, `scripts/`, `.githooks/`, `StyleGuide.md` and
`Configuration.cmake`. Carry your values over into `project/Configuration.cmake`: from the old
`Configuration.cmake` (`PROJECT_CODE_NAME`, `DEFAULT_BUILD_TYPE`, any output path you changed) and from
the old `CMakeLists.txt` (the `project()` arguments and the C++ standard). Run `breeze/scripts/setup`
once, which moves `core.hooksPath` from `.githooks` to `breeze/githooks`, and delete `out/build/`: the
old build directories point at files that are gone.
## Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Windows: CMake says `cl` is missing or targets the wrong architecture | CMake ran outside the right Visual Studio developer prompt. Use `breeze\scripts\build.bat`, or open the matching prompt, then delete the build directory. |
| Windows: a MinGW or `-clang` preset can't find its compiler | MSYS2's toolchain folder isn't on `PATH`. `breeze\scripts\build.bat` adds it; set `MSYS2_ROOT` if MSYS2 isn't in `<system drive>\msys64`. |
| Windows: `cannot open msvcrt.lib`, `could not open 'msvcrtd.lib'`, or an x64 `cl` used for an ARM64 preset | The Visual Studio component for that target isn't installed. For ARM64 it is "MSVC … C++ ARM64/ARM64EC build tools (Latest)", which an x64 machine needs to cross-compile. `breeze\scripts\setup.bat` offers to add it, and `breeze\scripts\build.bat` stops with this explanation before CMake runs. |
| `windows-arm64-clang` on an x64 machine says it needs MSYS2's CLANGARM64 | MSYS2 provides CLANGARM64 only on ARM64 Windows. Build ARM64 from x64 with `windows-arm64-msvc` or `windows-arm64-clang-cl`. |
| `linux-arm32-*` fails with `invalid -march= option` or `Scrt1.o: file not recognized` | The cross compiler, binutils and C library come from different package sets. `breeze/scripts/setup.sh` reports this as `[part]`; on Arch x64 it replaces them with a matching prebuilt set. |
| A cross build stops with `Could NOT find X11` (or OpenGL, ALSA, …), though the host has it | Cross builds look only in the target's root, which has the C/C++ runtime and nothing else; the host's libraries are for the wrong architecture. Give the build a target sysroot: a root filesystem for the target with those libraries in `.cache/sysroots/linux-<arch>`, or `-DCMAKE_SYSROOT=<dir>`. See [Cross builds and system libraries](#cross-builds-and-system-libraries). |
| `breeze/scripts/run` can't run a cross-built program | No emulator was installed when the build directory was configured. Install `qemu-user` or `wine` and run `breeze/scripts/build` again. |
| wine complains about its prefix | Delete `.cache/wine/`; it is rebuilt on the next run. |
| "No pinned Ninja / Doxygen binary is configured for this machine" | Pinned downloads exist for x64 and arm64 on Windows, Linux and macOS (Doxygen: x64 Linux and Windows, and macOS). A 32-bit `cmake.exe` on 64-bit Windows looks the same. Use 64-bit CMake, or put Ninja / Doxygen on `PATH` (`-DDOXYGEN_EXECUTABLE=…`). |
| `DIST_TARGET` mismatch error | `-DDIST_TARGET=…` disagrees with the compiler CMake chose. Use `--preset`, or drop the option. |
| `linux-arm32-gcc` looks for a cross compiler on a 32-bit userland running on a 64-bit ARM kernel | `uname -m` reports aarch64 there. Pass `-DCMAKE_CXX_COMPILER=g++`. |
| The pre-commit hook rejects a commit | clang-format or clang-tidy found problems in staged C/C++ files. Run `breeze/scripts/refactor` and review the diff. |
---
# Part 2: How it works
## Repository layout
```
CMakeLists.txt the project's entry point: project(), then breeze/ (never edited)
CMakePresets.json includes breeze/CMakePresets.json; the project's own presets go here
README.md the project's own
project/
Configuration.cmake the project's settings: identity, output paths. One source of truth
breeze/ the template: replace this folder to update it
README.md StyleGuide.md
VERSION the SDK's version, one line (the `breeze` CLI reads it)
CMakePresets.json 16 configure + 16 build presets, plus hidden base presets
cmake/
Breeze.cmake the build system: options, output dirs, module discovery, install rules
TargetSelection.cmake DIST_TARGET -> PLATFORM, ARCHITECTURE, COMPILER_ID
GitignoreSync.cmake keeps the generated block of .gitignore current
BuildHelpers.cmake module macros, output dirs, RPATH, runtime DLLs, manifest
Dependencies.cmake add_vendor_header_only, add_vendor_subdirectory, add_dependency
Documentation.cmake per-module Doxygen targets (+ DoxygenPin, FetchDoxygen)
NinjaPin.cmake pinned Ninja (+ FetchNinja)
HostArch.cmake machine architecture -> x64 / arm64 / arm32
PrintConfig.cmake script mode: print project/Configuration.cmake values
BuildDir.cmake script mode: which build directory a script uses
CopyRuntimeDlls.cmake post-build copy of runtime DLLs
toolchains/ one file per target, plus _common.cmake
scripts/ setup build run install docs refactor, each .sh + .bat
helpers: _common.sh _tools.sh _tools.ps1 _winenv.bat
githooks/pre-commit clang-format + clang-tidy on staged files
source/ Core/ (core_lib) CoreFarewell/ (core_farewell, objects) HelloApp/ FarewellApp/
[tests/ examples/]
vendor/ tiny_case/ (CMake project) tiny_ansi/ (header-only)
c-api/ .clang-tidy for a C API surface
.clang-format .clang-tidy .editorconfig .gitattributes .gitignore
```
Generated, git-ignored: `out/` (`build/<preset>/`, `install/<target>/`, `docs/`, `dist/` reserved)
and `.cache/` (`tools/`, `wine/`, `sysroots/`).
## The settings file
`project/Configuration.cmake` defines the project's identity and every output path. The output
settings are cache variables, so `-D` overrides any; the identity values are plain variables, because
the root `CMakeLists.txt` passes them to `project()`. The scripts never duplicate a value: they read
it through `cmake -P` helpers.
| Variable | Default | Meaning |
|---|---|---|
| `PROJECT_TITLE` `PROJECT_VERSION_NUMBER` `PROJECT_SUMMARY` `PROJECT_LANGUAGES` | `DemoApp` `0.1.0` … `CXX` | The name, `VERSION`, `DESCRIPTION` and `LANGUAGES` of `project()`. |
| `PROJECT_CXX_STANDARD` | `20` | `CMAKE_CXX_STANDARD`; the standard is required and compiler extensions are off. |
| `PROJECT_CODE_NAME` | `demo` | Compile definition `PROJECT_NAME_CODE`; the `cmake --install` component. |
| `DEFAULT_BUILD_TYPE` | `Release` | Used by CMake and the scripts when no type is given. |
| `OUT_DIR` | `<source>/out` | Root of everything generated. |
| `BUILD_OUTPUT_DIR` `INSTALL_OUTPUT_DIR` `DOCS_OUTPUT_DIR` `DIST_OUTPUT_DIR` | `OUT_DIR/build` `/install` `/docs` `/dist` | One build directory per preset, one install directory per `DIST_TARGET`; `dist` is reserved. |
| `TOOLCACHE_DIR` | `<source>/.cache/tools` | Pinned Ninja and Doxygen. |
| `RUNTIME_OUTPUT_SUBDIR` `LIBRARY_OUTPUT_SUBDIR` `ARCHIVE_OUTPUT_SUBDIR` | `bin` `bin` `lib` | Layout inside a build or install tree. |
| `COMPILE_COMMANDS_DESTINATION` | `<source>/compile_commands.json` | Copy of the compile database for clangd and clang-tidy, refreshed after each build. |
`DIST_TARGET`, `PLATFORM`, `ARCHITECTURE` and `COMPILER_ID` are not settings: the template derives them
(`breeze/cmake/TargetSelection.cmake`, see [Target selection](#target-selection)).
- **Script mode.** `cmake "-DPRINT_VARS=A;B" -P breeze/cmake/PrintConfig.cmake` prints `-- A=value` lines.
`cmake [-DPMN_NAME=<preset|default>] [-DPMN_RECORD=ON] -P breeze/cmake/BuildDir.cmake` prints
`-- BUILD_DIR=…` (or `-- ERROR=…`) and, with `PMN_RECORD`, remembers the choice in
`out/build/.last`. A plain `cmake -P` script can include the settings file because it holds
settings only: nothing in it needs a compiler or writes a file. Target selection and the
`.gitignore` sync are the template's, and only a real configure runs them.
- **`.gitignore`.** A marked block is regenerated on every real configure from the output paths, so
moving `OUT_DIR` updates it. The rest of the file is never touched.
## Modules
- **Discovery.** Every `source/<dir>/`, `source/tests/<dir>/` and `source/examples/<dir>/` that
contains a `CMakeLists.txt` is added, with `CONFIGURE_DEPENDS`, so adding or removing a module
reconfigures by itself.
- **Macros** (`breeze/cmake/BuildHelpers.cmake`). `add_lib_module` (`add_static_lib_module` and
`add_shared_lib_module` are shorthands for its `TYPE`), `add_objects_module`, `add_exe_module`,
`add_test_module`, `add_example_module`.
| Macro | Sources (recursive globs) | Output | Notes |
|---|---|---|---|
| `add_lib_module` | `src/*.cpp`, `include/*.h(pp)` | shared: `bin/`, static: `lib/` | `include/` is public. Without `TYPE`, `BUILD_SHARED_LIBS` decides. |
| `add_objects_module` | `src/*.cpp`, `include/*.h(pp)` | nothing of its own: the objects go into whatever names it in `DEPENDS` | `include/` is public. See "Objects modules" below. |
| `add_exe_module` | `*.cpp`, `include/*.h(pp)` | `bin/` | |
| `add_test_module` | same | `bin/tests/` | Registered with CTest and `all_tests`; `EXCLUDE_FROM_ALL` unless `BUILD_TESTS`. |
| `add_example_module` | same | `bin/examples/` | Same, with `BUILD_EXAMPLES`. |
- **What each module target gets.** The compile definitions `MODULE_NAME` (its output name; for an objects
module the `OUTPUT_NAME` it was given),
`PROJECT_NAME`, `PROJECT_NAME_CODE`, `PROJECT_VER` and, in Debug, `DEBUG`; warnings (`/W4
/permissive-` on MSVC, `-Wall -Wextra -Wpedantic` elsewhere); its `DEPENDS` linked `PRIVATE` (objects
modules, below, are the exception for libraries); an
RPATH of `$ORIGIN` (`$ORIGIN/..` for tests and examples), which becomes `@loader_path` for Mach-O
in one place, `_apply_rpath`; and a Doxygen target unless `NO_DOCS`. Project-wide: C++20 without
extensions, position-independent code, `CMAKE_BUILD_WITH_INSTALL_RPATH` off Windows.
- **Objects modules.** `add_objects_module` compiles a directory to object files and stops there: no
archive, no DLL, nothing in `bin/` or `lib/`. Name it in the `DEPENDS` of a library, an executable, a
test, an example or another objects module, and its objects become part of that target. That makes it
the way to split a big library into pieces: each piece is a module of its own, with its own `include/`,
and what remains of the library is a line naming them.
```cmake
# source/CoreMath/CMakeLists.txt
add_objects_module(core_math OUTPUT_NAME "core")
# source/CoreIo/CMakeLists.txt
add_objects_module(core_io OUTPUT_NAME "core" DEPENDS core_math)
# source/Core/CMakeLists.txt the library: its own sources, plus the pieces
add_lib_module(core_lib TYPE SHARED OUTPUT_NAME "core" DEPENDS core_io)
```
- **Name the direct pieces only.** `core_lib` above gets `core_math` too, because `core_io` needs it:
CMake links an object library's objects into the target that names it and no further, so the
template follows the objects modules behind the ones you name (`_resolve_object_modules`, run once
all modules are discovered, so directory order doesn't matter). Cycles stop the configure.
- **Headers.** A piece's `include/` is public, and a library passes its pieces' headers on: whoever
depends on `core_lib` sees `core_math`'s and `core_io`'s headers. Those consumers must not name the
pieces again, which would link a second copy of the objects into them (two copies of the library's
state, when it is shared). An executable takes pieces privately. Keep a piece's private headers in
`src/`.
- **`OUTPUT_NAME`** is only what `MODULE_NAME` carries, so that the pieces of one library all identify
as it. What a piece links itself, a static library for instance, is reached at the final link.
- **What they are not.** Not installed, not run, not in the manifest; always position-independent,
because they may end up in a shared library. Tests and examples can name a piece to test it without
the rest of the library. `source/CoreFarewell` is the demo: `core_lib` takes it in, and
`farewell_exe` finds `core/Farewell.hpp` through `core_lib` alone.
- **Manifest.** Each configure writes `module_manifest-<CONFIG>.txt` into the build directory, one
`target=path` line per executable; `breeze/scripts/run` resolves target names through it.
- **Runtime DLLs (Windows).** Shared modules an executable depends on are copied next to it after
the build (`CopyRuntimeDlls.cmake`: `cmake -E copy_if_different` fails on the empty list that
`$<TARGET_RUNTIME_DLLS:…>` is for most targets). For
MinGW the GCC runtime (`libgcc_s_*-1.dll`, `libstdc++-6.dll`, `libwinpthread-1.dll`) is located
through the compiler and copied the same way, and installed beside the executables.
`PMN_MINGW_STATIC_RUNTIME=ON` links executables statically instead; that is unsafe when they use a
shared module whose interface passes STL types (separate heaps and unwinders), hence off by
default.
- **Install.** Targets are tagged with the component `PROJECT_CODE_NAME` and go to
`out/install/<DIST_TARGET>/{bin,lib}` (tests and examples under `bin/tests`, `lib/tests`, … when
`INSTALL_TESTS` / `INSTALL_EXAMPLES`). `breeze/scripts/install` always passes `--component`, which keeps
the `install()` rules of fetched or vendored dependencies out; a bare `cmake --install` would run
them too.
- **Docs.** A `docs_<target>` target per module plus an aggregate `docs`, built into
`out/docs/<target>/html/` with `out/docs/index.html` linking them. Any Doxyfile tag can be set
as `DOXYGEN_<TAG>`. Without a usable Doxygen, or with `ENABLE_DOXYGEN=OFF`, `breeze/scripts/docs` does nothing and says
why; `-DDOXYGEN_PINNED_VERSION=…` replaces the pinned version.
## Dependencies
`breeze/cmake/Dependencies.cmake` offers `add_vendor_header_only(NAME [INCLUDE_DIR dir])` (an INTERFACE
target over `vendor/<name>/include`), `add_vendor_subdirectory(SUBDIR)` (a source drop, submodule or
subtree with its own `CMakeLists.txt`) and `add_dependency(NAME <FetchContent_Declare args>)`
(`find_package` first when `FIND_PACKAGE_ARGS` is given, a fetch otherwise). All three keep the
dependency's warnings out of your build (`SYSTEM`) and its targets out of `compile_commands.json`,
and all are no-ops when repeated for the same name, so two modules may ask for the same dependency.
The targets are usable from every module, wherever the call was made: an installed copy's imported
targets are directory-scoped by default, so `add_dependency` makes them global for the call
(`CMAKE_FIND_PACKAGE_TARGETS_GLOBAL`), and a sibling module's `DEPENDS fmt::fmt` finds them.
Always give `FIND_PACKAGE_ARGS` a minimum version: without one any installed copy beats the pinned
tag. A dependency's own options are set (`set(<OPTION> … CACHE …)`) before the call;
`source/Core/CMakeLists.txt` is the commented example. In cross builds `find_package` only looks in
the target's root, so a host's `libfmt` is never picked up -- and neither is anything the target's
root lacks, which is what a target sysroot is for (see
[Cross builds and system libraries](#cross-builds-and-system-libraries)).
## Targets and toolchains
### Target selection
`DIST_TARGET` is the one label of a build, `<platform>-<arch>-<compiler>`; `PLATFORM`,
`ARCHITECTURE` (`x64`, `arm64`, `arm32`, `x86`) and `COMPILER_ID` (`gcc`, `clang`, `msvc`,
`clang-cl`, `mingw64`, `mingw32`) follow from it.
- A **toolchain file** (`breeze/cmake/toolchains/<target>.cmake`, three lines each) declares it with
`pmn_toolchain_target(<target>)` and calls one family macro: `pmn_linux_toolchain(arch gcc|clang)`,
`pmn_macos_toolchain(arch)`, `pmn_msvc_toolchain(arch)`, `pmn_clang_cl_toolchain(arch)`,
`pmn_msys_clang_toolchain()`, `pmn_mingw_toolchain(arch)`. An explicit `-DDIST_TARGET` must agree
with the toolchain's.
- **Without a toolchain**, `TargetSelection.cmake` detects the target from the compiler CMake found.
Linux: the machine's architecture (`HostArch.cmake`) and the compiler ID. macOS:
`CMAKE_OSX_ARCHITECTURES`, else `CMAKE_SYSTEM_PROCESSOR`; several architectures (a universal
binary) is an error. Windows: MSVC and clang-cl by `CMAKE_CXX_COMPILER_ARCHITECTURE_ID`, GNU-style
Clang by the host architecture, GCC as MinGW by pointer size (there is no ARM MinGW target:
MinGW-w64's GCC has no ARM64 Windows backend).
- An explicit `-DDIST_TARGET` is **cross-checked** against that detection, and a disagreement stops
the configure with the toolchain file or preset to use. An undetectable architecture also stops
it, with the list of valid values.
- **Presets** (`breeze/CMakePresets.json`, version 6, included by the root `CMakePresets.json`). A hidden `base` (Ninja, `out/build/${presetName}`)
and host-OS bases with `condition`s (`base-linux`, `base-macos`, `base-windows`, `base-mingw` for
Windows or Linux hosts) sit under 16 visible configure presets and 16 build presets with the same
names. There are no test presets.
### Linux
`pmn_linux_toolchain` decides by comparing the requested architecture with the machine's.
- **Native** uses the ordinary `g++` / `clang++`. **Cross** uses `<triplet>-g++` /
`<triplet>-gcc`, resolved to absolute paths so every `try_compile` agrees; a missing compiler stops
with the install hint. A compiler passed with `-DCMAKE_CXX_COMPILER` beats the toolchain's; a stray
`CXX` environment variable does not. A 32-bit userland on a 64-bit ARM kernel reports `aarch64`, so
it needs `-DCMAKE_CXX_COMPILER=g++`.
- **Cross mechanics** (`pmn_cross_target`): `CMAKE_SYSTEM_NAME` / `PROCESSOR` are set, programs come
from the host, and libraries, headers and packages only from the target sysroot (if there is one, see below) and
`/usr/<triplet>`.
- **Emulator** (`pmn_cross_emulator`): `qemu-<arch> -L <root>` for Linux targets, `wine64` /
`wine` for MinGW. It is stored as the cache entry `CMAKE_CROSSCOMPILING_EMULATOR` once found, so an
emulator installed later is picked up by the next configure and a stored choice stays until you
change the entry or delete the build directory. `breeze/scripts/run` and CTest read it from there.
- **Clang** cross builds use `clang++ --target=<triple>` with the cross g++'s libstdc++ and linker.
When that g++ reports a real sysroot (dnf's COPR toolchains, ARM's own), Clang also gets
`--sysroot` and `--gcc-toolchain=/usr`; flat layouts (Debian's `/usr/<triplet>`, pacman's) need
neither.
- **Checks.** The toolchain asks the installed cross g++ about itself instead of trusting a
convention, because a cross compiler is only as good as the binutils and C library installed
beside it. Each check is a warning with the likely cause, printed once, and configure carries on:
- a sysroot that `-print-sysroot` reports but that isn't on disk is dropped (Clang would otherwise
fall back to the host's crt files: `Scrt1.o: file not recognized`);
- Clang's `--target` is the triple `-dumpmachine` reports when it is the same architecture:
ARM's own GCC is `arm-none-linux-gnueabihf`, and Clang finds its libraries only under that
name;
- `-print-prog-name=as` returning a bare `as` means GCC found no assembler for its target and will
run the host's (`invalid -march= option`).
- **Idempotence.** CMake reads a toolchain file again for every scope that needs it (`project()`,
each enabled language, `try_compile`), so everything here must be cheap and leave the same result
when repeated; flags appended to `*_FLAGS_INIT` are added only if absent.
#### Cross builds and system libraries
A cross compiler package ships the target's C/C++ runtime and nothing else, and a cross build looks only
in the target's own root, never at the host's libraries (they are for the wrong architecture). So a
dependency that needs another system library -- raylib's GLFW wants X11, others OpenGL or ALSA -- stops
with `Could NOT find X11` even though the host has it. The fix is a **target sysroot**: a root filesystem
for the target (`usr/include`, `usr/lib`, …) with those libraries installed.
- **Where it is looked for.** `-DCMAKE_SYSROOT=<dir>` (also in a preset), else
`.cache/sysroots/linux-<arch>` (`linux-arm64`, `linux-arm32`, `linux-x64`) if that directory exists:
one per architecture, shared by the gcc and clang presets, so several cross presets build one after
another with no options. The configure prints `Target sysroot: …` or, when there is none,
`Cross build without a target sysroot …`; `breeze/scripts/build.sh` repeats the explanation when such a
configure fails on something not found. `.cache/sysroots/` is git-ignored.
- **What it changes.** `--sysroot` for GCC and Clang; `find_package`/`find_library` search it before the
compiler's own root; qemu runs the result against it (`-L`) when it holds the target's dynamic loader;
pkg-config answers from its `.pc` files only (`PKG_CONFIG_SYSROOT_DIR`, `PKG_CONFIG_LIBDIR`, and
`PKG_CONFIG_PATH` dropped); a Debian-style `usr/lib/<triplet>` layout is found too.
- **Before the first configure, or a fresh build directory after.** A build directory keeps what CMake
detected about the compiler the first time it was configured, so adding, changing or removing a
sysroot afterwards stops the configure and names the directory to delete
(`rm -rf out/build/<preset>`); `breeze/scripts/build.sh` says the same when a configure fails on a
missing library.
- **Getting one.** Whatever gives you the target's own libraries and headers:
- *Copy it from the target*, with its `-dev` packages installed:
`rsync -aR pi@raspberrypi:/lib pi@raspberrypi:/usr/include pi@raspberrypi:/usr/lib .cache/sysroots/linux-arm64/`
(absolute symlinks that come along can be made relative with `symlinks -rc`).
- *Arch host:* the Arch Linux ARM root filesystem (archlinuxarm.org/about/downloads; check the
signature listed there), with the packages installed through qemu. For 32-bit ARM use the `armv7`
tarball and `linux-arm32`.
```bash
sudo pacman -S --needed qemu-user-static qemu-user-static-binfmt arch-install-scripts libarchive
sudo systemctl restart systemd-binfmt
mkdir -p .cache/sysroots/linux-arm64
sudo bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C .cache/sysroots/linux-arm64
sudo arch-chroot .cache/sysroots/linux-arm64 /bin/bash -c \
'pacman-key --init && pacman-key --populate archlinuxarm && pacman -Syu --noconfirm libx11 libxrandr libxinerama libxcursor libxi mesa'
```
- *Debian or Ubuntu host:* `debootstrap --arch=arm64 --include=libx11-dev,libxrandr-dev,libxinerama-dev,libxcursor-dev,libxi-dev,libgl-dev <suite> .cache/sysroots/linux-arm64 <mirror>`
with `qemu-user-static` installed.
- **Or build natively.** Run the build inside that same root filesystem under qemu (`arch-chroot`,
`chroot`) or on the device itself: an aarch64 machine takes the `linux-arm64-*` presets as native
builds, with no sysroot involved.
- **If linking picks the wrong C library.** The sysroot's glibc is used, not the cross package's. A
sysroot much older than the cross compiler's own glibc can fail to link; update the sysroot.
### macOS
Apple's clang is one multi-architecture compiler, so building x86_64 on Apple Silicon, or the
reverse, is an `-arch` flag, not a cross build: no sysroot, no emulator, `CMAKE_CROSSCOMPILING` stays
off. `pmn_macos_toolchain` pins `CMAKE_OSX_ARCHITECTURES` explicitly, even when it equals the
machine's (an x86_64 `cmake` under Rosetta would otherwise quietly build x86_64 for an arm64 target),
and sets `CMAKE_OSX_DEPLOYMENT_TARGET` to 11.0 unless you pass another; Apple's libc++ marks newer
library features (`std::format`, `std::pmr`, …) unavailable below the macOS that introduced them.
Universal binaries are not a target: build both presets and `lipo -create` the results. Apple's
clang (Command Line Tools) and LLVM's own, e.g. Homebrew's, both count as Clang; Homebrew's llvm is
keg-only and is where `clang-tidy` comes from, so `breeze/scripts/refactor.sh` and the hook look in
`/opt/homebrew/opt/llvm/bin` and `/usr/local/opt/llvm/bin` after `PATH`.
### Windows
- **MSVC** (`pmn_msvc_toolchain`) picks `cl`; its architecture comes from the developer environment it
runs in, not from CMake, so the architecture check is what notices the wrong prompt (a 32-bit-
targeting `cl` is reported with the fix). **clang-cl** picks `clang-cl` and pins
`--target=<arch>-pc-windows-msvc`. **MSYS2 Clang** (`pmn_msys_clang_toolchain`) picks `clang++`;
CLANG64 / CLANGARM64 decides its target. **MinGW** (`pmn_mingw_toolchain`) uses
`<triplet>-g++` (`x86_64-w64-mingw32`, `i686-w64-mingw32`), natively on Windows or as a cross build
from Linux, where `wine` runs the result.
- **Checks.** For the presets that link against MSVC's runtime libraries (msvc and clang-cl),
`pmn_check_msvc_target` warns once, before CMake's compiler test fails on it, when the prompt's
`VSCMD_ARG_TGT_ARCH` names another target or `VCToolsInstallDir\lib\<arch>\msvcrt.lib` does not
exist, i.e. the target's Visual Studio component is not installed.
- **Runtime.** MinGW executables use a dynamic GCC runtime whose DLLs are copied beside them (see
[Modules](#modules)).
- **`wine` hosts.** On x64 Debian/Ubuntu `wine` needs `wine64` and `wine32:i386` (an i386 multiarch
that setup offers to enable); on Arch it lives in `multilib`, which setup offers to enable. Wine is
not offered on ARM64 hosts.
#### Windows environments
`breeze\scripts\build.bat` calls `breeze\scripts\_winenv.bat <preset>` right before configuring. The environment
lives only in that run (`setlocal`); the user's own `cmake` is put back in front of `PATH`
afterwards, because Visual Studio's environment adds its own, often older, one.
| Presets | Environment entered |
|---|---|
| `windows-x64-msvc` `windows-arm64-msvc` | `vcvarsall.bat` of the newest Visual Studio or Build Tools that has the target's component, found with `vswhere` (the x86/x64 tools, plus the ARM64 tools for arm64, so an older Visual Studio that has them beats a newer one that doesn't), with `x64`, `x64_arm64`, `arm64` or `arm64_x64` according to the target and the machine (`x86_x64` and `x86_arm64` on 32-bit Windows). It then checks that `lib\<target>\msvcrt.lib` exists under `VCToolsInstallDir` and stops with the component to add if not: with the target's component missing, `vcvarsall` still reports the target and leaves the host's x64 `cl` on `PATH`, so neither its exit status nor `VSCMD_ARG_TGT_ARCH` is proof. |
| `windows-x64-clang-cl` `windows-arm64-clang-cl` | The same, then `clang-cl` on `PATH` if absent: `Program Files\LLVM\bin`, else Visual Studio's own copy (`VC\Tools\Llvm\x64\bin`, `…\ARM64\bin`, `…\bin`). |
| `windows-x64-mingw64` `windows-x86-mingw32` | `<MSYS2>\mingw64\bin` or `mingw32\bin` first on `PATH`, unless the preset's compiler is already on it. |
| `windows-x64-clang` `windows-arm64-clang` | `<MSYS2>\clang64\bin` or `clangarm64\bin` first on `PATH`, always: a `clang++` from elsewhere targets another ABI. `clangarm64` exists only on ARM64 Windows; on x64 the helper says so and points at the cross-building presets. |
`<MSYS2>` is `MSYS2_ROOT`, else `%SystemDrive%\msys64`. Left alone: a shell that already is a
developer prompt for the right target (`VSCMD_ARG_TGT_ARCH` and `cl` on `PATH`), presets it doesn't
know, and everything when `PMN_NO_ENV` is set. `vcvarsall` is judged by what it leaves behind, not
by its exit status; a failure prints its output and the likely fix. The helper is flat batch on
purpose (single-line `if` and `goto`, no parenthesized blocks), because the paths it expands contain
`(x86)`.
## Setup internals
`breeze/scripts/setup.sh` and `setup.bat` run four steps: check and offer the tools, fetch the pinned
Ninja, fetch the pinned Doxygen, activate the pre-commit hook (`git config core.hooksPath
breeze/githooks`). A step that cannot finish (no pinned binary for this machine, no network) says so and
the others still run.
- **Catalog.** `_tools.sh` holds one row per tool: id, required or optional, label, what it is for
and the package for each manager. `_tools.ps1` holds the Windows equivalent. For each missing tool
setup says what it is for, shows the exact command and waits; required tools default to yes,
optional ones to no.
- **Cross compilers must work, not merely exist.** An AArch64, 32-bit ARM or x86-64 cross compiler
counts as installed only if it compiles *and links* a small C++ program (which needs the target's
assembler, headers, libstdc++, crt1.o and libc). One that can't is reported as `[part]` with the
compiler's first error line and offered for repair. After an install the test runs again and, on a
failure, prints `-dumpmachine`, `-print-sysroot`, `-print-prog-name=as` and the installed
`<triplet>-*` packages.
- **Consent.** Third-party extras (today only the AUR toolchain below) are never answered by `--all`
or by an earlier `a`: only `--everything` or a `y` typed at their own question.
### What setup can install
| Tool | apt | dnf | pacman | Homebrew |
|---|---|---|---|---|
| Git, CMake | `git` `cmake` | `git` `cmake` | `git` `cmake` | `git` `cmake` |
| C++ compiler | `g++` | `gcc-c++` | `gcc` | Xcode Command Line Tools |
| Clang | `clang` | `clang` | `clang` | Apple's, from the tools |
| clang-format | `clang-format` | `clang-tools-extra` | `clang` | `clang-format` |
| clang-tidy | `clang-tidy` | `clang-tools-extra` | `clang` | `llvm` |
| AArch64 cross | `g++-aarch64-linux-gnu` | `aarch64-linux-gnu-{binutils,gcc,glibc}` (COPR) | `aarch64-linux-gnu-gcc` (x64 hosts) | — |
| 32-bit ARM cross | `g++-arm-linux-gnueabihf` | `arm-linux-gnueabihf-{binutils,gcc,glibc}` (COPR) | AUR, below (x64 hosts) | — |
| x86-64 cross | `g++-x86-64-linux-gnu` | — | — | — |
| MinGW-w64 | `g++-mingw-w64-x86-64` `g++-mingw-w64-i686` | `mingw64-gcc-c++` `mingw32-gcc-c++` | `mingw-w64-gcc` | — |
| qemu-user | `qemu-user` | `qemu-user` | `qemu-user` | — |
| wine | `wine` `wine64` `wine32:i386` | `wine` | `wine` (multilib) | — |
| Ninja | `ninja-build` | `ninja-build` | `ninja` | `ninja` |
Ninja is only offered where no pinned download exists (32-bit ARM Linux). Fedora's own cross GCC
packages ship no libstdc++, so on dnf the ARM compilers come from lantw44's COPR, enabled only after
a separate confirmation. On Windows, setup uses winget for Git, CMake, Visual Studio 2022 Build
Tools (C++ workload), LLVM and MSYS2; adds the "MSVC ARM64 build tools" component
(`Microsoft.VisualStudio.Component.VC.Tools.ARM64`) to an existing Visual Studio through the
Visual Studio Installer's `modify` command, which asks for administrator rights; and uses MSYS2's
`pacman` for `mingw-w64-x86_64-gcc`,
`mingw-w64-i686-gcc` and `mingw-w64-clang-x86_64-clang`; it can add the tools' folders to the user
`PATH`. On macOS it runs `xcode-select --install` and, if you agree, Homebrew's installer.
**32-bit ARM on Arch** has no official package. The AUR carries ARM's own prebuilt GNU toolchain
(x86-64 hosts only) as four packages, `arm-linux-gnueabihf-{binutils,glibc-headers,glibc,gcc}-bin`.
Setup builds each from its AUR repository with `makepkg` (sources are downloaded once into
`~/.cache/pmn-aur-arm32/`) and installs all four in one transaction,
`pacman -U --noconfirm --needed --ask 4`. `--ask 4` pre-answers yes to "X and Y are in conflict.
Remove Y?" (pacman's default, and `--noconfirm`'s, is no); that is how a source-built binutils or
glibc is replaced, and it applies to any conflicting package in the transaction, which the prompt says.
Leftover `gcc-stage1` / `gcc-stage2` packages are removed first, and nothing is removed until all
four have built. Installing `arm-linux-gnueabihf-gcc-bin` alone through an AUR helper does not work:
its dependencies are generic names (`binutils`, `glibc`, `glibc-headers`), which a helper resolves
to the source-built packages (hours of compiling), leaving ARM's prebuilt GCC beside a binutils and
glibc that don't fit it.
## Script internals
- **Same options, two languages.** Each script is `.sh` (bash 3.2-compatible, macOS's default shell:
no `mapfile`, no bash-4 expansions, empty arrays guarded for `set -u`, BSD `sed` and `mktemp`) and
`.bat`. `_common.sh` holds what the `.sh` scripts share: reading config, resolving the build
directory, the Homebrew llvm lookup. `build.bat` additionally enters the compiler environment
([Windows environments](#windows-environments)); the `.sh` scripts assume you are in the one you
need, which on Linux and macOS is every shell.
- **Which build.** `--preset NAME` selects `out/build/NAME`, otherwise `BuildDir.cmake` picks the
directory recorded in `out/build/.last`, falling back to `default`. `build` records the directory
it used.
- **`build`** prefers the pinned Ninja, then one on `PATH`, then CMake's default generator with a
warning. `cmd.exe` splits `%1`, `%2`, … on `=`, `,` and `;`, which would mangle `-DNAME=value`,
so `build.bat` takes the preset and build type off the front and forwards the rest from the
unsplit `%*`.
- **`run`** finds the executable in `module_manifest-<CONFIG>.txt`, reads
`CMAKE_CROSSCOMPILING_EMULATOR` from the build's `CMakeCache.txt` and runs through it. Under
wine it sets `XDG_RUNTIME_DIR` and a private `WINEPREFIX` in `.cache/wine/` (`WINEARCH` for the
x86 and x64 targets); `run.bat` runs natively.
- **`install`** runs `cmake --install --component <PROJECT_CODE_NAME>` into
`out/install/<DIST_TARGET>`.
- **`refactor`** runs clang-format, then clang-tidy with `--fix`, over `source/` and `c-api/`.
## Style and tooling
`StyleGuide.md` has two parts: C++ (PascalCase and camelCase naming) and a C API surface
(snake_case). Formatting (tabs, Allman braces, line length) comes from the single root
`.clang-format`; naming from the root `.clang-tidy`, and `c-api/.clang-tidy` swaps in the C rules for
files under `c-api/`. `.editorconfig` and `.gitattributes` keep editors and line endings in line
(`.bat` and `.ps1` stay CRLF). The pre-commit hook runs clang-format and clang-tidy on staged C/C++
files and skips with a warning when the tools are missing.
## Embedding
Used through `add_subdirectory`, the root project decides: `OUT_DIR` derives from the outermost
`CMAKE_SOURCE_DIR`, and its toolchain and presets apply.
## Verification status
- **Linux and Windows presets, Arch x64:** the 32-bit ARM presets build with the prebuilt AUR
toolchain (reported by the maintainer). The Arch install and repair logic was also exercised
against stand-in `pacman`, `git`, `makepkg` and cross-compiler programs: broken compiler,
declined, `--all`, `--everything`, a failing build leaving the machine untouched.
- **`_winenv.bat`:** exercised with an emulator of the batch subset it uses against a fake machine
(every preset, 32-bit, x64 and ARM64 hosts, already-correct prompts, an x64 machine without the
ARM64 tools, damaged installs, and each failure path). Its first run on a real `cmd.exe` is its
first test there.
- **Toolchain checks in `_common.cmake`** (including `pmn_check_msvc_target`): written without a
CMake available to run them; the Arch 32-bit ARM configure was their first real run, and the
Windows ones are untested.
- **Setup's ARM64 component row** (`Get-MsvcArm64Status`, `Install-MsvcComponent` in
`_tools.ps1`): written without PowerShell available; only bracket and quote balance were
checked.
- **Target sysroots** (`pmn_find_sysroot`, `pmn_use_sysroot`, the explanation `breeze/scripts/build.sh` adds to a
failed cross configure): exercised on Linux x64 with CMake 3.28 and Ubuntu's cross compilers, against a
*stand-in* sysroot (the cross libc, real X11 headers, and empty stub libraries compiled for the target),
not against Arch Linux ARM or Raspberry Pi OS packages. arm64 and arm32, gcc and clang, found by
directory and by `-DCMAKE_SYSROOT`, flat and Debian-style multiarch layouts, pkg-config kept to the
sysroot, a sysroot without a loader, a missing one, a wrong one; a raylib 5.5 game built for arm64 stops
with `Could NOT find X11` without a sysroot and builds with one. The native x64 and MinGW presets were
rebuilt unchanged. The recipes above for *making* a sysroot (`rsync`, Arch Linux ARM, `debootstrap`) have
not been run, and neither a Mac nor Windows has run any of this; the new code is reached only by Linux
cross builds.
- **The `breeze/` and `project/` layout:** the Linux side was exercised from the finished tree and
from the zip: the presets through the root file's `include` (and a project preset that inherits one),
native x64 gcc, arm64 gcc and clang and arm32 gcc with and without a sysroot, MinGW from Linux, a raylib
game for arm64, and `setup`, `build`, `run`, `install`, `docs`, `refactor`, the `cmake -P` queries and
the pre-commit hook; also replacing `breeze/` with a pristine copy, a `project/Configuration.cmake` that
lacks a setting, and a sysroot added or removed over an existing build directory. The changes to the
`.bat` and `.ps1` scripts are the moved paths and the `cd` to the root, nothing else (each changed line
was compared with the old file; CRLF endings are intact); nothing was run on Windows or on a Mac.
- **Objects modules:** built and run on Linux with gcc and clang for x64, arm64 and arm32 (the ARM ones
under qemu), and with MinGW from Linux (built and linked; the DLL exports the pieces' symbols, nothing
was run). A scratch tree covered pieces three deep, pieces behind a static library, a static and a
shared library, an executable, a test and an example taking pieces, consumers discovered before their
pieces, a raw library name in `DEPENDS`, and a cycle. Each piece appears once in the archive and on the
link line. Not run: MSVC and macOS. For MSVC, CMake's own generator source shows the absorbed objects
feeding the `.def` file that `WINDOWS_EXPORT_ALL_SYMBOLS` produces, so the DLL should export them; that
is a reading, not a run.
- **A dependency declared by one module and used by another** (`fmt::fmt`, declared in `source/Core`,
used by `source/CoreFarewell`): built with fmt 12.1.0 installed (static and shared) and not installed,
with CMake 3.25.2, 3.27.9, 3.28.3, 3.31.6 and 4.1.2, plus a module that CMake reaches before the one
that declares it. Cross builds ignore the host's copy and fetch one for the target.
- **macOS:** the toolchain, `TargetSelection.cmake`'s detection and the RPATH translation were
exercised by cross-compiling to Mach-O from Linux and reading the load commands (`LC_RPATH
@loader_path`, `@rpath/libdemo_core.dylib`, arm64 and x86_64); the scripts ran under a real bash
3.2. No Mac has run Apple's own clang and SDK yet, so look there first if something is off.
+748
View File
@@ -0,0 +1,748 @@
# C/C++ Coding Style Guide
This guide is split into two parts:
- **[Part I: C++ Style](#part-i-c-style)** applies to all internal application/library code written in C++.
- **[Part II: C API Style](#part-ii-c-api-style)** applies specifically to public API surfaces exposed as plain C (`extern "C"`, `.h` headers) — e.g. for dynamic library boundaries, plugin interfaces, or C-consumer compatibility. Keep these two worlds separate: don't let C-API constraints (no exceptions, no classes, `snake_case`) leak into internal C++ code, and don't let C++ constructs leak into a public C API.
## Contents
**[Part I: C++ Style](#part-i-c-style)**
1. [Naming Conventions](#1-naming-conventions)
2. [Indentation](#2-indentation)
3. [Brace Placement (Allman Style)](#3-brace-placement-allman-style)
4. [Pointers and References](#4-pointers-and-references)
5. [Spacing](#5-spacing)
6. [Header Files](#6-header-files)
- [Namespaces](#namespaces)
- [Headers as Public API: Keep Implementation Types Out](#headers-as-public-api-keep-implementation-types-out)
- [Templates](#templates)
- [Implementing Templates (Project Standard)](#implementing-templates-project-standard)
- [Class Member Organization](#class-member-organization)
7. [Include Directive Order](#7-include-directive-order)
8. [Line Length](#8-line-length)
9. [Comments](#9-comments)
10. [Language Feature Do's and Don'ts](#10-language-feature-dos-and-donts)
- [Memory and Ownership](#memory-and-ownership)
- [Error Handling](#error-handling)
- [Modern C++ Features](#modern-c-features)
11. [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading)
- [Static Linking](#static-linking)
- [Dynamic Linking (Shared Libraries)](#dynamic-linking-shared-libraries)
- [Runtime Loading (dlopen and LoadLibrary)](#runtime-loading-dlopen-and-loadlibrary)
- [Summary](#summary)
12. [Miscellaneous](#12-miscellaneous)
**[Part II: C API Style](#part-ii-c-api-style)**
13. [Naming Conventions (C API)](#13-naming-conventions-c-api)
14. [File Extensions and Structure](#14-file-extensions-and-structure)
15. [Indentation and Braces](#15-indentation-and-braces)
16. [API Design Do's and Don'ts](#16-api-design-dos-and-donts)
17. [Boundary Rule of Thumb](#17-boundary-rule-of-thumb)
---
# Part I: C++ Style
## 1. Naming Conventions
| Symbol Type | Convention | Example |
|-------------------------|-------------------|-----------------------------------|
| Classes / Structs | `PascalCase` | `class NetworkManager;` |
| Functions / Methods | `camelCase` | `void connectToServer();` |
| Local Variables | `camelCase` | `int retryCount = 0;` |
| Member Variables (private/protected) | `camelCase` with `m_` prefix | `int m_connectionTimeout;` |
| Static Member Variables (class-scope `static`) | `camelCase` with `s_` prefix | `static int s_instanceCount;` |
| Global Variables (namespace/file scope, mutable) | `camelCase` with `g_` prefix | `int g_activeConnectionCount;` |
| Static Variables (function-local or file-scope `static`) | `camelCase` with `s_` prefix | `static int s_retryAttempts;` |
| Constants | `PascalCase` | `constexpr int MaxRetries = 5;` |
| Enum Type Names | `PascalCase` | `enum class ConnectionState` |
| Enum Values | `PascalCase` | `ConnectionState::Connected` |
| Namespaces | `snake_case` | `namespace network_utils` |
| Template Parameters | `PascalCase` | `template <typename ValueType>` |
| Macros | `ALL_CAPS_SNAKE_CASE` | `#define MAX_BUFFER_SIZE 1024` |
| File Names | `PascalCase` | `NetworkManager.cpp` / `.hpp` |
> **Note:** Macros are an exception to the general PascalCase/camelCase scheme since `ALL_CAPS` is the near-universal C/C++ convention and helps macros stand out visually from regular code.
> **Note:** `g_`/`s_` mark *mutable* global/static state specifically so it stands out at every use site — genuinely global mutable variables are rare and worth flagging by name. A `constexpr` or `const` global/static value is a **constant**, not a variable for this rule's purposes: it keeps the `PascalCase` constant convention above, with no `g_`/`s_` prefix, since it can't cause the aliasing/lifetime bugs the prefix exists to call out (e.g. `constexpr int MaxRetries = 5;` at namespace scope, not `g_MaxRetries`). A `static` variable inside a function keeps `s_` for the same reason as a file-scope one — both persist across calls and are exactly the kind of hidden state the prefix is meant to surface.
---
## 2. Indentation
- Use **tabs** for indentation (not spaces).
- Do not mix tabs and spaces.
- Configure your editor's tab width to display consistently (recommended: 4 columns wide) across the team.
- **Namespace bodies are indented**, exactly like any other block (`if`, `class`, `for`, ...) — a namespace is not a special case left flush against the margin. See [Namespaces](#namespaces) for the full rule and examples.
```cpp
void doSomething()
{
if (condition)
{
performAction();
}
}
```
---
## 3. Brace Placement (Allman Style)
Braces always go on their **own new line**, aligned with the statement that opens the block.
```cpp
// Correct (Allman)
if (isReady)
{
start();
}
else
{
waitForReady();
}
class Widget
{
public:
Widget();
~Widget();
};
void process()
{
for (int i = 0; i < count; ++i)
{
handle(i);
}
}
```
```cpp
// Incorrect (K&R style — do not use)
if (isReady) {
start();
}
```
---
## 4. Pointers and References
Attach `*` and `&` to the **type**, not the variable name.
```cpp
int* pointerToValue;
const std::string& nameRef;
void processData(const Data* data, int& outResult);
```
---
## 5. Spacing
- One space around binary operators: `a + b`, `x == y`.
- No space between a function name and its parentheses: `doWork()`, not `doWork ()`.
- One space after control-flow keywords: `if (`, `for (`, `while (`.
```cpp
if (a == b)
{
total = a + b * c;
}
```
---
## 6. Header Files
- Use `#pragma once` at the top of every header.
- **`.hpp` for C++ headers, `.h` for C headers.** Any header containing C++-only constructs (classes, templates, namespaces, overloads, `enum class`, etc.) uses `.hpp`. Reserve `.h` for headers meant to be valid C — see [Part II: C API Style](#part-ii-c-api-style) for the public C API convention.
- **One `.cpp` per `.hpp`.** Each header declaring a class or module has exactly one corresponding source file implementing it, sharing the same base name (`NetworkManager.hpp` ↔ `NetworkManager.cpp`). Avoid dumping multiple unrelated classes' implementations into one `.cpp`, and avoid splitting one class's implementation across multiple `.cpp` files.
- **One class, struct, or enum per header — meaning one *publicly usable* type.** Each `.hpp` exposes exactly one type as part of its public API: the primary class/struct/enum a caller `#include`s the header to use. A header should never declare two independent, separately-usable types (e.g. `NetworkManager` and an unrelated `RetryPolicy`) side by side — each gets its own `.hpp`. This is what prevents the dependency problem: a file that only needs `RetryPolicy` should never be forced to also compile/depend on everything `NetworkManager` pulls in. Small types that have no meaning or use *outside* the primary type — a nested `enum class` used only as one of its parameters, a tag type only passed to its constructor — aren't "separate" types for this rule; they're part of the primary type's interface and stay in the same header.
- Header-only components (templates, small inline utilities) are the exception to the one-`.cpp`-per-`.hpp` rule — they may have a `.hpp` with no matching `.cpp` — but the one-type-per-header rule still applies to them (see [Templates](#templates) below).
- Match header/source file names to the primary class (`PascalCase`), e.g. `NetworkManager.hpp` / `NetworkManager.cpp` for `class NetworkManager`.
- **Do not add a trailing comment on a closing brace** (`}; // class NetworkManager`, `} // namespace network_utils`, `} // if (isReady)`, etc.), for namespaces, classes, functions, or any other block. Keep blocks short and readable enough that the comment isn't needed — if a block is too long to tell what its closing brace belongs to at a glance, the fix is to shorten or restructure the block, not to annotate the brace.
```cpp
// NetworkManager.hpp
#pragma once
namespace network_utils
{
class NetworkManager
{
public:
NetworkManager();
~NetworkManager();
void connectToServer();
private:
int m_connectionTimeout;
};
}
```
### Namespaces
- **Namespace contents are indented**, exactly like the body of any other block. Don't leave everything inside a namespace flush against the left margin just because the namespace itself doesn't "do" anything at runtime — indentation is what makes nesting depth obvious at a glance, and a namespace is nesting.
- **Use the C++17 nested-namespace syntax (`namespace outer::inner { ... }`) instead of manually nesting namespace blocks**, whenever you're declaring a fixed, known multi-level namespace (e.g. `network_utils::detail`). Manually nesting `namespace network_utils { namespace detail { ... } }` for a case like this just adds an extra level of indentation and an extra closing brace for no benefit.
- The one case that still requires manual nesting: wrapping an **anonymous** namespace inside a named one (e.g. hiding a `.cpp`-local helper type inside `network_utils`) — anonymous namespaces have no name, so there's nothing to join with `::`.
```cpp
// Preferred — a fixed multi-level namespace uses the single nested-namespace form
namespace network_utils::detail
{
struct RetryState
{
int attemptCount;
int backoffMs;
};
}
```
```cpp
// Avoid — manually nesting two named namespaces that could be joined with ::
namespace network_utils
{
namespace detail
{
struct RetryState
{
int attemptCount;
int backoffMs;
};
}
}
```
### Headers as Public API: Keep Implementation Types Out
Treat every header as a **public interface contract**, not a scratchpad for whatever the implementation happens to need. A caller including `NetworkManager.hpp` should see only what they need to *use* `NetworkManager` — not the types that exist purely to help `NetworkManager.cpp` implement it internally.
- **Implementation-only helper types belong in the `.cpp`, not the `.hpp`.** If a class needs a small helper struct, enum, or class that callers never see or interact with (a parser's internal token type, a lookup-table entry, a strategy object used only inside one function), define it in the `.cpp` file, wrapped in an unnamed (anonymous) namespace so it has internal linkage and can't leak or collide across translation units:
```cpp
// NetworkManager.cpp
#include "NetworkManager.hpp"
namespace network_utils
{
namespace
{
// Implementation detail — never appears in the header, never used outside this file.
struct RetryState
{
int attemptCount;
int backoffMs;
};
}
void NetworkManager::connectToServer()
{
RetryState state{0, 100};
// ...
}
}
```
- **Forward-declare instead of including, whenever the header only needs a pointer or reference to another type.** This keeps the header's dependency footprint to exactly the API surface it exposes, rather than transitively dragging in every header its implementation happens to use.
```cpp
// NetworkManager.hpp
#pragma once
namespace network_utils
{
class Socket; // forward declaration — full definition only needed in NetworkManager.cpp
class NetworkManager
{
public:
void connectToServer();
private:
Socket* m_socket;
};
}
```
- **One real C++ constraint to know:** a class's *own* private member variables (as opposed to helper types) generally must still be declared in the header, because C++ needs the complete type — including all members — to know the class's size and layout wherever it's used by value, on the stack, or as a direct member of another class. This is different from hiding a *helper type*; you're not hiding the fact that `NetworkManager` has a timeout, only hiding the definition of an unrelated struct it happens to use internally.
- If you need to hide private *members* too — for a stable ABI across a shared-library boundary (see [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading)) or to shrink recompiles when private internals change — the standard technique is the **Pimpl idiom** (pointer to implementation): the header holds only a forward-declared pointer, and the real member variables live in a struct defined entirely in the `.cpp`.
```cpp
// NetworkManager.hpp
#pragma once
#include <memory>
namespace network_utils
{
class NetworkManager
{
public:
NetworkManager();
~NetworkManager();
void connectToServer();
private:
struct Impl; // forward-declared, defined only in NetworkManager.cpp
std::unique_ptr<Impl> m_impl;
};
}
```
```cpp
// NetworkManager.cpp
#include "NetworkManager.hpp"
namespace network_utils
{
struct NetworkManager::Impl
{
int connectionTimeout;
};
NetworkManager::NetworkManager() : m_impl(std::make_unique<Impl>()) {}
NetworkManager::~NetworkManager() = default;
void NetworkManager::connectToServer()
{
// use m_impl->connectionTimeout
}
}
```
Pimpl isn't required for every class — it adds an extra pointer indirection and a heap allocation, so reserve it for types where header-hiding genuinely matters (public library API, ABI-sensitive dynamic-library boundaries, or classes whose private internals churn often enough that reducing rebuild fan-out is worth the cost). For ordinary internal application classes, plain private members in the header (as in the example at the top of this section) are fine — only the *helper types*, not the members themselves, need to move out.
### Templates
Templates are *more* exposed to the one-type-per-header problem than ordinary classes, for two structural reasons specific to how templates compile:
1. **No compiled-`.cpp` firewall.** An ordinary class hides its implementation behind a `.cpp` — callers only need the declaration to link against it. A template has no such firewall: the compiler needs the *full definition* everywhere it's instantiated, so templates live entirely in headers (see the exception to "one `.cpp` per `.hpp`" in [Header Files](#6-header-files)). That means anything sharing a header with a template gets pulled into every translation unit that only wanted that one template.
2. **Parsing cost applies even to unused templates.** Bundling `template <typename T> class Cache` and `template <typename T> class Pool` in the same header means a file that only uses `Cache` still has to parse `Pool`'s full definition, and a change to `Pool` still bumps the header's timestamp — forcing a rebuild of every translation unit that includes it, including the ones that never touch `Pool` at all.
Given that, the rules:
- **One template class/function per header.** `Cache` and `Pool` each get their own `.hpp`, even if they're conceptually related utilities that usually get used together.
- **Keep a template's declaration and inline implementation in the same `.hpp`** — templates generally can't be split into a separate `.cpp` the way non-template code can (see reason 1 above) — but that's not license to merge *other*, unrelated templates into the same file alongside it.
- **If several templates genuinely share implementation details** (a common traits struct, a shared constant), factor *only that shared piece* into its own single-purpose header, rather than merging the templates themselves into one file. If the shared piece belongs in a nested namespace (e.g. an internal `detail` namespace not meant for outside use), declare it with the `::` nested-namespace syntax from [Namespaces](#namespaces).
- **Avoid including heavy headers** (e.g. `<iostream>`, large third-party headers) inside a widely-used template header purely for convenience — because template headers are included wherever they're instantiated, an unnecessary include here silently bloats compile times project-wide, multiplied across every instantiation site.
```cpp
// Avoid — two unrelated templates sharing one header
// Cache.hpp
#pragma once
namespace storage
{
template <typename Key, typename Value>
class Cache
{
public:
void insert(const Key& key, const Value& value);
const Value* find(const Key& key) const;
};
template <typename T>
class Pool
{
public:
T* acquire();
void release(T* item);
};
}
```
```cpp
// Preferred — one template per header
// Cache.hpp
#pragma once
namespace storage
{
template <typename Key, typename Value>
class Cache
{
public:
void insert(const Key& key, const Value& value);
const Value* find(const Key& key) const;
};
}
```
```cpp
// Pool.hpp — separate file, included only where Pool is actually needed
#pragma once
namespace storage
{
template <typename T>
class Pool
{
public:
T* acquire();
void release(T* item);
};
}
```
If `Cache` and `Pool` both rely on the same internal helper — say, a fixed-capacity ring buffer trait — that shared piece moves to its own header, in an internal namespace declared with `::`:
```cpp
// DetailTraits.hpp — included by both Cache.hpp and Pool.hpp, nothing else
#pragma once
namespace storage::detail
{
template <typename T, std::size_t Capacity>
struct RingBufferTraits
{
using ValueType = T;
static constexpr std::size_t capacity = Capacity;
};
}
```
#### Implementing Templates (Project Standard)
Templates are the one exception to this guide's header-declares/`.cpp`-implements split: the compiler needs a template's *complete definition* at every point it gets instantiated, and a separately-compiled `.cpp` can't supply that. So the implementation has to live in the header — but to keep this consistent across the codebase rather than a per-template judgment call, **this project uses one standard pattern**:
> **Declare template members in the class body, and define them out-of-line further down in the same header** — the same shape as an out-of-line member definition of a non-template class, just kept in the `.hpp` instead of moved to a `.cpp`.
```cpp
// Cache.hpp
#pragma once
namespace storage
{
template <typename Key, typename Value>
class Cache
{
public:
void insert(const Key& key, const Value& value);
const Value* find(const Key& key) const;
private:
std::unordered_map<Key, Value> m_entries;
};
template <typename Key, typename Value>
void Cache<Key, Value>::insert(const Key& key, const Value& value)
{
m_entries[key] = value;
}
template <typename Key, typename Value>
const Value* Cache<Key, Value>::find(const Key& key) const
{
auto it = m_entries.find(key);
return it != m_entries.end() ? &it->second : nullptr;
}
}
```
- **Exception:** a genuinely trivial one-line body (a plain getter, a one-expression forwarding call) can stay inline in the class, same as it would for a non-template class. Don't force it out-of-line just for consistency's sake.
- **Escape hatch, not a pattern:** if a template is only ever meant to be used with a small, fixed set of types, its definitions *can* move to a `.cpp` via explicit instantiation (`template class Cache<int, std::string>;`). This is a deliberate, rare trade-off — it turns a general-purpose template into one that only supports the explicitly instantiated types — and should be a documented exception with a stated reason (e.g. reducing header bloat for a widely-included, ABI-sensitive type), not the default way templates get written.
> **See also:** [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading) — the Pimpl idiom above exists mainly to satisfy the ABI constraints described there · [Part II: C API Style](#part-ii-c-api-style) — the `.h`/`.hpp` split introduced here is defined in full there.
### Class Member Organization
- **Member order: `public`, then `protected`, then `private`.** Group each access level together, in that order. The widest audience — everyone who includes the header — reads what's relevant to them (the public interface) first, without wading through implementation details to find it. Don't reopen an access specifier further down "for organization"; interleaving defeats the ordering rule's purpose.
- **Avoid inline member function definitions in the class body.** Declare the member in the class; define its body separately — out-of-line in the matching `.cpp` for an ordinary class (see the `NetworkManager` example in [Header Files](#6-header-files) above), or out-of-line further down the same header for a template member (see [Implementing Templates](#implementing-templates-project-standard) above). This keeps the class body itself a clean, scannable interface instead of a mix of signatures and implementation.
- **Exception:** a genuinely trivial one-line body (a plain getter, a one-expression forwarding call) may stay inline in the class — the same exception already noted for templates above.
- **Inline template members go at the bottom of their access section.** When a class mixes ordinary members with template members that use the trivial-one-liner exception just above, keep the plain declarations together at the top of each `public`/`protected`/`private` block, and place the inline template members after them, at the bottom of that same block — not interleaved. A reader scanning the interface sees the plain signatures first; the inline implementation detail comes last, where it doesn't interrupt that scan.
```cpp
class NetworkManager
{
public:
NetworkManager();
~NetworkManager();
void connectToServer();
bool isConnected() const;
// Inline template member (trivial-one-liner exception, see Templates
// above) -- kept at the bottom of this access section rather than
// interleaved with the plain declarations above.
template <typename T> T configValue() const { return static_cast<T>(m_connectionTimeout); }
protected:
virtual void onDisconnected();
private:
int m_connectionTimeout;
};
```
None of this is mechanically enforced — `readability-redundant-inline-specifier` (see `.clang-tidy`) catches one narrow related mistake (a redundant explicit `inline` keyword) but not the member-order or inline-body rules themselves.
---
## 7. Include Directive Order
Group includes into blocks, separated by a blank line, in the following order. Within each block, sort alphabetically.
1. **Corresponding header** — for a `.cpp`, its own `.hpp` comes first (catches missing-include bugs in the header itself).
2. **C system headers** — `<cstdio>`, `<cstdlib>`, etc.
3. **C++ standard library headers** — `<vector>`, `<memory>`, `<string>`, etc.
4. **Third-party / external library headers** — e.g. `<boost/...>`, `<fmt/...>`.
5. **Project headers** — other headers from this codebase, using quoted includes (`"..."`).
```cpp
// NetworkManager.cpp
#include "NetworkManager.hpp" // 1. corresponding header
#include <cstdint> // 2. C system headers
#include <memory> // 3. C++ standard library
#include <string>
#include <vector>
#include <fmt/format.h> // 4. third-party libraries
#include "Logger.hpp" // 5. project headers
#include "SocketWrapper.hpp"
```
- Use angle brackets `<...>` for system, standard library, and third-party headers.
- Use quotes `"..."` for project-local headers.
- This ordering guarantees each header can be compiled standalone (no hidden dependency on something included before it) and makes missing includes fail fast in CI rather than silently compiling due to include order in some other file.
---
## 8. Line Length
- Prefer a maximum of **100 characters** per line.
- **Do not wrap parameters onto their own line.** Keep a function's parameter list together — on the declaration line if it fits, otherwise packed as tightly as the line allows on a continuation line. Never lay parameters out one per line; that turns a short signature into a tall list a reader has to scroll past to see the return type and name.
- `clang-format` enforces the "never one-per-line" part. It can't force a genuinely long signature to fit within 100 characters without wrapping at all — that would need line length limits disabled project-wide, which would also stop ordinary long expressions and comments from ever wrapping, so it isn't done. If a signature is long enough that even packing parameters together still overflows, let it overflow rather than reach for one-per-line as the fallback.
---
## 9. Comments
- **Document symbols with Doxygen-style comments, never a plain `//`.** Any comment documenting a function, class, struct, enum, member variable, or macro — explaining what it *is* or *does*, as opposed to annotating a line of implementation — uses `///` or `/** ... */`. This is what makes the project's generated documentation (see the Doxygen integration) actually useful instead of a bare list of undocumented signatures.
- **`///` for most documentation; `/** ... */` for longer, multi-paragraph explanations.** Both are valid Doxygen syntax — pick based on length, not per-file preference.
- **Plain `//` is still correct for everything that isn't documenting a symbol** — a note on a tricky line inside a function body, a `// TODO`, a block disabled during debugging. The rule above is about comments attached to a declaration, not every comment everywhere.
```cpp
/// Establishes a connection to the configured server.
/// @return true if the connection succeeded.
bool connectToServer();
```
```cpp
/**
* Parses and validates a server configuration blob.
*
* Accepts either JSON or the legacy INI format; the format is
* auto-detected from the first non-whitespace byte.
*
* @param raw the raw configuration bytes, in either supported format.
* @return a parsed Config, or std::nullopt if raw is malformed.
*/
std::optional<Config> parseConfig(std::string_view raw);
```
---
## 10. Language Feature Do's and Don'ts
### Memory and Ownership
| Do | Don't |
|----|-------|
| Use `std::unique_ptr` / `std::shared_ptr` for owning dynamic objects | Use raw `new` / `delete` for ownership |
| Use raw pointers or references for **non-owning** access | Use a raw pointer to express or transfer ownership |
| Use `std::make_unique` / `std::make_shared` to construct smart pointers | Construct a smart pointer with `new` directly (`std::unique_ptr<T>(new T())`) |
| Follow RAII for all resources (files, locks, sockets, etc.) | Manually pair acquire/release calls |
### Error Handling
| Do | Don't |
|----|-------|
| Throw exceptions for **true exceptional situations** (unrecoverable errors, invariant violations, constructor failures) | Use exceptions for expected/recoverable control flow (e.g. "value not found") |
| Use return values (`bool`, `std::optional`, error codes, `std::expected` where available) for expected failure cases | Use exceptions as a substitute for normal branching logic |
| Catch exceptions by `const&` | Catch exceptions by value (causes slicing) |
| Keep destructors `noexcept` (the default) | Let exceptions escape a destructor |
### Modern C++ Features
| Do | Don't |
|----|-------|
| Use `auto` when it improves readability (iterators, lambdas, long template types) | Use `auto` when it hides an important or non-obvious type |
| Use range-based `for` loops over index-based loops when the index isn't needed | Write manual index loops just to iterate a container |
| Use lambdas for short, local callbacks and algorithm predicates | Write a named functor class for a one-off, trivial operation |
| Use structured bindings for multi-value returns (`auto [key, value] = ...`) | Unpack pairs/tuples manually with `.first` / `.second` when structured bindings are clearer |
| Use `constexpr` / `consteval` for compile-time constants and computations | Use `#define` macros for constants |
| Use `enum class` over plain `enum` | Use unscoped `enum` (pollutes namespace, weak typing) |
| Use `override` and `final` explicitly on overriding virtual functions | Omit `override`, relying on the compiler to catch signature mismatches |
| Use `nullptr` | Use `NULL` or `0` for pointers |
| Prefer `std::array` / `std::vector` over C-style arrays | Use raw C-style arrays for anything beyond fixed, tiny buffers |
| Pass small, cheap-to-copy types by value; pass large objects by `const&` | Pass large objects by value out of habit |
| Use `std::string_view` for read-only, non-owning string parameters | Take `const std::string&` when the caller may pass a string literal or substring |
---
## 11. Library Linking and Runtime Loading
How a component is built and linked shapes how strictly some of the above rules should be enforced.
### Static Linking
- Libraries are compiled directly into the final binary at build time.
- Header/implementation split still matters for compile-time isolation, but ABI stability across the boundary is **not** a concern — the whole binary is rebuilt together.
- Inline-heavy, template-heavy, and header-only code (common with `auto`, templates, `constexpr`) is safe here since everything resolves at compile time in one binary.
### Dynamic Linking (Shared Libraries)
- Applies to `.so` (Linux), `.dll` (Windows), and `.dylib` (macOS) targets.
- The public headers exposed across a shared-library boundary form an **ABI contract** — style choices here have real consequences:
- Avoid exposing `inline` functions, templates, or `constexpr` values in the public API surface if the library and its consumers may be compiled by different compilers/versions — inlined code bakes assumptions into the caller.
- Avoid changing the layout of any `class`/`struct` exposed across the boundary (adding/reordering member variables) without a version bump — it breaks binary compatibility for already-compiled consumers.
- Prefer exposing a stable, minimal interface at the boundary (see [Part II: C API Style](#part-ii-c-api-style)), and keep the `PascalCase` classes with full modern C++ usage as **internal implementation detail** behind it.
- Mark exported symbols explicitly (e.g. `__declspec(dllexport)` / `__attribute__((visibility("default")))`), and keep everything else hidden by default, to keep the ABI surface intentional and small.
### Runtime Loading (dlopen and LoadLibrary)
- Any function loaded dynamically at runtime **must** be declared with C linkage (`extern "C"`) to avoid name-mangling issues — this is exactly the case [Part II: C API Style](#part-ii-c-api-style) exists for, even in an otherwise all-`.hpp` C++ codebase.
- Keep the loaded interface as a plain function-pointer table or `extern "C"` API — no classes, no exceptions crossing the boundary (an exception thrown from a plugin loaded via `dlopen` and caught in the host app is undefined behavior across incompatible runtimes/compilers).
- Document plugin/module boundaries clearly with `.hpp` interface headers describing the contract, even though the actual crossing point is a plain C-style vtable or function table.
### Summary
| Context | Modern C++ features across the boundary | ABI stability required |
|---|---|---|
| Static link | Freely (templates, `auto`, inline, `constexpr`) | No — whole program rebuilt together |
| Dynamic link (`.so`/`.dll`) | Restrict at the public boundary; free internally | Yes — layout & symbol changes are breaking |
| Runtime load (`dlopen`) | `extern "C"` only at the boundary ([Part II](#part-ii-c-api-style) style) | Yes — plus no exceptions/classes across it |
> **See also:** [Boundary Rule of Thumb](#17-boundary-rule-of-thumb) — decides, file by file, whether a piece of code follows this section or Part II · [Part II: C API Style](#part-ii-c-api-style) — the actual convention referenced throughout this section.
---
## 12. Miscellaneous
- Mark single-argument constructors `explicit` unless implicit conversion is intended.
- Prefer `const` correctness everywhere it applies.
- **Leave a blank line after an early exit** — a `return`, `break`, or `continue` statement, or a block whose last statement is one of these — whenever more code follows at that same level. It visually marks where the early-exit path ends and the normal-case code begins. Skip the blank line if the statement (or its block) is the last thing in its own enclosing function or loop; there's nothing after it to separate from.
- Not enforced by `clang-format` or `clang-tidy` — neither has a check for this. It's a code-review convention.
```cpp
void process(const std::vector<int>& values)
{
if (values.empty())
{
return;
}
for (int value : values)
{
accumulate(value);
}
}
```
---
# Part II: C API Style
This section governs any code written as **plain C**, or any `extern "C"` surface exposed *from* a C++ library for consumption by C callers, dynamic loaders (`dlopen`), or other languages via FFI. Nothing in this section applies to internal C++ implementation — only to the boundary itself and to files that are genuinely compiled as C.
## 13. Naming Conventions (C API)
| Symbol Type | Convention | Example |
|-------------------------|----------------------|----------------------------------------|
| Public API functions | `snake_case` with library prefix | `net_manager_connect(NetManager* mgr);` |
| Types (`struct`/`enum`/typedef) | `PascalCase` with library prefix, `_t` suffix optional | `NetManagerT` or `NetManager` |
| Struct fields | `snake_case` | `int connection_timeout;` |
| Function parameters | `snake_case` | `int retry_count` |
| Constants / `#define` | `ALL_CAPS_SNAKE_CASE` with library prefix | `#define NET_MAX_RETRIES 5` |
| Enum values | `ALL_CAPS_SNAKE_CASE` with library + enum prefix | `NET_STATE_CONNECTED` |
| Internal global variables (translation-unit-visible, mutable) | `g_snake_case` | `int g_active_connection_count;` |
| Static variables (function-local or file-scope `static`) | `s_snake_case` | `static int s_retry_attempts;` |
| File names | `snake_case` | `net_manager.h` / `net_manager.c` |
- Every public symbol (function, type, macro, enum value) is **prefixed with the library/module name** to avoid collisions in the global C symbol namespace, since C has no namespaces.
- `snake_case` is used throughout the C API — this is a deliberate departure from the `camelCase`/`PascalCase` used in Part I, and is one of the clearest visual signals that a symbol belongs to the C boundary rather than internal C++.
- The `g_`/`s_` prefixes are for **internal, non-exported** state — the kind of file-scope `static` variable that never appears in a public header. A genuinely public global exported from the API surface is unusual enough that it should get the full library prefix instead (`net_manager_default_timeout`, not `g_net_manager_default_timeout`) — don't stack both prefixes on the same symbol.
---
## 14. File Extensions and Structure
- **`.h`** for all public C API headers — must be valid to include from both C and C++ translation units.
- **`.c`** for files actually compiled as C. If the "C API" is really a C++ implementation exposing an `extern "C"` shim, the shim itself lives in a `.cpp` file but declares its exported functions in a `.h` header guarded for C++ (see below).
- One `.h` per logical module/library surface (e.g. `net_manager.h`), mirroring the one-header-per-component rule from Part I, but scoped to the *public* API rather than one per internal class.
```c
/* net_manager.h */
#ifndef NET_MANAGER_H
#define NET_MANAGER_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct NetManager NetManager;
NetManager* net_manager_create(void);
void net_manager_destroy(NetManager* mgr);
int net_manager_connect(NetManager* mgr, const char* host, int port);
#ifdef __cplusplus
}
#endif
#endif /* NET_MANAGER_H */
```
- Use traditional `#ifndef`/`#define`/`#endif` header guards in `.h` files rather than `#pragma once` — `.h` files must remain portable to strict C compilers where `#pragma once` support is less universally guaranteed.
- Always wrap declarations in `extern "C" { ... }` guarded by `#ifdef __cplusplus`, so the same header works whether included from a C or a C++ translation unit.
---
## 15. Indentation and Braces
Match Part I for consistency across the codebase: **tabs** for indentation, **Allman** brace placement.
```c
int net_manager_connect(NetManager* mgr, const char* host, int port)
{
if (mgr == NULL)
{
return NET_ERROR_INVALID_ARGUMENT;
}
return connect_impl(mgr, host, port);
}
```
---
## 16. API Design Do's and Don'ts
| Do | Don't |
|----|-------|
| Return an integer error code / status enum from every fallible function | Throw or let a C++ exception cross into `extern "C"` code (undefined behavior for C callers) |
| Use opaque pointers (`typedef struct NetManager NetManager;` with the definition hidden in the `.c`/`.cpp`) to hide implementation details and preserve ABI stability | Expose full `struct` layouts in the public header if the layout may change — this breaks binary compatibility |
| Provide explicit `_create` / `_destroy` (or `_init` / `_free`) pairs for any type owning resources | Rely on constructors/destructors (C has none) or leave lifetime management implicit |
| Take a `void* user_data` parameter on any callback-accepting function | Assume the caller can supply capturing closures — plain C function pointers can't capture state |
| Check all pointer parameters for `NULL` before dereferencing at public entry points | Assume public API callers always pass valid pointers |
| Keep the header C89/C99-compatible (no `//` comments, no variable declarations mid-block if targeting strict C89) unless the project's minimum C standard is confirmed higher | Use C++-only syntax anywhere in a `.h` that must remain includable from C |
| Document ownership transfer explicitly in comments (e.g. "caller owns the returned pointer and must call `net_manager_destroy`") | Leave ownership of returned pointers ambiguous |
---
## 17. Boundary Rule of Thumb
- If a file is `.cpp`/`.hpp` and never crosses a dynamic-library, plugin, or FFI boundary: **use [Part I](#part-i-c-style), full stop.**
- If a file declares or implements symbols meant to be called from C, another language, or loaded via `dlopen`/`LoadLibrary`: **use [Part II](#part-ii-c-api-style)** for that surface, even if the implementation behind it is C++.
- Never mix the two conventions within the same file — a `.h` should never contain `camelCase` C++-flavored declarations, and a `.hpp` should never contain `snake_case` C-flavored ones. Where a C++ class needs a C API, write a thin `extern "C"` shim (Part II style) that internally calls into the real C++ implementation (Part I style).
> **See also:** [Library Linking and Runtime Loading](#11-library-linking-and-runtime-loading) — the dynamic-library and `dlopen` boundaries that make this rule necessary in the first place.
+1
View File
@@ -0,0 +1 @@
1.0.0
+530
View File
@@ -0,0 +1,530 @@
# =============================================================================
# breeze/cmake/Breeze.cmake
#
# The build system itself: what the template contributes to a project, from
# the C++ standard and the output directories to module discovery and the
# install rules. Included by the project's CMakeLists.txt right after
# project(); the project's own settings are in project/Configuration.cmake (read
# here, through CMakeLists.txt having included it). Replacing breeze/ updates
# the template without touching either.
# =============================================================================
cmake_minimum_required(VERSION 3.25)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# This project's settings (PROJECT_CODE_NAME, DEFAULT_BUILD_TYPE, the output paths
# and the bin/lib subdirectory names) come from project/Configuration.cmake --
# already included by CMakeLists.txt, before project(); this is a no-op unless
# something includes this file on its own. The target selection
# (DIST_TARGET/PLATFORM/ARCHITECTURE/COMPILER_ID) is TargetSelection.cmake: it
# needs real compiler detection, which is why it comes after project(), and
# everything below depends on it. Edit project/Configuration.cmake, or pass
# -D<NAME>=... at configure time, rather than hardcoding a value it already owns
# anywhere else in this project, scripts included.
# Where the template is, and where the project that uses it is -- found from this
# file's own location rather than from CMAKE_SOURCE_DIR, which is the outermost
# project's when this one is embedded with add_subdirectory().
set(PMN_BREEZE_CMAKE_DIR "${CMAKE_CURRENT_LIST_DIR}")
get_filename_component(PMN_PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
include("${PMN_PROJECT_ROOT}/project/Configuration.cmake")
# The contract with project/Configuration.cmake: these are the settings the template
# reads. A project file from an older template version may lack one that a newer
# version added -- say which, instead of failing somewhere downstream on an empty value.
foreach(_pmn_setting
PROJECT_CODE_NAME DEFAULT_BUILD_TYPE
OUT_DIR BUILD_OUTPUT_DIR INSTALL_OUTPUT_DIR DOCS_OUTPUT_DIR DIST_OUTPUT_DIR
TOOLCACHE_DIR COMPILE_COMMANDS_DESTINATION
RUNTIME_OUTPUT_SUBDIR LIBRARY_OUTPUT_SUBDIR ARCHIVE_OUTPUT_SUBDIR)
if(NOT DEFINED ${_pmn_setting})
message(FATAL_ERROR
"project/Configuration.cmake does not define ${_pmn_setting}, which this version "
"of breeze/ needs. Add it: a fresh copy of the template's project/Configuration.cmake "
"shows the default.")
endif()
endforeach()
include("${CMAKE_CURRENT_LIST_DIR}/TargetSelection.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/GitignoreSync.cmake")
# The language standard is the project's choice (project/Configuration.cmake);
# requiring it and turning compiler extensions off is the template's policy.
if(NOT DEFINED PROJECT_CXX_STANDARD)
set(PROJECT_CXX_STANDARD 20)
endif()
set(CMAKE_CXX_STANDARD ${PROJECT_CXX_STANDARD})
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 project/Configuration.cmake (RUNTIME_OUTPUT_SUBDIR
# etc.); the base directory is always CMAKE_BINARY_DIR, wherever -B points
# (breeze/scripts/build.sh uses out/build/<preset> -- see project/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 breeze/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 toolchains)
# libwinpthread by default, which then have to ship alongside every
# binary that needs them -- easy to forget, and the exact DLL names
# differ between mingw32 and mingw64 and between exception models (SEH
# on x86_64; DWARF-2 or SJLJ on i686). PMN_MINGW_RUNTIME_DLLS below
# resolves the actual files by asking the compiler rather than
# hardcoding names (-print-file-name=NAME echoes NAME back unchanged if
# it can't find it, which is how "not applicable to this toolchain" is
# detected), and BuildHelpers.cmake's _copy_mingw_runtime_dlls() copies
# them next to every EXECUTABLE/SHARED/test/example target that needs
# them, in the build tree and at install time (the "install" section
# below) -- so nothing here needs a manual step. MINGW is CMake's
# built-in equivalent of this project's own COMPILER_ID being "mingw32"
# or "mingw64".
#
# PMN_MINGW_STATIC_RUNTIME (default OFF) is what this project used to do
# unconditionally: link every EXECUTABLE against a private, static copy
# of libgcc/libstdc++/winpthread instead (-static-libgcc -static-libstdc++
# -static -lwinpthread -- EXECUTABLE targets only, via a generator
# expression, since applying it to a SHARED target collides with
# WINDOWS_EXPORT_ALL_SYMBOLS: a statically-linked unwinder re-exported
# from the DLL's own import library and a second static copy in the EXE
# both trying to define `_Unwind_Resume` is a link error). It trades
# shipping the runtime DLLs for a standalone .exe -- which is safe only
# as long as no EXECUTABLE built this way ALSO links a MinGW SHARED
# module (this project always has one -- core_lib/demo_core.dll) THAT
# PASSES AN STL TYPE ACROSS THAT BOUNDARY BY VALUE. core_lib's
# std::string makeGreeting(...) does exactly that: an executable with
# its own private, statically-linked libstdc++ has its own private
# allocator, separate from the one the dynamically-linked DLL uses
# (libstdc++-6.dll's); a std::string built on the DLL side -- heap
# allocated there once its contents outgrow small-string optimization,
# which "Hello, World! (from demo_core)" does -- and then destroyed on
# the executable's side is freed through a DIFFERENT allocator instance
# than the one that allocated it. That is undefined behavior across any
# Windows DLL boundary (well documented -- e.g.
# github.com/msys2/MINGW-packages/issues/5763, a DLL/EXE pair crashing
# the moment an exception -- which unwinds through the same kind of
# per-runtime-instance bookkeeping a heap allocator uses -- crosses a
# static/dynamic runtime mismatch like this one), and on real Windows
# (confirmed directly: some emulators are considerably more lenient
# about a cross-heap free() than real Windows is, so a mismatch like
# this can look fine there and still be live) it reliably crashes
# shortly after the mismatched value is used -- which is exactly what
# this project's own demos did while PMN_MINGW_STATIC_RUNTIME was the
# only option: hello_exe/farewell_exe printed their (still-valid, not
# yet corrupted) greeting and then crashed destroying the very
# std::string they had just printed. Turning this option ON is safe
# again only for a MinGW build with no SHARED module at all, or where
# every SHARED module's boundary is a plain C API (no STL type crosses
# it by value) -- e.g. also statically-linking that module and giving
# it a C entry point avoids the whole class of problem, since it no
# longer depends on the dynamic runtime at all.
# -------------------------------------------------------
option(PMN_MINGW_STATIC_RUNTIME
"MinGW: statically link libgcc/libstdc++/winpthread into every executable instead of shipping the runtime DLLs next to it. UNSAFE for an executable that also links a MinGW SHARED module (like this project's core_lib) which passes an STL type across that boundary by value -- see the comment above. Default OFF: dynamic linking everywhere, with the runtime DLLs copied automatically."
OFF)
if(MINGW)
if(PMN_MINGW_STATIC_RUNTIME)
add_link_options(
"$<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,EXECUTABLE>:-static-libgcc;-static-libstdc++;-static;-lwinpthread>"
)
endif()
set(PMN_MINGW_RUNTIME_DLLS "")
# -print-file-name=<name>.dll only searches "lib"-shaped paths, where
# the .dll.a import library lives -- not "bin", where the runtime
# .dll itself does on every package manager here except apt: asked
# for the bare .dll on those, it just echoes the name back
# unresolved, silently leaving it out of this list further down.
# Confirmed directly against each package's own file listing:
# pacman -- /usr/<triplet>/bin/<dll> (flat)
# dnf -- /usr/<triplet>/sys-root/mingw/bin/<dll> (nested)
# -- and against discussion.fedoraproject.org's "Location of
# mingw64/mingw32 system libraries like libgcc_s_seh-1.dll and
# libwinpthread-1.dll" (Oct 2024) for why dnf's -print-file-name
# specifically comes up empty. On apt, -print-file-name already
# resolves the .dll directly (confirmed independently in that same
# thread) -- both fallbacks below only run where that lookup came
# back empty, so apt is unaffected either way.
get_filename_component(_pmn_mingw_cxx_name "${CMAKE_CXX_COMPILER}" NAME)
string(REGEX REPLACE "-g\\+\\+(\\.exe)?$" "" _pmn_mingw_triplet "${_pmn_mingw_cxx_name}")
execute_process(
COMMAND "${CMAKE_CXX_COMPILER}" -print-sysroot
OUTPUT_VARIABLE _pmn_mingw_sysroot
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
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(NOT EXISTS "${_pmn_resolved_dll}")
if(_pmn_mingw_triplet AND EXISTS "/usr/${_pmn_mingw_triplet}/bin/${_pmn_dll_candidate}")
set(_pmn_resolved_dll "/usr/${_pmn_mingw_triplet}/bin/${_pmn_dll_candidate}")
elseif(_pmn_mingw_sysroot AND EXISTS "${_pmn_mingw_sysroot}/mingw/bin/${_pmn_dll_candidate}")
set(_pmn_resolved_dll "${_pmn_mingw_sysroot}/mingw/bin/${_pmn_dll_candidate}")
endif()
endif()
if(EXISTS "${_pmn_resolved_dll}")
list(APPEND PMN_MINGW_RUNTIME_DLLS "${_pmn_resolved_dll}")
endif()
endforeach()
if(PMN_MINGW_RUNTIME_DLLS)
if(PMN_MINGW_STATIC_RUNTIME)
message(STATUS "MinGW runtime DLLs (bundled next to SHARED targets only -- PMN_MINGW_STATIC_RUNTIME is ON): ${PMN_MINGW_RUNTIME_DLLS}")
else()
message(STATUS "MinGW runtime DLLs (bundled next to every EXECUTABLE/SHARED target that needs them): ${PMN_MINGW_RUNTIME_DLLS}")
endif()
endif()
endif()
# -------------------------------------------------------
# Build machinery (macros/functions) -- see breeze/cmake/BuildHelpers.cmake.
# This file should change rarely; it holds nothing project-specific.
# -------------------------------------------------------
include("${PMN_BREEZE_CMAKE_DIR}/BuildHelpers.cmake")
# -------------------------------------------------------
# Third-party dependencies -- see breeze/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("${PMN_BREEZE_CMAKE_DIR}/Dependencies.cmake")
# -------------------------------------------------------
# Documentation -- see breeze/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("${PMN_BREEZE_CMAKE_DIR}/Documentation.cmake")
# -------------------------------------------------------
# Module discovery
#
# Every module lives in its own directory with its own CMakeLists.txt --
# usually a single add_lib_module/add_objects_module/add_exe_module/
# add_test_module/add_example_module(...) call (see breeze/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, objects 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")
# Object modules (add_objects_module) can only be told from libraries once every
# module exists: this links what each one needs of them -- the pieces behind
# the pieces, and their headers for a library's consumers. See its comment.
_resolve_object_modules()
# Emits <build-dir>/module_manifest-<CONFIG>.txt ("target_name=path_to_binary"
# per line) for every executable module discovered above. breeze/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 project/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 breeze/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
# breeze/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). breeze/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 runtime DLLs (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. Always into RUNTIME_OUT_DIR: the
# primary executables land there, and so does core_lib/demo_core.dll,
# which needs this regardless of PMN_MINGW_STATIC_RUNTIME (see that
# option's comment -- a SHARED module is never a candidate for 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}
)
# Matches BuildHelpers.cmake's add_test_module: skipped under
# PMN_MINGW_STATIC_RUNTIME, where test executables are
# self-contained instead.
if(PMN_MINGW_RUNTIME_DLLS AND NOT PMN_MINGW_STATIC_RUNTIME)
install(FILES ${PMN_MINGW_RUNTIME_DLLS}
COMPONENT ${PROJECT_CODE_NAME}
DESTINATION ${TEST_RUNTIME_OUT_DIR}
)
endif()
# The install-time match for _copy_target_runtime_dlls(): a test
# that depends on a SHARED module (core_lib included) needs that
# module's .dll installed alongside it too, on any Windows
# compiler -- bin/tests/ is a separate install destination from
# bin/, and Windows has no relative-RPATH equivalent (see that
# function's comment). install(FILES) silently installs nothing
# for a target with no SHARED dependency -- confirmed directly --
# so this needs no separate "is there anything to install" guard.
if(WIN32)
foreach(_pmn_test_target ${_pmn_test_targets})
install(FILES "$<TARGET_RUNTIME_DLLS:${_pmn_test_target}>"
COMPONENT ${PROJECT_CODE_NAME}
DESTINATION ${TEST_RUNTIME_OUT_DIR}
)
endforeach()
endif()
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}
)
# See the matching pair of comments in the INSTALL_TESTS block above.
if(PMN_MINGW_RUNTIME_DLLS AND NOT PMN_MINGW_STATIC_RUNTIME)
install(FILES ${PMN_MINGW_RUNTIME_DLLS}
COMPONENT ${PROJECT_CODE_NAME}
DESTINATION ${EXAMPLE_RUNTIME_OUT_DIR}
)
endif()
if(WIN32)
foreach(_pmn_example_target ${_pmn_example_targets})
install(FILES "$<TARGET_RUNTIME_DLLS:${_pmn_example_target}>"
COMPONENT ${PROJECT_CODE_NAME}
DESTINATION ${EXAMPLE_RUNTIME_OUT_DIR}
)
endforeach()
endif()
else()
message(WARNING "INSTALL_EXAMPLES is ON but no example modules were found under source/examples/.")
endif()
endif()
+125
View File
@@ -0,0 +1,125 @@
# =============================================================================
# BuildDir.cmake
#
# Script-mode bridge, the same idea as PrintConfig.cmake: tells
# breeze/scripts/*.sh and breeze/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 project/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 breeze/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.
# =============================================================================
get_filename_component(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
include("${CMAKE_SOURCE_DIR}/project/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 breeze/scripts/build to create one build directory per preset.")
else()
set(_pmn_value "No build found under ${_pmn_root_shown}. Run breeze/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}")
+627
View File
@@ -0,0 +1,627 @@
# =============================================================================
# BuildHelpers.cmake
#
# Reusable, project-agnostic build machinery: module discovery, per-target
# compiler/RPATH settings, and the module-registration macros (add_lib_module,
# add_objects_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 Breeze.cmake --
# see the "Module discovery" section of Breeze.cmake and the per-module
# CMakeLists.txt convention described there.
#
# Expects the including file (Breeze.cmake) to have already set (from project()
# and project/Configuration.cmake): 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]):
#
# Breeze.cmake 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()
# -----------------------------------------------------------------------
# _copy_mingw_runtime_dlls(TARGET): copies PMN_MINGW_RUNTIME_DLLS (computed
# once in Breeze.cmake's "MinGW runtime linking" section) next to
# TARGET's own built binary, in the build tree, so it runs without a
# manual step. install()-time placement is the matching
# install(FILES ...) in Breeze.cmake's "install" section. A no-op
# whenever PMN_MINGW_RUNTIME_DLLS is empty (every non-MinGW platform, or
# a MinGW toolchain where none of the candidate DLLs were found).
#
# Called for every SHARED module unconditionally (source/Core's
# core_lib/demo_core.dll always uses the dynamic runtime -- see the
# "MinGW runtime linking" comment for why making it static too isn't an
# option), and for EXECUTABLE/test/example targets only when
# PMN_MINGW_STATIC_RUNTIME is OFF (the default): with it ON those are
# statically linked and don't need these DLLs, so the caller skips this
# call entirely rather than copying files nothing imports.
# -----------------------------------------------------------------------
function(_copy_mingw_runtime_dlls TARGET)
if(MINGW AND PMN_MINGW_RUNTIME_DLLS)
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PMN_MINGW_RUNTIME_DLLS} "$<TARGET_FILE_DIR:${TARGET}>"
COMMENT "Copying MinGW runtime DLLs next to ${TARGET}"
VERBATIM
)
endif()
endfunction()
# -----------------------------------------------------------------------
# _copy_target_runtime_dlls(TARGET): on Windows (any compiler -- MSVC,
# clang-cl, or MinGW), copies every SHARED-library dependency's own .dll
# next to TARGET's binary, via CMake's TARGET_RUNTIME_DLLS generator
# expression. This is the OTHER half of running on Windows that
# _copy_mingw_runtime_dlls doesn't cover: that one only bundles the
# compiler's OWN runtime (libgcc/libstdc++/winpthread); this one bundles
# this project's SHARED modules (core_lib/demo_core.dll, and anything
# else a module's own CMakeLists.txt declares SHARED).
#
# Needed specifically for add_test_module/add_example_module: they land
# one directory below the primary modules (bin/tests, bin/examples),
# with an RPATH of "$ORIGIN/.." for the ELF/Mach-O builds where that
# works (see _apply_rpath) -- but Windows has no equivalent of a
# *relative*, directory-based DLL search path, so a test or example that
# depends on a SHARED module built by this project needs that DLL
# copied into ITS OWN directory to run at all, not just the primary
# one. (The primary modules -- add_exe_module -- don't need this: they
# already land in the SAME flat bin/ directory as every SHARED module,
# so they already find it there.) Confirmed directly by cross-building
# for windows-x64-mingw64 and running the result under Wine: a test
# depending on core_lib fails to start (STATUS_DLL_NOT_FOUND,
# "libdemo_core.dll ... not found") without this.
#
# Delegates the actual copy to CopyRuntimeDlls.cmake rather than calling
# `cmake -E copy_if_different` directly, because that fails the whole
# build the moment TARGET_RUNTIME_DLLS is empty -- the common case for
# any test/example that doesn't depend on a SHARED module -- and
# `cmake -E copy_if_different <dest-only, no sources>` is exactly that:
# zero sources. See that script for the confirmed failure and the
# one-line `if(FILES)` fix.
# -----------------------------------------------------------------------
function(_copy_target_runtime_dlls TARGET)
if(WIN32)
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DDEST=$<TARGET_FILE_DIR:${TARGET}>
"-DFILES=$<TARGET_RUNTIME_DLLS:${TARGET}>"
-P "${PMN_BREEZE_CMAKE_DIR}/CopyRuntimeDlls.cmake"
COMMENT "Copying ${TARGET}'s own SHARED-library dependencies next to it"
VERBATIM
)
endif()
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
# Breeze.cmake'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.
#
# breeze/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 breeze/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})
# Every module and what it DEPENDS on, for _resolve_object_modules(): object
# modules can't be told apart from libraries here -- the module they name
# may not have been discovered yet -- so that is decided once all exist.
set_property(GLOBAL APPEND PROPERTY PMN_ALL_MODULES ${TARGET})
set_property(GLOBAL PROPERTY PMN_DEPENDS_${TARGET} "${FIN_DEPENDS}")
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})
# An objects module has no file to name: for it OUTPUT_NAME is only the name
# MODULE_NAME carries (the library it is a piece of, usually).
get_target_property(_pmn_target_type ${TARGET} TYPE)
if(FIN_OUTPUT_NAME AND NOT _pmn_target_type STREQUAL "OBJECT_LIBRARY")
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 Breeze.cmake'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)
# A SHARED module always keeps the dynamic MinGW runtime -- see
# Breeze.cmake's "MinGW runtime linking" comment for why it's
# never a candidate for PMN_MINGW_STATIC_RUNTIME -- so this call
# is unconditional, unlike the EXECUTABLE-producing macros below.
_copy_mingw_runtime_dlls(${NAME})
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_objects_module(NAME [OUTPUT_NAME name] [DEPENDS ...] [EXTRA_SOURCES ...] [NO_DOCS])
#
# Called from an objects module's own CMakeLists.txt (e.g. source/CoreMath/CMakeLists.txt).
# An OBJECT library: its sources are compiled, and that is all -- no archive, no
# DLL, nothing in bin/ or lib/. Name it in the DEPENDS of a library, an
# executable, a test, an example, or another objects module, and its object
# files become part of that target. That is what makes it the way to split
# one big library into pieces: each piece is a module of its own (own
# directory, own include/, built and tested on its own), and the library
# that is left is one line naming them.
#
# source/CoreMath/CMakeLists.txt add_objects_module(core_math)
# source/CoreIo/CMakeLists.txt add_objects_module(core_io DEPENDS core_math)
# source/Core/CMakeLists.txt add_lib_module(core DEPENDS core_io)
#
# Sources/headers are globbed like a library's: src/*.cpp and include/*.h(pp)
# under the module's own directory. include/ is public: whoever takes the
# objects sees the headers -- and for a library, so do ITS consumers (see
# _resolve_object_modules below), because the headers of the pieces are the
# library's headers. Keep a piece's private headers in src/.
#
# OUTPUT_NAME names nothing here -- there is no file -- but is what the
# MODULE_NAME definition carries: pass the library's name so the pieces
# of one library all identify as it.
#
# Not installed, not run, not in the module manifest: the objects live on
# inside whatever took them. Always compiled position-independent, since
# they may land in a shared library.
# =============================================================================
macro(add_objects_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 src/*.cpp)
file(GLOB_RECURSE ${NAME}_HEADERS CONFIGURE_DEPENDS include/*.h include/*.hpp)
add_library(${NAME} OBJECT ${${NAME}_SOURCES} ${${NAME}_HEADERS} ${ARG_EXTRA_SOURCES})
target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
# Its own category: install() (Breeze.cmake) only takes PRIMARY, and an
# OBJECT library has nothing of its own to install.
_register_module_target(OBJECTS ${NAME})
_finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN})
endmacro()
# -----------------------------------------------------------------------
# _resolve_object_modules(): call once, after every module has been
# discovered (Breeze.cmake does).
#
# CMake links an OBJECT library's objects into the target that names it,
# exactly once -- and no further. A piece that DEPENDS on another piece does
# not carry that piece's objects along, so a library naming only the first
# one fails at link time with undefined references. And a library that has
# taken a piece's objects does not pass the piece's headers on to its own
# consumers, unless it links the piece PUBLIC.
#
# So, for every module target, the objects modules it names -- directly, or
# through other objects modules -- are collected, and each is linked:
#
# * library (static/shared) or objects module: PUBLIC. Objects go into it;
# its consumers see the pieces' include/ and compile definitions, and
# get no second copy of the objects -- they are inside it already.
# * executable (app, test, example): PRIVATE. Nothing consumes it.
#
# Only objects modules are followed. A module that is an ordinary library
# keeps whatever it has inside it; what a piece links (a static library, say)
# is reached by CMake on its own at the final link.
#
# Done here and not in the macros because the macros can't know what a
# name in DEPENDS is: modules are discovered in directory order, and the
# one named may not exist yet.
# -----------------------------------------------------------------------
function(_resolve_object_modules)
get_property(_pmn_all_modules GLOBAL PROPERTY PMN_ALL_MODULES)
foreach(_pmn_module IN LISTS _pmn_all_modules)
get_target_property(_pmn_module_type ${_pmn_module} TYPE)
get_property(_pmn_direct GLOBAL PROPERTY PMN_DEPENDS_${_pmn_module})
set(_pmn_queue "${_pmn_direct}")
set(_pmn_seen "")
set(_pmn_pieces "")
while(NOT "${_pmn_queue}" STREQUAL "")
list(POP_FRONT _pmn_queue _pmn_name)
if(_pmn_name IN_LIST _pmn_seen)
continue()
endif()
list(APPEND _pmn_seen ${_pmn_name})
if(NOT TARGET ${_pmn_name})
continue()
endif()
get_target_property(_pmn_name_type ${_pmn_name} TYPE)
if(NOT _pmn_name_type STREQUAL "OBJECT_LIBRARY")
continue()
endif()
if(_pmn_name STREQUAL _pmn_module)
message(FATAL_ERROR
"The objects module '${_pmn_module}' depends on itself, through the "
"objects modules it names (directly or not) in DEPENDS.")
endif()
list(APPEND _pmn_pieces ${_pmn_name})
get_property(_pmn_more GLOBAL PROPERTY PMN_DEPENDS_${_pmn_name})
list(APPEND _pmn_queue ${_pmn_more})
endwhile()
foreach(_pmn_piece IN LISTS _pmn_pieces)
if(_pmn_module_type STREQUAL "EXECUTABLE")
# Already linked PRIVATE when named directly (_finalize_module_target).
if(NOT _pmn_piece IN_LIST _pmn_direct)
target_link_libraries(${_pmn_module} PRIVATE ${_pmn_piece})
endif()
else()
target_link_libraries(${_pmn_module} PUBLIC ${_pmn_piece})
endif()
endforeach()
endforeach()
endfunction()
# =============================================================================
# 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})
# Skipped when PMN_MINGW_STATIC_RUNTIME is ON: that link option (set in
# Breeze.cmake) makes this target self-contained instead, and these
# DLLs would be unused files sitting next to it. See that option's own
# comment for why OFF (dynamic, copied here) is the safe default.
if(NOT PMN_MINGW_STATIC_RUNTIME)
_copy_mingw_runtime_dlls(${NAME})
endif()
_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})
# See add_exe_module's identical guard on the first call: skipped
# under PMN_MINGW_STATIC_RUNTIME, which makes this target
# self-contained instead. The second call is unconditional and
# WIN32-wide (not just MinGW): bin/tests/ is a different directory
# from bin/, so any SHARED module this test depends on (this
# project's core_lib included) needs its own .dll copied here too --
# see _copy_target_runtime_dlls's own comment for why a *different*
# mechanism than the one above is needed for that.
if(NOT PMN_MINGW_STATIC_RUNTIME)
_copy_mingw_runtime_dlls(${NAME})
endif()
_copy_target_runtime_dlls(${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})
# See add_test_module's identical pair of calls and comment just above.
if(NOT PMN_MINGW_STATIC_RUNTIME)
_copy_mingw_runtime_dlls(${NAME})
endif()
_copy_target_runtime_dlls(${NAME})
_finalize_module_target(${NAME} ${_MODULE_NAME} ${ARGN})
add_dependencies(all_examples ${NAME})
endmacro()
+21
View File
@@ -0,0 +1,21 @@
# =============================================================================
# CopyRuntimeDlls.cmake
#
# Usage (from a POST_BUILD add_custom_command; see BuildHelpers.cmake's
# _copy_target_runtime_dlls()):
# cmake -DDEST=<dir> "-DFILES=$<TARGET_RUNTIME_DLLS:tgt>" -P CopyRuntimeDlls.cmake
#
# Exists only because `cmake -E copy_if_different <dest>` -- the obvious,
# one-line way to do this -- errors out (and fails the whole build) when
# given zero source files, which $<TARGET_RUNTIME_DLLS:tgt> legitimately
# evaluates to for most targets (anything that links no SHARED library,
# which is most executables in most projects, this one's included: only
# a target that depends on a SHARED module -- like core_lib -- has any
# files here at all). Confirmed directly: without this indirection, that
# empty case stops the build with cmake -E's own usage text as the error.
# A plain `if(FILES)` here is all it takes to make "nothing to do" a
# success instead of a build failure.
# =============================================================================
if(FILES)
file(COPY ${FILES} DESTINATION "${DEST}")
endif()
+159
View File
@@ -0,0 +1,159 @@
# =============================================================================
# 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/etc. 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 breeze/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_objects_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.
#
# The targets it makes are usable from EVERY module, wherever the call is
# made and whichever module CMake reaches first -- one module can declare
# fmt and another just DEPENDS on fmt::fmt. A copy built from source gets
# that for free (its targets are global). An installed copy found through
# FIND_PACKAGE_ARGS does not: find_package() defines imported targets in the
# directory that called it and below, so a sibling module's
# target_link_libraries(... fmt::fmt) failed with "target was not found" --
# only on machines that happen to have the package installed. Hence
# CMAKE_FIND_PACKAGE_TARGETS_GLOBAL, for the duration of FetchContent_
# MakeAvailable -- not of FetchContent_Declare, which reads the variable too
# and, in CMake 3.24 to 3.28 at least, builds a broken find_package() call
# from it ("Argument not separated from preceding token by whitespace").
# -----------------------------------------------------------------------
macro(add_dependency NAME)
include(FetchContent)
FetchContent_Declare(${NAME} SYSTEM ${ARGN})
if(DEFINED CMAKE_FIND_PACKAGE_TARGETS_GLOBAL)
set(_pmn_prev_targets_global "${CMAKE_FIND_PACKAGE_TARGETS_GLOBAL}")
else()
unset(_pmn_prev_targets_global)
endif()
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
FetchContent_MakeAvailable(${NAME})
if(DEFINED _pmn_prev_targets_global)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL "${_pmn_prev_targets_global}")
else()
unset(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL)
endif()
unset(_pmn_prev_targets_global)
string(TOLOWER "${NAME}" _pmn_lc_name)
if(DEFINED ${_pmn_lc_name}_SOURCE_DIR)
_exclude_from_compile_commands("${${_pmn_lc_name}_SOURCE_DIR}")
endif()
endmacro()
+190
View File
@@ -0,0 +1,190 @@
# =============================================================================
# Documentation.cmake
#
# Automatic, per-module Doxygen documentation, using a pinned Doxygen
# version -- never your system's doxygen, so every machine gets identical
# output. Zero setup required at the CMakeLists.txt level: every module
# registered via add_lib_module/add_objects_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 (breeze/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 breeze/cmake/DoxygenPin.cmake for where) -- it never downloads
# anything itself, so a plain `breeze/scripts/build.sh` never touches Doxygen or
# needs network access. Run `breeze/scripts/setup.sh` / `setup.bat` once (which
# runs breeze/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 breeze/scripts/docs.sh / docs.bat, which builds `docs` and prints the path)
# Paths above use the defaults from project/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 breeze/cmake/DoxygenPin.cmake to match
# that release's assets, and re-run breeze/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 breeze/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 "
"breeze/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
# breeze/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 breeze/scripts/setup.sh (or setup.bat) once to fetch the pinned Doxygen, then reconfigure."
VERBATIM
)
endif()
# -----------------------------------------------------------------------
# _add_module_docs(TARGET): called automatically from _finalize_module_target
# for every module -- creates docs_<TARGET> (a Doxygen run scoped to that
# module's own directory) and wires it into the aggregate `docs` target.
# A no-op if ENABLE_DOXYGEN is off. Excluded from the default build same
# as tests/examples -- documentation is opt-in per build, not generated
# on every compile.
#
# If the module's own directory has a README.md, it's used as that
# module's Doxygen mainpage automatically.
# -----------------------------------------------------------------------
function(_add_module_docs TARGET)
if(NOT ENABLE_DOXYGEN)
return()
endif()
set(DOXYGEN_OUTPUT_DIRECTORY "${DOCS_OUTPUT_DIR}/${TARGET}")
set(DOXYGEN_PROJECT_NAME "${TARGET}")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
else()
unset(DOXYGEN_USE_MDFILE_AS_MAINPAGE)
endif()
doxygen_add_docs(docs_${TARGET}
"${CMAKE_CURRENT_SOURCE_DIR}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating documentation for module '${TARGET}'"
)
set_target_properties(docs_${TARGET} PROPERTIES EXCLUDE_FROM_ALL TRUE)
add_dependencies(docs docs_${TARGET})
set_property(GLOBAL APPEND PROPERTY PMN_DOC_MODULES ${TARGET})
endfunction()
# -----------------------------------------------------------------------
# _write_docs_index(OUTPUT_DIR): call once, after every module has been
# discovered, to write a plain landing page at OUTPUT_DIR/index.html
# linking to each module's own generated docs -- so "where do I start
# reading" has one obvious answer regardless of how many modules exist.
# -----------------------------------------------------------------------
function(_write_docs_index OUTPUT_DIR)
if(NOT ENABLE_DOXYGEN)
return()
endif()
file(MAKE_DIRECTORY "${OUTPUT_DIR}")
get_property(_pmn_doc_modules GLOBAL PROPERTY PMN_DOC_MODULES)
list(SORT _pmn_doc_modules)
set(_pmn_doc_links "")
foreach(_pmn_mod ${_pmn_doc_modules})
string(APPEND _pmn_doc_links " <li><a href=\"${_pmn_mod}/html/index.html\">${_pmn_mod}</a></li>\n")
endforeach()
file(WRITE "${OUTPUT_DIR}/index.html" "<!DOCTYPE html>
<html>
<head><meta charset=\"utf-8\"><title>${PROJECT_NAME} -- module documentation</title></head>
<body>
<h1>${PROJECT_NAME} module documentation</h1>
<ul>
${_pmn_doc_links} </ul>
</body>
</html>
")
endfunction()
+98
View File
@@ -0,0 +1,98 @@
# =============================================================================
# DoxygenPin.cmake
#
# Pure computation of "where does the pinned Doxygen live, and where would
# it be downloaded from" -- no network calls, no download, no extraction.
# Shared by two very different callers, which is why this is its own file:
#
# breeze/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.
#
# breeze/cmake/FetchDoxygen.cmake include()s this when run standalone via
# `cmake -P breeze/cmake/FetchDoxygen.cmake` (what
# breeze/scripts/setup.sh / setup.bat call) to know
# what to actually download.
#
# Needs TOOLCACHE_DIR to already be set -- Documentation.cmake gets it from
# project/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()
+110
View File
@@ -0,0 +1,110 @@
# =============================================================================
# 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 breeze/cmake/FetchDoxygen.cmake
#
# This is what breeze/scripts/setup.sh / setup.bat call. It's deliberately not part
# of the main `cmake --preset <name>` configure -- see breeze/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 project/Configuration.cmake, which isn't
# loaded in this standalone script-mode invocation -- read it from there
# directly (it holds settings only, so including it has no 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 breeze/cmake/FetchDoxygen.cmake
if(NOT DEFINED TOOLCACHE_DIR)
get_filename_component(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
include("${CMAKE_SOURCE_DIR}/project/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 "
"breeze/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 `breeze/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 breeze/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()
+108
View File
@@ -0,0 +1,108 @@
# =============================================================================
# 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 breeze/cmake/FetchNinja.cmake
#
# This is what breeze/scripts/setup.sh / setup.bat call, and also what breeze/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 project/Configuration.cmake, which isn't
# loaded in this standalone script-mode invocation -- read it from there
# directly (it holds settings only, so including it has no 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 breeze/cmake/FetchNinja.cmake
if(NOT DEFINED TOOLCACHE_DIR)
get_filename_component(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
include("${CMAKE_SOURCE_DIR}/project/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 breeze/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 -- breeze/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 breeze/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()
+114
View File
@@ -0,0 +1,114 @@
# =============================================================================
# .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.
# =============================================================================
include_guard(GLOBAL)
# out/dist/ is reserved (see the output layout in project/Configuration.cmake) and
# nothing writes to it yet, so it is created eagerly here -- otherwise it simply
# would not exist until something populates it, which could easily read as "this
# was never wired up" rather than "intentionally reserved for later". A file
# system side effect, so it lives here and not in the settings file: a script's
# query of those settings runs far more often than a real configure.
file(MAKE_DIRECTORY "${DIST_OUTPUT_DIR}")
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()
# breeze/scripts/run.sh keeps the wine prefixes it creates in .cache/wine/ -- a
# fixed place, not one of the path variables above. Listed here rather
# than added to .gitignore by hand: this block is rewritten on every
# configure, which silently deleted a hand-added line.
list(APPEND _pmn_gitignore_entries ".cache/wine/")
# Likewise the target sysroots breeze/cmake/toolchains/_common.cmake looks for
# in .cache/sysroots/linux-<arch> (see pmn_find_sysroot there): root
# filesystems for cross builds, hundreds of megabytes, never to commit.
list(APPEND _pmn_gitignore_entries ".cache/sysroots/")
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()
+155
View File
@@ -0,0 +1,155 @@
# =============================================================================
# HostArch.cmake
#
# One place to turn this project's own architecture labels -- x64, arm64,
# or arm32 (empty if unrecognized) -- out of a processor string. Two entry
# points, because "which architecture" has two different questions behind it:
#
# pmn_host_arch() the machine CMake is actually running ON.
# NinjaPin.cmake / DoxygenPin.cmake use it to decide
# whether a pinned-tool download exists for the machine
# running CMake -- always a HOST question, independent
# of what's being built. TargetSelection.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. TargetSelection.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 (breeze/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 breeze/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
# TargetSelection.cmake (CMake issue #25466: gitlab.kitware.com/cmake/cmake/-/issues/25466),
# just via WoW64 process redirection rather than a cross-targeting
# compiler -- confirmed the hard way, as exactly what broke the pinned-
# ninja/-doxygen lookup below on an entirely ordinary 64-bit machine with a
# 32-bit cmake.exe on PATH. PROCESSOR_ARCHITEW6432 is what Windows itself
# sets to expose the TRUE architecture to a WoW64-redirected 32-bit
# process; it's simply unset outside WoW64, which is exactly when
# PROCESSOR_ARCHITECTURE was already correct anyway -- so preferring it
# when present, falling back to PROCESSOR_ARCHITECTURE otherwise, is
# correct either way.
# =============================================================================
include_guard(GLOBAL)
# _pmn_arch_label(<processor-string> <out-var>)
#
# The one place the aarch64/x86_64/armv7l/... spellings are mapped to this
# project's x64/arm64/arm32 labels. <out-var> gets one of those, or "" if
# unrecognized.
function(_pmn_arch_label PROCESSOR OUT_VAR)
if(PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(${OUT_VAR} "arm64" PARENT_SCOPE)
elseif(PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
set(${OUT_VAR} "x64" PARENT_SCOPE)
elseif(PROCESSOR MATCHES "^[Aa][Rr][Mm]")
# Every other spelling of 32-bit ARM this project has seen: armv6l
# (Raspberry Pi 1/Zero), armv7l (the common case -- Raspberry Pi
# 2/3/4 32-bit OS, generic armhf), armv8l (a 32-bit personality/
# chroot on 64-bit ARM hardware), or bare "arm"/"ARM". Checked
# after arm64/aarch64 above since those also start with "arm".
set(${OUT_VAR} "arm32" PARENT_SCOPE)
else()
set(${OUT_VAR} "" PARENT_SCOPE)
endif()
endfunction()
# pmn_host_arch(<out-var> [<raw-out-var>])
#
# <out-var> gets one of x64/arm64/arm32/"" (unrecognized) for the machine
# running CMake. The optional <raw-out-var> gets whatever raw string was
# actually checked to produce that -- callers building an error message for
# the "" case should use this rather than assuming which underlying
# variable/environment variable was consulted, since that differs by
# platform (see above).
function(pmn_host_arch OUT_VAR)
if(CMAKE_HOST_WIN32)
if(NOT "$ENV{PROCESSOR_ARCHITEW6432}" STREQUAL "")
set(_pmn_ha_processor "$ENV{PROCESSOR_ARCHITEW6432}")
else()
set(_pmn_ha_processor "$ENV{PROCESSOR_ARCHITECTURE}")
endif()
elseif(CMAKE_HOST_SYSTEM_PROCESSOR)
set(_pmn_ha_processor "${CMAKE_HOST_SYSTEM_PROCESSOR}")
elseif(CMAKE_HOST_UNIX)
# Script mode (cmake -P): not defined -- see the header. This is
# exactly what CMake itself does to fill it in during a configure.
execute_process(COMMAND uname -m
OUTPUT_VARIABLE _pmn_ha_processor
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
else()
set(_pmn_ha_processor "${CMAKE_SYSTEM_PROCESSOR}")
endif()
# macOS: a process running under Rosetta 2 -- an x86_64 cmake on an Apple
# Silicon Mac, entirely ordinary since Intel builds of CMake and of most
# developer tools still circulate -- is told it's on an x86_64 machine,
# by `uname -m` and so by CMAKE_HOST_SYSTEM_PROCESSOR alike. The same
# class of trap as a 32-bit process under WoW64 on Windows (see the
# header), and fixed the same way: ask about the MACHINE.
# hw.optional.arm64 is 1 on any Apple Silicon Mac no matter whether the
# asking process is translated, and doesn't exist on Intel (-i makes a
# missing OID a silent empty answer instead of an error).
if(CMAKE_HOST_APPLE)
_pmn_arch_label("${_pmn_ha_processor}" _pmn_ha_probe)
if(_pmn_ha_probe STREQUAL "x64")
execute_process(COMMAND sysctl -in hw.optional.arm64
OUTPUT_VARIABLE _pmn_ha_arm64
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(_pmn_ha_arm64 STREQUAL "1")
set(_pmn_ha_processor "arm64")
endif()
endif()
endif()
if(ARGC GREATER 1)
set(${ARGV1} "${_pmn_ha_processor}" PARENT_SCOPE)
endif()
_pmn_arch_label("${_pmn_ha_processor}" _pmn_ha_label)
set(${OUT_VAR} "${_pmn_ha_label}" PARENT_SCOPE)
endfunction()
# pmn_target_arch(<out-var> [<raw-out-var>])
#
# Same contract as pmn_host_arch(), but for the machine the build is FOR:
# CMAKE_SYSTEM_PROCESSOR. Only meaningful where that variable can be
# trusted -- Linux, where it is `uname -m` for a native build and whatever
# the toolchain file says for a cross build. Not for MSVC/clang-cl, where
# it reflects the cmake.exe process instead (see the header).
function(pmn_target_arch OUT_VAR)
if(ARGC GREATER 1)
set(${ARGV1} "${CMAKE_SYSTEM_PROCESSOR}" PARENT_SCOPE)
endif()
_pmn_arch_label("${CMAKE_SYSTEM_PROCESSOR}" _pmn_ta_label)
set(${OUT_VAR} "${_pmn_ta_label}" PARENT_SCOPE)
endfunction()
+109
View File
@@ -0,0 +1,109 @@
# =============================================================================
# NinjaPin.cmake
#
# Pure computation of "where does the pinned ninja live, and where would it
# be downloaded from" -- no network calls, no download, no extraction.
# Mirrors DoxygenPin.cmake exactly; see that file for the full rationale.
# Shared by two very different callers:
#
# breeze/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.
#
# breeze/cmake/FetchNinja.cmake include()s this when run standalone via
# `cmake -P breeze/cmake/FetchNinja.cmake` (what
# breeze/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 project/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 / breeze/scripts/setup.sh give a clear error, and
# breeze/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()
+53
View File
@@ -0,0 +1,53 @@
# =============================================================================
# PrintConfig.cmake
#
# Script-mode bridge: lets breeze/scripts/*.sh and breeze/scripts/*.bat read values out of
# project/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: breeze/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 breeze/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
# breeze/scripts/build.sh's _pmn_read_config() or breeze/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.
#
# Includes project/Configuration.cmake, which holds settings only: nothing in
# it needs a compiler and it touches nothing on disk, which is what makes it
# safe here -- a value lookup shouldn't have side effects, and this runs far
# more often than a real configure. What does need a real compiler
# (DIST_TARGET/PLATFORM/ARCHITECTURE/COMPILER_ID, which a plain `cmake -P`
# script would get wrong: project(... LANGUAGES CXX) never runs) is
# breeze/cmake/TargetSelection.cmake, not included here. So only ask for names
# the settings file defines -- PROJECT_CODE_NAME, DEFAULT_BUILD_TYPE, the path
# variables; see that file's header for the rules on adding to it.
# =============================================================================
if(NOT DEFINED PRINT_VARS)
message(FATAL_ERROR "Usage: cmake \"-DPRINT_VARS=NAME1;NAME2;...\" -P breeze/cmake/PrintConfig.cmake")
endif()
get_filename_component(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
include("${CMAKE_SOURCE_DIR}/project/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 "
"project/Configuration.cmake -- see that file's header "
"('How scripts read this file').")
endif()
message(STATUS "${_pmn_print_name}=${${_pmn_print_name}}")
endforeach()
+610
View File
@@ -0,0 +1,610 @@
# =============================================================================
# TargetSelection.cmake
#
# What the build is FOR: DIST_TARGET -- a single "<platform>-<arch>-<compiler>"
# string -- and the PLATFORM, ARCHITECTURE and COMPILER_ID derived from it.
# Included by breeze/cmake/Breeze.cmake right after project(), since it needs
# real compiler detection; the project's own settings (project/Configuration.cmake)
# deliberately have none of it, so that a plain `cmake -P` script can read them.
#
# Everything below is the template's: nothing in it is configuration. To build
# for a different target, pick a CMakePresets.json preset or pass -DDIST_TARGET.
#
# -----------------------------------------------------------------------
# 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 (breeze/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 breeze/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: breeze/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. breeze/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. TargetSelection.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=breeze/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; Breeze.cmake and the other breeze/cmake/*.cmake modules consume these
# rather than re-deriving them.
# =============================================================================
include_guard(GLOBAL)
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
# (breeze/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}/breeze/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_CURRENT_LIST_DIR}/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. breeze\\scripts\\build.bat enters the right environment on "
"its own, but the build directory still has to go.\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 (breeze/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=breeze/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}/breeze/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), or build with breeze\\scripts\\build.bat, which does that for you.")
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, or build with breeze\\scripts\\build.bat, which puts that toolchain first on PATH.")
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})")
+850
View File
@@ -0,0 +1,850 @@
# =============================================================================
# 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
# TargetSelection.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_warn_once(<key> <text>...)
#
# message(WARNING ...) at most once per configure. This file is read again
# for the same scope (see the note at the top), so a plain warning would
# print as many times as the file is read -- and the same multi-line
# warning twice in a row reads as two problems. The <text> arguments are
# joined with nothing between them, like message()'s own, so they must not
# contain a semicolon (a list separator, which would be lost).
# -----------------------------------------------------------------------
function(pmn_warn_once _pmn_key)
get_property(_pmn_seen GLOBAL PROPERTY "PMN_WARNED_${_pmn_key}")
if(NOT _pmn_seen)
set_property(GLOBAL PROPERTY "PMN_WARNED_${_pmn_key}" TRUE)
string(JOIN "" _pmn_text ${ARGN})
message(WARNING "${_pmn_text}")
endif()
endfunction()
# -----------------------------------------------------------------------
# pmn_check_msvc_target(<arch>)
#
# For the presets that link against MSVC's runtime libraries (msvc and
# clang-cl): asks the Visual Studio developer prompt CMake runs in what it is
# set up for, before CMake's own compiler test fails with an error that
# names neither -- "cannot open msvcrt.lib", or lld-link's "could not open
# 'msvcrtd.lib'". Both are warnings: the configure carries on, and either
# works anyway or fails right below the explanation.
#
# * VSCMD_ARG_TGT_ARCH, which the developer prompts set, names another
# target than the preset builds for.
# * VCToolsInstallDir, set by the same prompts, has no lib/<arch>/msvcrt.lib:
# the component that provides the target is not installed. From an x64
# machine ARM64 is a cross build and needs the ARM64 build tools; vcvarsall
# still "succeeds" without them, leaving the host's x64 cl first on PATH
# and the ARM64 library directory in LIB.
# -----------------------------------------------------------------------
function(pmn_check_msvc_target _pmn_arch)
if(DEFINED ENV{VSCMD_ARG_TGT_ARCH} AND NOT "$ENV{VSCMD_ARG_TGT_ARCH}" STREQUAL "${_pmn_arch}")
pmn_warn_once(msvc-target-arch
"This Visual Studio developer prompt targets $ENV{VSCMD_ARG_TGT_ARCH}, but the "
"'${PMN_TOOLCHAIN_DIST_TARGET}' toolchain builds for ${_pmn_arch}. Open the matching prompt "
"(for example an x64 Native Tools or x64_arm64 Cross Tools Command Prompt), or build with "
"breeze\\scripts\\build.bat, which enters the right environment for you, and delete the "
"build directory after switching.")
endif()
if(DEFINED ENV{VCToolsInstallDir} AND NOT EXISTS "$ENV{VCToolsInstallDir}/lib/${_pmn_arch}/msvcrt.lib")
pmn_warn_once(msvc-target-libs
"The MSVC runtime libraries for ${_pmn_arch} are missing: $ENV{VCToolsInstallDir}lib/${_pmn_arch}/msvcrt.lib "
"does not exist, so linking will fail with 'cannot open msvcrt.lib'. Add the component for "
"${_pmn_arch} in the Visual Studio Installer (Modify, Individual components): \"MSVC ... C++ "
"ARM64/ARM64EC build tools (Latest)\" for arm64, which an x64 machine needs too because it "
"cross-compiles, or \"MSVC ... C++ x64/x86 build tools (Latest)\" for x64. breeze\\scripts\\setup.bat "
"offers to add the ARM64 one.")
endif()
endfunction()
# -----------------------------------------------------------------------
# pmn_note_once(<key> <text>...)
#
# message(STATUS ...) at most once per configure -- pmn_warn_once's quiet
# sibling, for information that is useful in the log but is not a problem
# (which target sysroot is in use; that there is none).
# -----------------------------------------------------------------------
function(pmn_note_once _pmn_key)
get_property(_pmn_seen GLOBAL PROPERTY "PMN_NOTED_${_pmn_key}")
if(NOT _pmn_seen)
set_property(GLOBAL PROPERTY "PMN_NOTED_${_pmn_key}" TRUE)
string(JOIN "" _pmn_text ${ARGN})
message(STATUS "${_pmn_text}")
endif()
endfunction()
# -----------------------------------------------------------------------
# pmn_find_sysroot(<x64|arm64|arm32> <dir-var> <why-var>)
#
# Which TARGET SYSROOT, if any, a Linux cross build should use: a directory
# laid out like the target machine's root filesystem (usr/include, usr/lib,
# ...) that holds the libraries and headers the project's dependencies need
# beyond the C/C++ runtime -- X11, OpenGL, ALSA and the like. A cross
# compiler package brings only the runtime (/usr/<triplet>), so a dependency
# that asks for anything else stops with "Could NOT find X11" (raylib's GLFW
# does exactly that on Linux). In order:
#
# 1. -DCMAKE_SYSROOT=<dir> on the command line or in a preset: used as is.
# 2. <project>/.cache/sysroots/linux-<arch>, if that directory exists
# (linux-arm64, linux-arm32, linux-x64): one per architecture, shared
# by the gcc and clang presets, found without any option -- which is
# what lets several cross presets be built one after the other.
#
# Both outputs are empty when there is none. A directory that does not exist
# is an error; one without usr/ in it only a warning (the usual mistake is
# pointing at the folder that CONTAINS the root filesystem).
# -----------------------------------------------------------------------
function(pmn_find_sysroot _pmn_arch _pmn_out_dir _pmn_out_why)
set(_pmn_dir "")
set(_pmn_why "")
# The cache, not the plain variable: a toolchain file sets a plain
# CMAKE_SYSROOT itself (below), and a re-read must not mistake that for
# the user's choice.
if(DEFINED CACHE{CMAKE_SYSROOT} AND NOT "$CACHE{CMAKE_SYSROOT}" STREQUAL "")
set(_pmn_dir "$CACHE{CMAKE_SYSROOT}")
set(_pmn_why "CMAKE_SYSROOT")
else()
get_filename_component(_pmn_root "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE)
if(IS_DIRECTORY "${_pmn_root}/.cache/sysroots/linux-${_pmn_arch}")
set(_pmn_dir "${_pmn_root}/.cache/sysroots/linux-${_pmn_arch}")
set(_pmn_why ".cache/sysroots/linux-${_pmn_arch}")
endif()
endif()
if(_pmn_dir)
if(NOT IS_DIRECTORY "${_pmn_dir}")
message(FATAL_ERROR
"The target sysroot '${_pmn_dir}' (${_pmn_why}) is not a directory.\n"
"Point CMAKE_SYSROOT at a root filesystem for the target (it has usr/include and "
"usr/lib in it), or drop the option. See \"Cross builds and system libraries\" "
"in the README.")
endif()
if(NOT IS_DIRECTORY "${_pmn_dir}/usr")
pmn_warn_once(sysroot-layout
"The target sysroot '${_pmn_dir}' has no usr/ directory, so it does not look like "
"a root filesystem -- is it the folder that contains one? Headers are looked for in "
"<sysroot>/usr/include and libraries in <sysroot>/usr/lib.")
endif()
endif()
set(${_pmn_out_dir} "${_pmn_dir}" PARENT_SCOPE)
set(${_pmn_out_why} "${_pmn_why}" PARENT_SCOPE)
endfunction()
# -----------------------------------------------------------------------
# pmn_use_sysroot(<dir> <triplet>)
#
# Makes <dir> the target's root for this build:
#
# * CMAKE_SYSROOT -- CMake passes --sysroot= to GCC and Clang, and
# find_package/find_library/find_path search under it. It also goes
# FIRST in CMAKE_FIND_ROOT_PATH, ahead of the cross compiler's own
# /usr/<triplet>, so what the target really has wins.
# * CMAKE_LIBRARY_ARCHITECTURE, when the sysroot is Debian-style
# multiarch (libraries in usr/lib/<triplet>/): without it find_library
# doesn't look there.
# * pkg-config: PKG_CONFIG_SYSROOT_DIR / PKG_CONFIG_LIBDIR point at the
# sysroot's .pc files only, and PKG_CONFIG_PATH is dropped, so
# pkg_check_modules() answers for the target instead of the host (whose
# .pc files would hand the compiler x86 include paths and libraries).
#
# A macro, like the rest of this file's helpers that set variables: the
# toolchain file's own scope is where they must land.
# -----------------------------------------------------------------------
macro(pmn_use_sysroot _pmn_sr_dir _pmn_sr_triplet)
set(CMAKE_SYSROOT "${_pmn_sr_dir}")
list(PREPEND CMAKE_FIND_ROOT_PATH "${_pmn_sr_dir}")
list(REMOVE_DUPLICATES CMAKE_FIND_ROOT_PATH)
if(NOT CMAKE_LIBRARY_ARCHITECTURE AND IS_DIRECTORY "${_pmn_sr_dir}/usr/lib/${_pmn_sr_triplet}")
set(CMAKE_LIBRARY_ARCHITECTURE "${_pmn_sr_triplet}")
endif()
set(_pmn_sr_pc "")
foreach(_pmn_sr_sub
usr/lib/pkgconfig
usr/lib64/pkgconfig
usr/lib/${_pmn_sr_triplet}/pkgconfig
usr/share/pkgconfig)
list(APPEND _pmn_sr_pc "${_pmn_sr_dir}/${_pmn_sr_sub}")
endforeach()
list(JOIN _pmn_sr_pc ":" _pmn_sr_pc_joined)
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${_pmn_sr_dir}")
set(ENV{PKG_CONFIG_LIBDIR} "${_pmn_sr_pc_joined}")
unset(ENV{PKG_CONFIG_PATH})
endmacro()
# -----------------------------------------------------------------------
# pmn_toolchain_target(<DIST_TARGET>)
#
# Declares which DIST_TARGET the calling toolchain file is for.
# TargetSelection.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 breeze/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 breeze/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, breeze/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++ -- breeze/scripts/setup.sh installs this for you on
# apt (Debian/Ubuntu), pacman (Arch, arm64 only), and dnf
# (Fedora/RHEL, AArch64/32-bit ARM only, from a COPR since
# Fedora's own cross-gcc ships no libstdc++) -- see its own
# "why" text for exactly what and why, including the gaps:
# x86-64-from-arm64 has no known working package anywhere
# yet, and 32-bit ARM on Arch is AUR-only (x64 hosts only).
# 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>. <triplet> is the one the cross
# g++ says it was built for (-dumpmachine) when that is the
# same architecture -- the preset's own default otherwise.
#
# Both families check what the installed cross compiler tells them instead of
# trusting it: a sysroot it reports that isn't on disk is dropped, with a
# warning; a GCC that can't find the assembler for its own target (and would
# quietly run the host's) is warned about up front, before CMake's compiler
# test fails with an error that names neither. Both are the signature of a
# compiler installed beside binutils or a C library from a different package
# set -- see "32-bit ARM on Arch" in the README.
#
# A cross build can also have a TARGET SYSROOT -- a root filesystem for the
# target with the libraries and headers its dependencies need beyond the
# C/C++ runtime (X11, OpenGL, ALSA, ...), which no cross compiler package
# ships: -DCMAKE_SYSROOT=<dir>, or just a directory at
# .cache/sysroots/linux-<arch>. pmn_find_sysroot / pmn_use_sysroot below
# say what it changes; without one the configure says so in its first lines,
# and a dependency that wants such a library stops with "Could NOT find".
#
# 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 "Run breeze/scripts/setup.sh to install one, or by hand -- e.g. on Debian/Ubuntu: sudo apt install g++")
else()
pmn_use_compilers(clang++ clang "Run breeze/scripts/setup.sh to install one, or by hand -- e.g. on Debian/Ubuntu: sudo apt install clang")
endif()
else()
# Cross.
pmn_cross_target(Linux "${_pmn_tc_processor}" "/usr/${_pmn_tc_triplet}")
# The actual sysroot the installed cross package for this triplet
# was built with -- nothing meaningful on a flat layout (apt's,
# and pacman's -- see the "/" normalization below for what their
# cross-gcc actually prints), a real path on a nested one (dnf's
# COPR toolchain, which mirrors Fedora's own cross-gcc convention:
# /usr/<triplet>/sys-root). Queried once, up front, because both
# families need it below: GCC already knows its own paths for
# compiling and linking, but qemu-user doesn't -- given the flat
# guess on a nested layout, it looks for the target's dynamic
# linker (ld-linux-aarch64.so.1, etc.) in the wrong place and
# doesn't find it ("Could not open '/lib/ld-linux-aarch64.so.1'",
# confirmed on Fedora) -- the same class of problem as Clang's
# link-time one below, just at run time and for either family.
# Clang additionally can't compile at all on a nested layout
# without being told the same thing explicitly -- see below.
find_program(_pmn_tc_cross_gxx NAMES "${_pmn_tc_triplet}-g++" NO_CACHE)
set(_pmn_tc_cross_sysroot "")
set(_pmn_tc_cross_machine "")
if(_pmn_tc_cross_gxx)
execute_process(
COMMAND "${_pmn_tc_cross_gxx}" -print-sysroot
OUTPUT_VARIABLE _pmn_tc_cross_sysroot
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
# A cross-gcc that was never configured with a sysroot of its
# own (apt's -- confirmed on Debian, where this broke qemu for
# every cross preset the moment this query was added) doesn't
# print an empty line for that: it prints "/", gcc's ordinary
# "no special sysroot" default. That is not a sysroot to
# hand anyone -- as qemu's -L it means "the target's root
# filesystem is THIS machine's", so it goes looking for the
# target's dynamic linker in the host's own /lib
# ("Could not open '/lib/ld-linux-aarch64.so.1'") -- and as
# Clang's --sysroot it points header and library lookup at the
# host's /usr too. Treated exactly like empty from here on.
if(_pmn_tc_cross_sysroot STREQUAL "/")
set(_pmn_tc_cross_sysroot "")
endif()
# A sysroot the compiler reports but that isn't on disk is what a
# toolchain whose parts don't belong together looks like: GCC
# relocates its configured sysroot next to itself, but only
# where the relocated directory exists -- otherwise it keeps the
# configured, unrelocated path. Seen with ARM's prebuilt GCC
# (built with --with-sysroot=/arm-none-linux-gnueabihf/libc)
# installed beside a binutils and glibc from somewhere else:
# -print-sysroot says "/arm-none-linux-gnueabihf/libc", nothing is
# there, and Clang -- handed that as --sysroot -- fell back to the
# HOST's crt files and failed to link ("Scrt1.o: file not
# recognized"). A path that doesn't exist is worse than none, so
# it is dropped, and said so.
if(_pmn_tc_cross_sysroot AND NOT IS_DIRECTORY "${_pmn_tc_cross_sysroot}")
pmn_warn_once(sysroot
"The cross compiler ${_pmn_tc_cross_gxx} reports the sysroot "
"'${_pmn_tc_cross_sysroot}', which does not exist, so it is ignored. That is "
"what a toolchain whose parts don't belong together looks like -- a compiler "
"from one package set, binutils or the C library from another -- and linking "
"will most likely fail (\"file not recognized\", a missing crt1.o or libc). "
"Install the compiler, binutils and C library for this target from the same "
"source: breeze/scripts/setup.sh does that where it can (on Arch, for 32-bit ARM, "
"see \"32-bit ARM on Arch\" in the README), or pass -DCMAKE_SYSROOT=<the "
"target's root filesystem>.")
set(_pmn_tc_cross_sysroot "")
endif()
# The triple this compiler itself was built for -- not necessarily
# the one the preset is named after: ARM's own GCC release is
# arm-none-linux-gnueabihf, and its libraries live under
# lib/gcc/arm-none-linux-gnueabihf/, which Clang only finds when
# told that exact triple. (Debian's, Fedora's COPR and Arch's own
# cross compilers all say the same triple the preset uses, so
# nothing changes for them.)
execute_process(
COMMAND "${_pmn_tc_cross_gxx}" -dumpmachine
OUTPUT_VARIABLE _pmn_tc_cross_machine
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
# What the cross compiler package itself reports (or /usr/<triplet>):
# the root qemu runs the result against unless a target sysroot of the
# user's (below) can do it better.
if(_pmn_tc_cross_sysroot)
set(_pmn_tc_emulator_root "${_pmn_tc_cross_sysroot}")
else()
set(_pmn_tc_emulator_root "/usr/${_pmn_tc_triplet}")
endif()
# A target sysroot of the user's, if there is one: where the libraries
# and headers that a cross compiler package doesn't ship (X11,
# OpenGL, ALSA, ...) come from. See pmn_find_sysroot for where it is
# looked for and pmn_use_sysroot for what it changes. It replaces
# the compiler-reported sysroot for Clang's flags below, and for
# qemu's -L when it can run programs, i.e. holds the target's dynamic
# loader -- a partial copy (headers and link libraries only) must not
# take qemu's root away from the toolchain's.
pmn_find_sysroot("${_pmn_tc_arch}" _pmn_tc_sysroot _pmn_tc_sysroot_why)
# A build directory keeps what CMake detected about the compiler -- its library
# directories, the multiarch name -- from the FIRST configure on. A sysroot
# added, changed or removed since then leaves that stale, and the link lines
# go quietly wrong (a stray -L<sysroot>/usr/lib ahead of the compiler's own,
# say). The usual way to get here: a configure stops on a library the
# target's root lacks, the sysroot that has it is put in place, and the same
# build directory is configured again. So stop, and say what to do.
if(_pmn_tc_sysroot)
set(_pmn_tc_sysroot_id "${_pmn_tc_sysroot}")
else()
set(_pmn_tc_sysroot_id "(none)")
endif()
if(DEFINED CACHE{PMN_TC_SYSROOT_USED} AND NOT "$CACHE{PMN_TC_SYSROOT_USED}" STREQUAL "${_pmn_tc_sysroot_id}")
message(FATAL_ERROR
"The target sysroot changed since this build directory was first configured: "
"it was $CACHE{PMN_TC_SYSROOT_USED}, it is now ${_pmn_tc_sysroot_id}.\n"
"CMake keeps what it detected about the compiler in the build directory, and "
"that was detected against the old sysroot, so the link lines would go subtly "
"wrong. Delete the build directory and configure again:\n"
" rm -rf ${CMAKE_BINARY_DIR}")
endif()
set(PMN_TC_SYSROOT_USED "${_pmn_tc_sysroot_id}" CACHE INTERNAL
"The target sysroot this build directory was first configured with")
if(_pmn_tc_sysroot)
pmn_use_sysroot("${_pmn_tc_sysroot}" "${_pmn_tc_triplet}")
pmn_note_once(sysroot "Target sysroot: ${_pmn_tc_sysroot} (${_pmn_tc_sysroot_why})")
set(_pmn_tc_cross_sysroot "${_pmn_tc_sysroot}")
file(GLOB _pmn_tc_loaders
"${_pmn_tc_sysroot}/lib/ld-linux*"
"${_pmn_tc_sysroot}/lib64/ld-linux*"
"${_pmn_tc_sysroot}/usr/lib/ld-linux*"
"${_pmn_tc_sysroot}/usr/lib64/ld-linux*"
"${_pmn_tc_sysroot}/lib/${_pmn_tc_triplet}/ld-linux*"
"${_pmn_tc_sysroot}/usr/lib/${_pmn_tc_triplet}/ld-linux*")
if(_pmn_tc_loaders)
set(_pmn_tc_emulator_root "${_pmn_tc_sysroot}")
endif()
else()
pmn_note_once(no-sysroot
"Cross build without a target sysroot: only ${_pmn_tc_emulator_root} is searched for \
libraries and headers, so a dependency that needs system libraries (X11, OpenGL, ALSA, ...) will \
stop with \"Could NOT find ...\". Put a root filesystem for the target in \
.cache/sysroots/linux-${_pmn_tc_arch}, or pass -DCMAKE_SYSROOT=<dir> -- see \"Cross builds and \
system libraries\" in the README.")
endif()
if("${_pmn_tc_family}" STREQUAL "gcc")
pmn_use_compilers("${_pmn_tc_triplet}-g++" "${_pmn_tc_triplet}-gcc"
"Run breeze/scripts/setup.sh to install the cross compiler for your package manager (apt for every target; dnf for AArch64 and 32-bit ARM, from a COPR; pacman for AArch64, and for 32-bit ARM from the AUR, which breeze/scripts/setup.sh offers -- see its own \"why\" text for details), or by hand -- e.g. on Debian/Ubuntu: sudo apt install ${_pmn_tc_gcc_pkg}")
# A cross GCC finds the assembler for its target in its own
# directories (<prefix>/<target>/bin/as and the like); when it
# doesn't, it quietly runs whatever `as` is on PATH -- the HOST's
# -- and the first file fails with an error that names neither
# (x86's `as` says: "invalid -march= option: `armv7-a+simd'").
# -print-prog-name=as says what GCC would run: an absolute path
# when it found one, the bare name when it didn't.
if(_pmn_tc_cross_gxx AND "${CMAKE_CXX_COMPILER}" STREQUAL "${_pmn_tc_cross_gxx}")
execute_process(
COMMAND "${_pmn_tc_cross_gxx}" -print-prog-name=as
OUTPUT_VARIABLE _pmn_tc_cross_as
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(_pmn_tc_cross_as AND NOT _pmn_tc_cross_as MATCHES "/")
pmn_warn_once(assembler
"The cross compiler ${_pmn_tc_cross_gxx} cannot find the assembler for its own "
"target (-print-prog-name=as gives the bare name '${_pmn_tc_cross_as}'), so "
"it will run the HOST's assembler instead, and the first file will fail with "
"something like \"invalid -march= option\". The compiler and its binutils have "
"to come from the same package set. breeze/scripts/setup.sh checks this with a test "
"compile and, on Arch for 32-bit ARM, installs a matching set (see "
"\"32-bit ARM on Arch\" in the README). Elsewhere install the binutils that "
"belong to this compiler.")
endif()
endif()
else()
# Clang itself is multi-target and happily emits object code for
# ${_pmn_tc_triplet} with no cross package installed at all --
# the failure this check guards against only shows up much
# later, and far more cryptically, at the LINK step: Clang
# still hands linking to whatever plain `ld` it finds (the
# host's own, unless told otherwise), which doesn't recognize
# the target's ELF flavor ("unrecognised emulation mode:
# armelf_linux_eabi", say) since it was never built with that
# target enabled. The GCC branch above never has this problem
# -- "${_pmn_tc_triplet}-g++" simply doesn't exist without the
# cross package, so pmn_use_compilers already fails clearly --
# so checking for that same binary here (found above, for the
# sysroot query), even though Clang doesn't need it to
# compile, catches the same missing-package case before it
# reaches a linker error with no obvious connection to the
# real cause.
if(NOT _pmn_tc_cross_gxx)
message(FATAL_ERROR
"The '${PMN_TOOLCHAIN_DIST_TARGET}' toolchain needs the ${_pmn_tc_triplet} cross "
"package (for its linker and the target's libstdc++/libc) even to use Clang, and "
"it isn't on PATH: run breeze/scripts/setup.sh to install it for your package manager "
"(apt for every target; dnf for AArch64 and 32-bit ARM, from a COPR; pacman for "
"AArch64, and for 32-bit ARM from the AUR, which breeze/scripts/setup.sh offers -- "
"see its own \"why\" text for details), or by hand -- e.g. on Debian/Ubuntu: "
"sudo apt install ${_pmn_tc_gcc_pkg}\n"
"(Without it, configuring would proceed but linking would fail with an obscure "
"\"unrecognised emulation mode\" or similar error instead of this one.)")
endif()
pmn_use_compilers(clang++ clang
"Run breeze/scripts/setup.sh to install Clang plus the matching cross package, or by hand -- e.g. on Debian/Ubuntu: sudo apt install clang ${_pmn_tc_gcc_pkg} (the second package provides the target's libraries)")
set(_pmn_tc_clang_triple "${_pmn_tc_triplet}")
if(_pmn_tc_cross_machine MATCHES "^${_pmn_tc_processor}")
set(_pmn_tc_clang_triple "${_pmn_tc_cross_machine}")
endif()
if(NOT _pmn_tc_clang_triple STREQUAL _pmn_tc_triplet)
message(STATUS "Clang targets ${_pmn_tc_clang_triple}, the triple ${_pmn_tc_cross_gxx} was built for (the preset's default is ${_pmn_tc_triplet})")
endif()
set(CMAKE_CXX_COMPILER_TARGET "${_pmn_tc_clang_triple}")
set(CMAKE_C_COMPILER_TARGET "${_pmn_tc_clang_triple}")
# Unlike GCC, Clang has no distro-specific packaging knowledge
# baked in, so it falls back to a generic guess for where
# THIS triplet's crt1.o/crtbegin.o/libgcc live -- one that
# happens to match a flat /usr/<triplet> layout (apt's,
# pacman's) but not one nested under its own sys-root (dnf's
# COPR toolchain). Guessed wrong, it silently resolves to the
# HOST's own crt1.o instead -- still a real file, just the
# wrong architecture, which surfaces as an obscure linker
# error ("Relocations in generic ELF", confirmed on a Fedora
# aarch64 build) with no obvious connection to a missing
# flag. The real sysroot queried above, from the cross-gcc
# that's actually installed rather than guessed at here,
# fixes this the same way for either family; only Clang needs
# telling. --gcc-toolchain additionally points Clang at
# crtbegin.o/libgcc, which a sysroot alone doesn't cover on
# every layout (confirmed necessary for this same class of
# problem on Arch by the Linux kernel's own cross-build
# tooling, tools/scripts/Makefile.include -- included here
# only alongside a real sysroot, for the same "don't touch
# what Clang already gets right" reason); /usr is where a
# package manager's own cross-gcc lives on every platform
# this toolchain supports.
if(_pmn_tc_cross_sysroot)
set(CMAKE_SYSROOT "${_pmn_tc_cross_sysroot}")
# Appended only if absent: this file is read again for the
# same scope (project() and each enabled language, not just
# try_compile), and a plain append put the flag twice on every
# compile line and four times on every link line.
foreach(_pmn_tc_flags_var CMAKE_CXX_FLAGS_INIT CMAKE_C_FLAGS_INIT
CMAKE_EXE_LINKER_FLAGS_INIT CMAKE_SHARED_LINKER_FLAGS_INIT)
if(NOT "${${_pmn_tc_flags_var}}" MATCHES "--gcc-toolchain=/usr")
string(APPEND ${_pmn_tc_flags_var} " --gcc-toolchain=/usr")
endif()
endforeach()
endif()
endif()
pmn_cross_emulator("${_pmn_tc_qemu}" -L "${_pmn_tc_emulator_root}")
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 the distro's own mingw-w64 packages -- apt, dnf
# and pacman all ship a working one (breeze/scripts/setup.sh knows each; pacman's
# single mingw-w64-gcc covers both x64 and x86, unlike apt/dnf's separate
# packages per architecture) -- 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. breeze\\scripts\\build.bat puts MSYS2's folder on PATH by itself.")
else()
pmn_cross_target(Windows "${_pmn_tc_processor}" "/usr/${_pmn_tc_triplet}")
pmn_use_compilers("${_pmn_tc_triplet}-g++" "${_pmn_tc_triplet}-gcc"
"Run breeze/scripts/setup.sh to install the MinGW-w64 cross compiler for your package manager, or by hand -- e.g. on Debian/Ubuntu: sudo apt install ${_pmn_tc_pkg}")
# Tried architecture-matched name first, the other as fallback --
# not just for distros that ship only one of the two (the
# original reason for trying both), but because on at least one
# that ships both, plain "wine" turned out not to actually be
# 64-bit capable: windows-x64-mingw64 failed to start at all
# through it ("Bad EXE format") while windows-x86-mingw32 through
# that same "wine" worked fine, and explicitly preferring
# "wine64" for the x64 build fixed it. Confirmed harmless where
# this was never a problem (Arch, Fedora): wine64 there either
# already matches or doesn't exist, so the fallback is what runs
# either way, unchanged from before.
if("${_pmn_tc_arch}" STREQUAL "x64")
pmn_cross_emulator("wine64;wine")
else()
pmn_cross_emulator("wine;wine64")
endif()
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
# TargetSelection.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. Or build with breeze\\scripts\\build.bat --preset ${PMN_TOOLCHAIN_DIST_TARGET}, which enters that environment for you.")
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. Or build with breeze\\scripts\\build.bat --preset ${PMN_TOOLCHAIN_DIST_TARGET}, which enters that environment for you.")
endif()
pmn_check_msvc_target("${_pmn_tc_arch}")
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. breeze\\scripts\\build.bat finds LLVM's and Visual Studio's copies by itself.")
set(CMAKE_CXX_COMPILER_TARGET "${_pmn_tc_triple}")
set(CMAKE_C_COMPILER_TARGET "${_pmn_tc_triple}")
pmn_check_msvc_target("${_pmn_tc_arch}")
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. breeze\\scripts\\build.bat puts that shell's toolchain folder on PATH for you.")
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
# TargetSelection.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"
"(breeze/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,21 @@
# 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
# -- or whatever triple the installed cross g++ reports for
# itself (-dumpmachine): ARM's own GCC, the one on Arch, says
# arm-none-linux-gnueabihf, and Clang only finds its libraries
# under that name
# (needs Clang plus the GCC cross package for the target's
# libstdc++/libc, or -DCMAKE_SYSROOT=...; breeze/scripts/setup.sh
# installs both on apt -- clang g++-arm-linux-gnueabihf --
# and, from a COPR, dnf -- see linux-arm32-gcc.cmake for
# details, and for why not on pacman).
# 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,23 @@
# 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++.
# breeze/scripts/setup.sh installs this for you on apt
# (g++-arm-linux-gnueabihf) or, from a COPR, dnf
# (arm-linux-gnueabihf-{binutils,gcc,glibc}); not through
# pacman -- on Arch it exists only in the AUR, as ARM's own
# prebuilt toolchain split into four packages
# (arm-linux-gnueabihf-{binutils,glibc-headers,glibc,gcc}-bin;
# x64 hosts only -- see the README, "32-bit ARM on Arch").
# That GCC is built for the triple arm-none-linux-gnueabihf,
# and finds its assembler and C library by that name, so the
# four have to be installed together: a source-built binutils
# or glibc beside it is the mix that fails (_common.cmake
# warns about both symptoms). 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,17 @@
# 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
# (needs Clang plus the GCC cross package for the target's
# libstdc++/libc, or -DCMAKE_SYSROOT=...; breeze/scripts/setup.sh
# installs both on apt -- clang g++-aarch64-linux-gnu --
# pacman -- clang aarch64-linux-gnu-gcc -- or, from a COPR,
# dnf -- clang aarch64-linux-gnu-{binutils,gcc,glibc}).
# 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,15 @@
# 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++.
# breeze/scripts/setup.sh installs this for you on apt
# (g++-aarch64-linux-gnu), pacman (aarch64-linux-gnu-gcc), or,
# from a COPR, dnf (aarch64-linux-gnu-{binutils,gcc,glibc}).
# 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,16 @@
# 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
# (needs Clang plus the GCC cross package for the target's
# libstdc++/libc, or -DCMAKE_SYSROOT=...; breeze/scripts/setup.sh
# installs both on apt -- clang g++-x86-64-linux-gnu -- see
# _common.cmake's own comment for other package managers).
# 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,16 @@
# 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++.
# breeze/scripts/setup.sh installs this for you on apt
# (g++-x86-64-linux-gnu); not through dnf or pacman -- see
# _common.cmake's own comment (and this is a rare case in
# the first place: it only matters on a non-x64 Linux host).
# 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. TargetSelection.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.
# TargetSelection.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. TargetSelection.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,15 @@
# 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. breeze/scripts/setup.sh installs this for you on
# apt (g++-mingw-w64-x86-64), dnf (mingw64-gcc-c++) or
# pacman (mingw-w64-gcc, which also covers the 32-bit
# preset below). 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. TargetSelection.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,15 @@
# 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. breeze/scripts/setup.sh installs this for you on
# apt (g++-mingw-w64-i686), dnf (mingw32-gcc-c++) or pacman
# (mingw-w64-gcc, which also covers the 64-bit preset
# above). 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)
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
#
# breeze/githooks/pre-commit
#
# Runs clang-format and clang-tidy against staged C/C++ files before each
# commit — no Python, no pre-commit framework, just bash and (optionally)
# the clang tools themselves.
#
# Neither tool is required to have this hook installed: if a binary isn't
# on PATH, this prints a warning and skips that check instead of blocking
# the commit. Only *actual formatting/lint issues* from a tool that IS
# installed will block a commit.
#
# Platforms:
# - Linux / macOS: runs under the system bash (or Homebrew bash on macOS)
# with no extra setup.
# - Windows: runs automatically through Git for Windows' bundled Git Bash —
# git.exe recognizes the "#!/usr/bin/env bash" line above and executes
# hooks through it directly, so no WSL, Cygwin, or separate bash install
# is needed. Everything this script uses (mktemp, diff, grep, printf,
# command -v) ships with Git for Windows out of the box.
#
# One-time setup per clone (same command on every OS):
# chmod +x breeze/githooks/pre-commit (no-op on Windows, harmless either way)
# git config core.hooksPath breeze/githooks
set -uo pipefail
# Colors only when writing to an interactive terminal — avoids raw escape
# codes in CI logs, redirected output, or terminals that don't support ANSI.
if [ -t 1 ]; then
RED=$'\033[0;31m'
YELLOW=$'\033[0;33m'
GREEN=$'\033[0;32m'
RESET=$'\033[0m'
else
RED=''
YELLOW=''
GREEN=''
RESET=''
fi
exit_code=0
# --- Collect staged C/C++ files (added/copied/modified) ---
# ':!vendor/*' excludes third-party code vendored under vendor/ (see
# breeze/cmake/Dependencies.cmake in the parent project) -- it isn't this
# project's code, so it isn't held to this style guide. Not part of the
# toolkit as shipped; added for this project's vendor/ convention.
files=()
while IFS= read -r line; do
[ -n "$line" ] && files+=("$line")
done < <(git diff --cached --name-only --diff-filter=ACM -- \
'*.c' '*.h' '*.cpp' '*.hpp' '*.cc' '*.cxx' '*.tpp' ':!vendor/*')
if [ ${#files[@]} -eq 0 ]; then
exit 0
fi
# On macOS, Homebrew's llvm (the formula that ships clang-tidy) is keg-only:
# installed but not on PATH. Look there too, AFTER the normal PATH so Apple's
# tools stay first. (breeze/scripts/_common.sh's _pmn_extend_tool_path does the same
# for the scripts; a git hook is kept standalone, so it's repeated here.)
if [ "$(uname -s)" = Darwin ]; then
for _llvm_bin in /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin; do
if [ -d "$_llvm_bin" ]; then PATH="$PATH:$_llvm_bin"; break; fi
done
fi
# --- clang-format: check staged files are already formatted ---
if command -v clang-format >/dev/null 2>&1; then
unformatted=()
for f in "${files[@]}"; do
[ -f "$f" ] || continue
if ! diff -q <(clang-format "$f") "$f" >/dev/null 2>&1; then
unformatted+=("$f")
fi
done
if [ ${#unformatted[@]} -gt 0 ]; then
echo "${RED}clang-format: these files aren't formatted:${RESET}"
printf ' %s\n' "${unformatted[@]}"
echo "Fix with: clang-format -i ${unformatted[*]}"
exit_code=1
fi
else
echo "${YELLOW}warning: clang-format not found on PATH — skipping format check.${RESET}"
fi
# --- clang-tidy: static analysis, needs a compile database ---
if command -v clang-tidy >/dev/null 2>&1; then
if [ ! -f compile_commands.json ]; then
echo "${YELLOW}warning: compile_commands.json not found — skipping clang-tidy.${RESET}"
echo "${YELLOW}Generate one with: cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ...${RESET}"
else
# A template, because BSD mktemp (macOS) wants one where GNU's doesn't.
tidy_output=$(mktemp "${TMPDIR:-/tmp}/pmn-clang-tidy.XXXXXX")
tidy_had_issues=0
for f in "${files[@]}"; do
[ -f "$f" ] || continue
if ! clang-tidy --quiet "$f" >>"$tidy_output" 2>&1; then
tidy_had_issues=1
fi
done
if grep -q "warning:\|error:" "$tidy_output" 2>/dev/null; then
echo "${RED}clang-tidy found issues:${RESET}"
cat "$tidy_output"
exit_code=1
elif [ "$tidy_had_issues" -eq 1 ]; then
# Non-zero exit with no warning/error text usually means clang-tidy
# itself couldn't run (e.g. missing headers) — surface it, but
# don't block the commit on a tooling problem rather than a style one.
echo "${YELLOW}warning: clang-tidy exited with an error unrelated to style:${RESET}"
cat "$tidy_output"
fi
rm -f "$tidy_output"
fi
else
echo "${YELLOW}warning: clang-tidy not found on PATH — skipping static analysis.${RESET}"
fi
if [ $exit_code -eq 0 ]; then
echo "${GREEN}pre-commit checks passed.${RESET}"
fi
exit $exit_code
+114
View File
@@ -0,0 +1,114 @@
# Shared helpers for breeze/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 breeze/cmake/<something>.cmake` relative to that.
#
# There is deliberately no Windows twin of this file: breeze/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 project/Configuration.cmake -- this project's single
# source of truth -- via breeze/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 breeze/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 breeze/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 breeze/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 breeze/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: breeze/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
# `breeze/scripts/run.sh` / `install.sh` / `docs.sh` without a --preset uses it.
_pmn_record_build_dir() {
cmake "-DPMN_NAME=$1" -DPMN_RECORD=ON -P breeze/cmake/BuildDir.cmake >/dev/null
}
# _pmn_explain_configure_failure <configure-log> [<build-directory>]
#
# Called by breeze/scripts/build.sh after a failed configure. When it was a cross
# build with no target sysroot (the toolchain says so in the first lines of
# its output -- breeze/cmake/toolchains/_common.cmake, pmn_linux_toolchain) AND
# something could not be found, says what that means: the dependency asked
# for a system library (X11, OpenGL, ALSA, ...) that only a target sysroot
# can provide. Silent in every other case -- not every "Could NOT find" is
# this one, and the configure's own error is already on screen.
_pmn_explain_configure_failure() {
[ -r "$1" ] || return 0
grep -q 'Cross build without a target sysroot' "$1" || return 0
grep -qE 'Could NOT find|Could not find|No package .* found' "$1" || return 0
cat >&2 <<'EOF'
==> Configure failed in a cross build that has no target sysroot, and something
it needs was not found. That is most likely this: a cross build looks only
in the target's own root (the cross compiler's /usr/<triplet>), never at the
host's libraries -- those are for the wrong architecture -- and that root has
the C/C++ runtime and nothing else. A library such as X11, OpenGL or ALSA is
there only if you provide it:
* put a root filesystem for the target, with those libraries installed, in
.cache/sysroots/linux-<arch> (arm64, arm32 or x64), or
* pass -DCMAKE_SYSROOT=<dir> to breeze/scripts/build.sh.
Then delete the build directory before building again: it keeps what it detected
about the compiler without the sysroot, and a configure over that stops.
EOF
if [ -n "${2:-}" ]; then
echo " rm -rf $2" >&2
fi
echo ' breeze/README.md, "Cross builds and system libraries", has the steps.' >&2
}
# 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
}
+697
View File
@@ -0,0 +1,697 @@
<#
.SYNOPSIS
Interactive installer for the tools this project builds with. Windows;
called by breeze\scripts\setup.bat (Linux and macOS have breeze/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), the MSVC
ARM64 build tools (to build the windows-arm64-msvc and
-clang-cl presets), 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, breeze/cmake/Breeze.cmake, and the presets files' 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')
}
# The ARM64 half of the Visual Studio tools: what an x64 machine cross-compiles
# the windows-arm64-* presets with (the x64-hosted ARM64 compiler and the ARM64
# runtime libraries), and an ARM64 machine builds them natively with. vswhere
# again, asking for both components: the x86/x64 one is what the ARM64 tools run
# on from an x64 machine.
function Get-MsvcArm64Status {
$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 Microsoft.VisualStudio.Component.VC.Tools.ARM64 -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)
}
# Adds a component to the Visual Studio (or Build Tools) install Get-MsvcStatus
# found, with the Visual Studio Installer's own `modify` command: the documented
# way to add a component to an existing install. It needs administrator rights,
# so the installer is started elevated (the usual UAC prompt) and waited for.
# Exit code 3010 means "done, restart needed", which is still a success.
function Install-MsvcComponent([string]$ComponentId) {
$pf86 = Get-ProgramFilesX86
if (-not $pf86) { return $false }
$setup = Join-Parts $pf86 'Microsoft Visual Studio' 'Installer' 'setup.exe'
$vswhere = Join-Parts $pf86 'Microsoft Visual Studio' 'Installer' 'vswhere.exe'
if (-not ((Test-Path -LiteralPath $setup -PathType Leaf) -and (Test-Path -LiteralPath $vswhere -PathType Leaf))) {
Bad ' The Visual Studio Installer was not found.'
return $false
}
$installPath = $null
try {
$installPath = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null | Select-Object -First 1
} catch { }
if (-not $installPath) {
Bad ' No Visual Studio install with the C++ tools was found to add it to.'
return $false
}
$installerArgs = @('modify', '--installPath', ('"' + $installPath + '"'), '--add', $ComponentId, '--passive', '--norestart')
Write-Host (' $ "' + $setup + '" ' + ($installerArgs -join ' ')) -ForegroundColor DarkGray
try {
$proc = Start-Process -FilePath $setup -ArgumentList $installerArgs -Verb RunAs -Wait -PassThru
} catch {
Bad (' Could not start the Visual Studio Installer: ' + $_.Exception.Message)
return $false
}
return (($proc.ExitCode -eq 0) -or ($proc.ExitCode -eq 3010))
}
# Whether Update-Msys2Base has already run (successfully or not) in this
# process, so installing several MSYS2-hosted toolchains in one setup.bat
# run (e.g. -All pulling in mingw64, mingw32 AND clang64) only pays for the
# update dance -- the slow, disruptive part -- once, not once per package.
$script:Msys2BaseUpdated = $false
# Brings the MSYS2 installation's OWN packages (msys2-runtime, bash, pacman,
# filesystem, ...) up to date, which a fresh install -- what a winget install
# just produced -- badly needs: its package databases are whatever shipped
# in that installer image.
#
# This is exactly the three-step sequence msys2/setup-msys2 -- the official
# GitHub Action, and about as battle-tested a reference as this gets --
# uses, confirmed by reading its source (main.js): an update whose result is
# NOT trusted, an explicit taskkill, then a second, final update whose
# result IS. It exists because updating msys2-runtime/bash/pacman while
# they are the very things running the update can end with MSYS2 force-
# closing every one of its own processes for safety (mid-transaction, to
# avoid the heap corruption a live-running old copy of msys2-runtime would
# otherwise risk) rather than the update finishing normally -- documented
# behavior (msys2.org/docs/updating), not a bug in this project, and not
# something --noconfirm reliably suppresses in every MSYS2-pacman version
# (see msys2/MSYS2-packages#1141, "Updating MSYS2 unattended is broken").
# Running the update a second time is what actually finishes the job (per
# that same documentation: "you need to start a new terminal and run the
# update again"); the taskkill in between is what makes that safe to do
# from the SAME script, back-to-back, without a real terminal to close --
# and it must run from OUTSIDE the MSYS2 process tree to have any effect,
# i.e. from PowerShell, exactly as done here (see the discussion on
# msys2/MSYS2-packages#4340, where MSYS2's own maintainers say so).
# `-FI "MODULES eq msys-2.0.dll"` targets any process with that DLL loaded,
# not any process literally named bash.exe/pacman.exe -- so it cannot catch
# an unrelated bash.exe (WSL's, Git for Windows', a plain MSYS bash someone
# else is using) that never loaded MSYS2's runtime in the first place.
# `--overwrite '*'` on every pacman call, also lifted from that same
# reference implementation, avoids pacman refusing an update over an
# unimportant file conflict -- a real, if uncommon, way this could
# otherwise still stall unattended.
function Update-Msys2Base {
if ($script:Msys2BaseUpdated) { return $true }
$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 --overwrite ''*'' -Syuu" (updates MSYS2 itself; may need to close and reopen)' -ForegroundColor DarkGray
& $bash -lc "pacman --noconfirm --overwrite '*' -Syuu"
Write-Host ' $ taskkill /F /FI "MODULES eq msys-2.0.dll" (clears any MSYS2 process the update above force-closed)' -ForegroundColor DarkGray
& taskkill /F /FI 'MODULES eq msys-2.0.dll' 2>$null | Out-Null
Write-Host ' $ bash -lc "pacman --noconfirm --overwrite ''*'' -Syuu" (final update)' -ForegroundColor DarkGray
& $bash -lc "pacman --noconfirm --overwrite '*' -Syuu"
$script:Msys2BaseUpdated = $true
return ($LASTEXITCODE -eq 0)
}
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
}
if (-not (Update-Msys2Base)) {
Bad ' Updating MSYS2 itself failed -- see the output above. Try re-running breeze\scripts\setup.bat once MSYS2 has settled (it may need a moment after force-closing its own processes), or update it by hand in an MSYS2 terminal: pacman -Syuu (repeat until it reports nothing to do).'
return $false
}
$list = $Packages -join ' '
Write-Host (' $ ' + $bash + ' -lc "pacman -S --needed --noconfirm --overwrite ''*'' ' + $list + '"') -ForegroundColor DarkGray
& $bash -lc ("pacman -S --needed --noconfirm --overwrite '*' " + $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 },
# Needs msvc: it adds a component to that install. Without it an x64
# machine cannot build the windows-arm64-msvc and -clang-cl presets, which
# cross-compile through it.
@{ Id = 'msvc-arm64'; Kind = 'optional'; Label = 'MSVC ARM64 build tools';
Why = 'the windows-arm64-msvc and windows-arm64-clang-cl presets (a cross build from an x64 machine)';
Needs = 'msvc';
Status = { Get-MsvcArm64Status };
Install = { Install-MsvcComponent 'Microsoft.VisualStudio.Component.VC.Tools.ARM64' };
PathDir = $null },
@{ Id = 'llvm'; Kind = 'optional'; Label = 'LLVM (clang-cl, Clang, clang-format, clang-tidy)';
Why = 'the windows-*-clang-cl presets, breeze\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 ' (breeze\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 ' (breeze\scripts\build.bat does not need this: it puts the right MSYS2 toolchain folder first on PATH'
Note ' for the preset it builds, for that run only. Adding the folder here puts its g++/gcc on PATH for every program.)'
}
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
}
File diff suppressed because it is too large. Load diff
+318
View File
@@ -0,0 +1,318 @@
@echo off
REM Enters the compiler environment a Windows preset needs, in THIS cmd.exe
REM session: what opening "x64 Native Tools Command Prompt for VS" or an MSYS2
REM MINGW64 shell by hand does, without opening a window or leaving the
REM console you are in.
REM
REM Not run directly -- breeze\scripts\build.bat calls it once the preset is known:
REM call "breeze\scripts\_winenv.bat" ^<preset^>
REM It uses no SETLOCAL on purpose: the variables vcvarsall.bat sets (PATH,
REM INCLUDE, LIB, ...) have to land in the CALLER's session. build.bat's own
REM SETLOCAL discards them again when it ends, so nothing leaks into the
REM prompt you started it from.
REM
REM preset environment entered
REM windows-x64-msvc, windows-arm64-msvc the Visual Studio developer
REM prompt for that target
REM (vcvarsall.bat from the newest
REM Visual Studio or Build Tools
REM that has the C++ tools)
REM windows-x64-clang-cl, the same, plus clang-cl on PATH
REM windows-arm64-clang-cl if it is not already (LLVM's own
REM folder, else the copy Visual
REM Studio ships)
REM windows-x64-mingw64, MSYS2's mingw64\bin or
REM windows-x86-mingw32 mingw32\bin first on PATH, unless
REM that compiler is already there
REM windows-x64-clang, MSYS2's clang64\bin or
REM windows-arm64-clang clangarm64\bin first on PATH --
REM always: a clang++ from anywhere
REM else targets a different ABI
REM
REM After entering a Visual Studio environment it checks that the target's runtime
REM libraries exist (lib\TARGET\msvcrt.lib), and for arm64 picks the newest Visual
REM Studio that has the ARM64 component: from an x64 machine that is the only way
REM to build arm64, as a cross build, and without it the failure would only appear
REM at CMake's compiler test. MSYS2 has no CLANGARM64 environment on an x64
REM machine; windows-arm64-clang says so and points at the cross-building presets.
REM
REM Left alone: a shell that is already a developer prompt for the right
REM architecture; any preset not listed (cmake reports an unknown one itself);
REM and everything, when PMN_NO_ENV is set to anything -- for a toolchain this script does
REM not know about, or a prompt you have set up yourself.
REM MSYS2 is looked for in MSYS2_ROOT, else the msys64 folder on the system
REM drive -- the same place breeze\scripts\setup.bat installs it.
REM
REM Exit status: 0 = ready, or nothing to do. 1 = the environment the preset
REM needs is not available, after saying what to install.
REM
REM Flat on purpose: single-line IF and GOTO, no parenthesized blocks. The paths
REM expanded here can contain "(x86)", and an unquoted close-paren ends a block
REM early.
if "%~1"=="" exit /b 0
if defined PMN_NO_ENV exit /b 0
set "_pmn_e_tgt="
set "_pmn_e_llvm="
set "_pmn_e_sub="
set "_pmn_e_exe="
set "_pmn_e_probe="
set "_pmn_e_pkg="
set "_pmn_e_vs="
set "_pmn_e_arg="
set "_pmn_e_need="
if /i "%~1"=="windows-x64-msvc" goto pick_msvc_x64
if /i "%~1"=="windows-arm64-msvc" goto pick_msvc_arm64
if /i "%~1"=="windows-x64-clang-cl" goto pick_clangcl_x64
if /i "%~1"=="windows-arm64-clang-cl" goto pick_clangcl_arm64
if /i "%~1"=="windows-x64-mingw64" goto pick_mingw64
if /i "%~1"=="windows-x86-mingw32" goto pick_mingw32
if /i "%~1"=="windows-x64-clang" goto pick_clang64
if /i "%~1"=="windows-arm64-clang" goto pick_clangarm64
goto env_ok
:pick_msvc_x64
set "_pmn_e_tgt=x64"
goto run_vs
:pick_msvc_arm64
set "_pmn_e_tgt=arm64"
goto run_vs
:pick_clangcl_x64
set "_pmn_e_tgt=x64"
set "_pmn_e_llvm=1"
goto run_vs
:pick_clangcl_arm64
set "_pmn_e_tgt=arm64"
set "_pmn_e_llvm=1"
goto run_vs
:pick_mingw64
set "_pmn_e_sub=mingw64"
set "_pmn_e_exe=g++.exe"
set "_pmn_e_probe=x86_64-w64-mingw32-g++"
set "_pmn_e_pkg=mingw-w64-x86_64-gcc"
goto run_msys
:pick_mingw32
set "_pmn_e_sub=mingw32"
set "_pmn_e_exe=g++.exe"
set "_pmn_e_probe=i686-w64-mingw32-g++"
set "_pmn_e_pkg=mingw-w64-i686-gcc"
goto run_msys
:pick_clang64
set "_pmn_e_sub=clang64"
set "_pmn_e_exe=clang++.exe"
set "_pmn_e_pkg=mingw-w64-clang-x86_64-clang"
goto run_msys
:pick_clangarm64
set "_pmn_e_sub=clangarm64"
set "_pmn_e_exe=clang++.exe"
set "_pmn_e_pkg=mingw-w64-clang-aarch64-clang"
goto run_msys
REM ---------------------------------------------------------------------
REM Visual Studio: msvc and clang-cl presets
REM ---------------------------------------------------------------------
:run_vs
if not defined VSCMD_VER goto vs_find
if /i "%VSCMD_ARG_TGT_ARCH%"=="%_pmn_e_tgt%" goto vs_have
goto vs_find
:vs_have
where cl >nul 2>nul
if errorlevel 1 goto vs_find
if defined VSINSTALLDIR set "_pmn_e_vs=%VSINSTALLDIR%"
goto vs_verify
:vs_find
set "_pmn_e_vs="
set "_pmn_e_vswhere=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%_pmn_e_vswhere%" goto vs_missing
REM Which component provides the target's compilers and libraries: the x86/x64
REM tools for x64, and for arm64 the ARM64 tools too -- an x64 machine reaches
REM arm64 through them (a cross build), and an ARM64 machine builds natively.
REM The newest install that has what the target needs wins, so an older Visual
REM Studio that has it beats a newer one that does not.
set "_pmn_e_need=Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
if /i "%_pmn_e_tgt%"=="arm64" set "_pmn_e_need=Microsoft.VisualStudio.Component.VC.Tools.x86.x64 Microsoft.VisualStudio.Component.VC.Tools.ARM64"
for /f "usebackq delims=" %%I in (`"%_pmn_e_vswhere%" -latest -products * -requires %_pmn_e_need% -property installationPath`) do set "_pmn_e_vs=%%I"
if defined _pmn_e_vs goto vs_have_install
REM None has it. Is there a Visual Studio with C++ at all? Then it is only the
REM target's component that is missing, which has its own message.
for /f "usebackq delims=" %%I in (`"%_pmn_e_vswhere%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "_pmn_e_vs=%%I"
if not defined _pmn_e_vs goto vs_missing
goto vs_target_missing
:vs_have_install
set "_pmn_e_vcvars=%_pmn_e_vs%\VC\Auxiliary\Build\vcvarsall.bat"
if not exist "%_pmn_e_vcvars%" goto vs_missing
REM vcvarsall's argument is HOST_TARGET, or just TARGET when the two match.
set "_pmn_e_host=%PROCESSOR_ARCHITECTURE%"
if defined PROCESSOR_ARCHITEW6432 set "_pmn_e_host=%PROCESSOR_ARCHITEW6432%"
if /i "%_pmn_e_host%"=="AMD64" goto vs_arg_amd64
if /i "%_pmn_e_host%"=="ARM64" goto vs_arg_arm64
set "_pmn_e_arg=x86_%_pmn_e_tgt%"
goto vs_enter
:vs_arg_amd64
if /i "%_pmn_e_tgt%"=="x64" set "_pmn_e_arg=x64"
if /i "%_pmn_e_tgt%"=="arm64" set "_pmn_e_arg=x64_arm64"
goto vs_enter
:vs_arg_arm64
if /i "%_pmn_e_tgt%"=="x64" set "_pmn_e_arg=arm64_x64"
if /i "%_pmn_e_tgt%"=="arm64" set "_pmn_e_arg=arm64"
goto vs_enter
REM Success is judged by what vcvarsall left behind -- the target it reports
REM and a cl on PATH, which is what the build needs -- not by its exit status.
:vs_enter
set "VSCMD_SKIP_SENDTELEMETRY=1"
echo ==^> Environment: Visual Studio developer prompt for %_pmn_e_tgt% ^(vcvarsall %_pmn_e_arg%^)
call "%_pmn_e_vcvars%" %_pmn_e_arg% >"%TEMP%\pmn-vcvarsall.log" 2>&1
if /i "%VSCMD_ARG_TGT_ARCH%"=="%_pmn_e_tgt%" goto vs_check_cl
goto vs_failed
:vs_check_cl
where cl >nul 2>nul
if errorlevel 1 goto vs_failed
if not defined VCToolsInstallDir goto vs_failed
goto vs_verify
REM The target's runtime libraries must exist: with the target's component
REM missing, vcvarsall can still report the target and leave a cl on PATH (the
REM host's x64 one), and the failure only shows up at CMake's compiler test as
REM "cannot open msvcrt.lib". This catches it first, with the fix.
:vs_verify
if not defined VCToolsInstallDir goto vs_after
if exist "%VCToolsInstallDir%lib\%_pmn_e_tgt%\msvcrt.lib" goto vs_after
goto vs_target_missing
:vs_after
del "%TEMP%\pmn-vcvarsall.log" >nul 2>nul
if not "%_pmn_e_llvm%"=="1" goto env_ok
where clang-cl >nul 2>nul
if not errorlevel 1 goto env_ok
set "_pmn_e_pf=%ProgramW6432%"
if not defined _pmn_e_pf set "_pmn_e_pf=%ProgramFiles%"
if exist "%_pmn_e_pf%\LLVM\bin\clang-cl.exe" goto llvm_standalone
if not defined _pmn_e_vs goto llvm_missing
if exist "%_pmn_e_vs%\VC\Tools\Llvm\x64\bin\clang-cl.exe" goto llvm_vs_x64
if exist "%_pmn_e_vs%\VC\Tools\Llvm\ARM64\bin\clang-cl.exe" goto llvm_vs_arm64
if exist "%_pmn_e_vs%\VC\Tools\Llvm\bin\clang-cl.exe" goto llvm_vs_x86
goto llvm_missing
:llvm_standalone
set "PATH=%_pmn_e_pf%\LLVM\bin;%PATH%"
echo ==^> Environment: clang-cl from %_pmn_e_pf%\LLVM\bin
goto env_ok
:llvm_vs_x64
set "PATH=%_pmn_e_vs%\VC\Tools\Llvm\x64\bin;%PATH%"
echo ==^> Environment: clang-cl from Visual Studio ^(VC\Tools\Llvm\x64\bin^)
goto env_ok
:llvm_vs_arm64
set "PATH=%_pmn_e_vs%\VC\Tools\Llvm\ARM64\bin;%PATH%"
echo ==^> Environment: clang-cl from Visual Studio ^(VC\Tools\Llvm\ARM64\bin^)
goto env_ok
:llvm_vs_x86
set "PATH=%_pmn_e_vs%\VC\Tools\Llvm\bin;%PATH%"
echo ==^> Environment: clang-cl from Visual Studio ^(VC\Tools\Llvm\bin^)
goto env_ok
:llvm_missing
echo error: preset %~1 needs clang-cl, and none was found on PATH, in %_pmn_e_pf%\LLVM\bin or in Visual Studio. 1>&2
echo Run breeze\scripts\setup.bat to install LLVM, or: winget install LLVM.LLVM 1>&2
goto env_fail
:vs_missing
echo error: preset %~1 needs Visual Studio or Build Tools with the C++ tools ^(workload "Desktop development with C++"^), and none was found. 1>&2
echo Run breeze\scripts\setup.bat to install it. If you use a Visual Studio this script cannot find, open its 1>&2
echo developer prompt for %_pmn_e_tgt% yourself and set PMN_NO_ENV=1. 1>&2
if /i "%_pmn_e_tgt%"=="arm64" echo For ARM64 targets add the "MSVC ARM64 build tools" component in the Visual Studio Installer. 1>&2
goto env_fail
:vs_target_missing
echo error: preset %~1 needs the MSVC tools and libraries for %_pmn_e_tgt%, and the Visual Studio install here does not have them. 1>&2
if defined VCToolsInstallDir echo %VCToolsInstallDir%lib\%_pmn_e_tgt%\msvcrt.lib is missing. 1>&2
if exist "%TEMP%\pmn-vcvarsall.log" type "%TEMP%\pmn-vcvarsall.log" 1>&2
if /i "%_pmn_e_tgt%"=="arm64" goto vs_target_missing_arm64
echo Add the component "MSVC ... C++ x64/x86 build tools (Latest)" in the Visual Studio Installer ^(Modify, Individual components^). 1>&2
goto env_fail
:vs_target_missing_arm64
echo Building for ARM64 -- from an x64 machine that is a cross build -- needs the component 1>&2
echo "MSVC ... C++ ARM64/ARM64EC build tools (Latest)". Run breeze\scripts\setup.bat, which offers to add it, or add it in the 1>&2
echo Visual Studio Installer: Modify, Individual components, search for ARM64. 1>&2
goto env_fail
:vs_failed
echo error: could not enter the Visual Studio environment for %_pmn_e_tgt% ^(vcvarsall %_pmn_e_arg%^). Its output: 1>&2
if exist "%TEMP%\pmn-vcvarsall.log" type "%TEMP%\pmn-vcvarsall.log" 1>&2
echo If this shell is already a developer prompt for another architecture, open a fresh one. 1>&2
if /i "%_pmn_e_tgt%"=="arm64" echo ARM64 targets need the "MSVC ARM64 build tools" component of Visual Studio. 1>&2
goto env_fail
REM ---------------------------------------------------------------------
REM MSYS2: mingw and clang presets
REM ---------------------------------------------------------------------
:run_msys
set "_pmn_e_root=%MSYS2_ROOT%"
if not defined _pmn_e_root set "_pmn_e_root=%SystemDrive%\msys64"
if not defined _pmn_e_probe goto msys_enter
where %_pmn_e_probe% >nul 2>nul
if not errorlevel 1 goto env_ok
:msys_enter
if not exist "%_pmn_e_root%\%_pmn_e_sub%\bin\%_pmn_e_exe%" goto msys_missing
set "PATH=%_pmn_e_root%\%_pmn_e_sub%\bin;%PATH%"
echo ==^> Environment: MSYS2 %_pmn_e_sub% ^(%_pmn_e_root%\%_pmn_e_sub%\bin first on PATH^)
goto env_ok
:msys_missing
if /i "%_pmn_e_sub%"=="clangarm64" goto msys_missing_arm
goto msys_missing_pkg
REM MSYS2 has a CLANGARM64 environment only on ARM64 Windows; on an x64 machine
REM there is nothing to install, so say what to use instead.
:msys_missing_arm
set "_pmn_e_host=%PROCESSOR_ARCHITECTURE%"
if defined PROCESSOR_ARCHITEW6432 set "_pmn_e_host=%PROCESSOR_ARCHITEW6432%"
if /i "%_pmn_e_host%"=="ARM64" goto msys_missing_pkg
echo error: preset %~1 needs MSYS2's CLANGARM64 environment, which MSYS2 provides only on ARM64 Windows ^(this machine is %_pmn_e_host%^). 1>&2
echo To build for Windows ARM64 from here use windows-arm64-clang-cl or windows-arm64-msvc: they cross-compile 1>&2
echo with Visual Studio's ARM64 build tools ^(breeze\scripts\setup.bat offers to add them^). 1>&2
goto env_fail
:msys_missing_pkg
echo error: preset %~1 needs MSYS2's %_pmn_e_sub% toolchain, and %_pmn_e_root%\%_pmn_e_sub%\bin\%_pmn_e_exe% was not found. 1>&2
echo Run breeze\scripts\setup.bat to install it, or in an MSYS2 shell: pacman -S %_pmn_e_pkg% 1>&2
echo MSYS2 itself: winget install MSYS2.MSYS2 ^(set MSYS2_ROOT if it lives somewhere other than the msys64 folder on the system drive^) 1>&2
goto env_fail
REM ---------------------------------------------------------------------
REM Exits. Every path out goes through one of these two, which clear the
REM scratch variables this script made (all start with _pmn_e_).
REM ---------------------------------------------------------------------
:env_ok
call :clear
exit /b 0
:env_fail
call :clear
exit /b 1
:clear
set "_pmn_e_tgt="
set "_pmn_e_llvm="
set "_pmn_e_sub="
set "_pmn_e_exe="
set "_pmn_e_probe="
set "_pmn_e_pkg="
set "_pmn_e_vs="
set "_pmn_e_arg="
set "_pmn_e_vswhere="
set "_pmn_e_vcvars="
set "_pmn_e_need="
set "_pmn_e_host="
set "_pmn_e_pf="
set "_pmn_e_root="
goto :eof
+383
View File
@@ -0,0 +1,383 @@
@echo off
REM Configure (if needed) and build the project, preferably with Ninja.
REM
REM Usage (from the project root):
REM breeze\scripts\build.bat [--preset ^<name^>] [BUILD_TYPE] [CMAKE_ARGS...]
REM breeze\scripts\build.bat --list-presets
REM breeze\scripts\build.bat --help
REM
REM --preset <name> Use a CMakePresets.json preset. A preset selects that
REM target's toolchain file (breeze\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 breeze\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: project/Configuration.cmake's DEFAULT_BUILD_TYPE)
REM CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.:
REM breeze\scripts\build.bat --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
REM breeze\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=breeze\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, TargetSelection.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 Compiler environment: for a Windows preset (windows-*-msvc, -clang-cl,
REM -mingw* and -clang) this script first enters the environment that preset's
REM compiler needs, for this run only: the Visual Studio developer prompt for the
REM preset's target (vcvarsall.bat), or MSYS2's toolchain folder first on PATH. So
REM it works from any prompt -- no need to open the x64 Native Tools Command
REM Prompt for VS or an MSYS2 shell first. A prompt that is already right is left alone;
REM set PMN_NO_ENV=1 to skip all of this and use your shell as it is. See
REM _winenv.bat for exactly what it does.
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. breeze\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: breeze\scripts\build.bat [--preset ^<name^>] [BUILD_TYPE] [CMAKE_ARGS...]
echo breeze\scripts\build.bat --list-presets
echo breeze\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: project/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 Windows presets: the compiler's environment ^(the Visual Studio developer prompt
echo for the target, or MSYS2's toolchain folder^) is entered for you,
echo for this run only -- no special prompt needed first. Set
echo PMN_NO_ENV=1 to use your shell as it is.
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 ^(breeze\scripts\build.bat^) and TargetSelection.cmake's "Target selection"
echo section for why picking a DIST_TARGET is one or the other, not both.
echo.
echo Examples:
echo breeze\scripts\build.bat --preset linux-x64-clang
echo breeze\scripts\build.bat --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
echo breeze\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 Which cmake the user's PATH resolves to, so it can be put back in front after
REM a Visual Studio environment is entered below: that puts Visual Studio's own
REM cmake -- often an older one -- ahead of it.
set "_pmn_cmake_dir="
for /f "delims=" %%C in ('where cmake') do if not defined _pmn_cmake_dir set "_pmn_cmake_dir=%%~dpC"
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. breeze\cmake\BuildDir.cmake owns the
REM layout (project/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 breeze\scripts\build.bat --preset %PRESET%
echo breeze\scripts\build.bat -DCMAKE_TOOLCHAIN_FILE=breeze\cmake\toolchains\^<target^>.cmake
echo See the comment at the top of this script, or TargetSelection.cmake's
echo "Target selection" section, for why these two aren't combined.
exit /b 1
)
)
REM Prefer a pinned ninja (fetched by breeze\scripts\setup.bat -- see
REM breeze\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 project/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 breeze\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 breeze\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 breeze\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
if not defined PRESET goto configure_env_done
call "%~dp0_winenv.bat" "%PRESET%"
if errorlevel 1 exit /b 1
if defined _pmn_cmake_dir set "PATH=%_pmn_cmake_dir%;%PATH%"
:configure_env_done
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 breeze\cmake\BuildDir.cmake >nul
echo ==^> Building
cmake --build "%BUILD_DIR%"
if errorlevel 1 exit /b 1
echo ==^> Done. Run a module with: breeze\scripts\run.bat ^<target-name^>
exit /b 0
REM ---------------------------------------------------------------------
REM :read_config <ConfigurationVarName> <LocalVarName>
REM
REM Reads one value out of project/Configuration.cmake -- this project's single
REM source of truth -- via breeze\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 breeze\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 breeze\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 breeze\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 breeze\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: breeze\cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+289
View File
@@ -0,0 +1,289 @@
#!/usr/bin/env bash
# Configure (if needed) and build the project, preferably with Ninja.
#
# Usage (from the project root):
# breeze/scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
# breeze/scripts/build.sh --list-presets
# breeze/scripts/build.sh --help
#
# --preset <name> Use a CMakePresets.json preset. A preset selects that
# target's toolchain file (breeze/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:
# breeze/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: project/Configuration.cmake's DEFAULT_BUILD_TYPE)
# CMAKE_ARGS Anything else is forwarded to `cmake` verbatim, e.g.:
# breeze/scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
# breeze/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=breeze/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, TargetSelection.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. breeze/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: breeze/scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
breeze/scripts/build.sh --list-presets
breeze/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: project/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 (breeze/scripts/build.sh) and TargetSelection.cmake's "Target selection"
section for why picking a DIST_TARGET is one or the other, not both.
Examples:
breeze/scripts/build.sh --preset linux-x64-clang
breeze/scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
breeze/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 TargetSelection.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. breeze/cmake/BuildDir.cmake owns the
# layout (project/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 " breeze/scripts/build.sh --preset ${PRESET}" >&2
echo " breeze/scripts/build.sh -DCMAKE_TOOLCHAIN_FILE=breeze/cmake/toolchains/<target>.cmake" >&2
echo " See the comment at the top of this script, or TargetSelection.cmake's" >&2
echo " \"Target selection\" section, for why these two aren't combined." >&2
exit 1
fi
# Prefer a pinned ninja (fetched by breeze/scripts/setup.sh -- see
# breeze/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 project/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 " breeze/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 breeze/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
# The configure's output is also kept in a temporary file, only so that a
# failed cross configure can be explained afterwards (_pmn_explain_configure_
# failure, breeze/scripts/_common.sh). Its exit status is still what decides.
# If no temporary file can be made the build goes on without the explanation.
_pmn_cfg_log="$(mktemp "${TMPDIR:-/tmp}/pmn-configure.XXXXXX" 2>/dev/null)" || _pmn_cfg_log=/dev/null
trap 'if [ -f "$_pmn_cfg_log" ]; then rm -f "$_pmn_cfg_log"; fi' EXIT
if ! cmake "${CONFIGURE_ARGS[@]}" ${GENERATOR_ARGS[@]+"${GENERATOR_ARGS[@]}"} -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" ${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"} 2>&1 | tee "$_pmn_cfg_log"; then
_pmn_explain_configure_failure "$_pmn_cfg_log" "${BUILD_DIR:-}"
exit 1
fi
# 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: breeze/scripts/run.sh <target-name>"
+136
View File
@@ -0,0 +1,136 @@
@echo off
REM Build Doxygen documentation for every module (see breeze\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 project/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 breeze\scripts\docs.bat REM every module
REM breeze\scripts\docs.bat core_lib REM just one module's docs (docs_<target>)
REM breeze\scripts\docs.bat --preset windows-x64-clang-cl REM ... from that preset's build directory
REM breeze\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: breeze\scripts\docs.bat [--preset ^<name^>] [module-target-name]
echo.
echo Builds Doxygen documentation for every module ^(see breeze\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 breeze\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 breeze\scripts\docs.bat core_lib
echo.
echo Output goes to out\docs\ ^(DOCS_OUTPUT_DIR in project/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 breeze\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 breeze\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 breeze\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 breeze\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 breeze\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: breeze\cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Build Doxygen documentation for every module (see breeze/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 project/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):
# breeze/scripts/docs.sh # every module
# breeze/scripts/docs.sh core_lib # just one module's docs (docs_<target>)
# breeze/scripts/docs.sh --preset linux-x64-gcc # ... from that preset's build directory
# breeze/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: breeze/scripts/docs.sh [--preset <name>] [module-target-name]
Builds Doxygen documentation for every module (see breeze/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
breeze/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.:
breeze/scripts/docs.sh core_lib
Output goes to out/docs/ (DOCS_OUTPUT_DIR in project/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 breeze/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 breeze/scripts/build.sh${PRESET:+ --preset $PRESET} first." >&2
exit 1
fi
echo "==> Using build directory ${BUILD_DIR}"
if [ $# -ge 1 ]; then
TARGET="docs_$1"
cmake --build "${BUILD_DIR}" --target "${TARGET}"
echo "==> Open: ${DOCS_DIR}/$1/html/index.html"
else
cmake --build "${BUILD_DIR}" --target docs
if [ -f "${DOCS_DIR}/index.html" ]; then
echo "==> Open: ${DOCS_DIR}/index.html"
fi
fi
+151
View File
@@ -0,0 +1,151 @@
@echo off
REM Install this project's own build artifacts to
REM out\install\<platform>-<arch>-<compiler>\ (INSTALL_OUTPUT_DIR in
REM project/Configuration.cmake; see breeze\cmake\Breeze.cmake'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 breeze\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 breeze\scripts\install.bat [--preset <name>] [BUILD_TYPE]
REM breeze\scripts\install.bat --help
REM
REM --preset <name> Install the build made with that preset
REM (out\build\<name>). Without it: the build
REM breeze\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: project/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: breeze\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 project/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 breeze\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: project/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 breeze\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 breeze\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 breeze\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 project/Configuration.cmake -- this project's single
REM source of truth -- via breeze\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 breeze\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 breeze\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 breeze\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 breeze\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: breeze\cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Install this project's own build artifacts to
# out/install/<platform>-<arch>-<compiler>/ (INSTALL_OUTPUT_DIR in
# project/Configuration.cmake; see breeze/cmake/Breeze.cmake'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
# breeze/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):
# breeze/scripts/install.sh [--preset <name>] [BUILD_TYPE]
# breeze/scripts/install.sh --help
#
# --preset <name> Install the build made with that preset
# (out/build/<name>). Without it: the build
# breeze/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: project/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: breeze/scripts/install.sh [--preset <name>] [BUILD_TYPE]
Installs this project's own build artifacts to
out/install/<platform>-<arch>-<compiler>/ (INSTALL_OUTPUT_DIR in
project/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 breeze/scripts/build.sh made last. Must come
first if given at all; --preset=<name> also works.
BUILD_TYPE Debug|Release|RelWithDebInfo|MinSizeRel
(default: project/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 project/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 breeze/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 breeze/scripts/build.sh${PRESET:+ --preset $PRESET} first." >&2
exit 1
fi
echo "==> Installing (component: ${COMPONENT}) from ${BUILD_DIR}"
cmake --install "${BUILD_DIR}" --config "${BUILD_TYPE}" --component "${COMPONENT}"
+104
View File
@@ -0,0 +1,104 @@
@echo off
REM Applies clang-format and clang-tidy's --fix across this project's own
REM C/C++ code. The pre-commit hook only CHECKS; this is the "just fix it"
REM counterpart -- review the result with `git diff` afterward, same as any
REM auto-formatter.
REM
REM Defaults to source\ and c-api\, this template's own-code roots -- NEVER
REM walks out\, .cache\, vendor\, or .git\ by default, so it can't
REM accidentally spend minutes reformatting/analyzing a fetched dependency's
REM entire source tree the way scanning the whole repo from "." would.
REM
REM Usage, from the project root: breeze\scripts\refactor.bat [optional path]
REM breeze\scripts\refactor.bat every file under source\ and c-api\
REM breeze\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 breeze\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: breeze\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 breeze\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 breeze\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 breeze\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 breeze\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 breeze\scripts\build.bat first^)
goto after_tidy
:run_tidy
echo ==^> Running clang-tidy --fix ^(auto-fixable issues only; build once first if this is stale^)
for /f "usebackq delims=" %%F in ("%FILELIST%") do clang-tidy --quiet --fix "%%F"
:after_tidy
del "%FILELIST%" 2>nul
echo ==^> Done. Review the changes: git diff
exit /b 0
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Applies clang-format and clang-tidy's --fix across this project's own
# C/C++ code. The pre-commit hook only CHECKS; this is the "just fix it"
# counterpart -- review the result with `git diff` afterward, same as any
# auto-formatter.
#
# Defaults to source/ and c-api/ (this template's own-code roots) --
# NEVER walks out/, .cache/, vendor/, or .git/ by default, so it can't
# accidentally spend minutes reformatting/analyzing a fetched dependency's
# entire source tree (e.g. out/build/_deps/*-src/) the way scanning the
# whole repo from "." would. Passing an explicit scope still excludes
# those four directories defensively, in case the scope you give overlaps
# with them.
#
# Usage (from the project root):
# breeze/scripts/refactor.sh # every file under source/ and c-api/
# breeze/scripts/refactor.sh source/Core # just one file or directory
# breeze/scripts/refactor.sh --help
set -euo pipefail
case "${1:-}" in
-h|--help)
cat <<'EOF'
Usage: breeze/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.:
breeze/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 breeze/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 breeze/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 breeze/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 breeze/scripts/setup.sh)"
fi
echo "==> Done. Review the changes: git diff"
+185
View File
@@ -0,0 +1,185 @@
@echo off
REM Run an executable module by its CMake target name (as registered in
REM that module's own CMakeLists.txt), resolved via the manifest that
REM _write_module_manifest() generates at configure time -- so this script
REM never needs updating when modules are added, removed, or renamed.
REM
REM Usage (from the project root):
REM breeze\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 breeze\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 breeze\scripts\run.bat hello_exe
REM breeze\scripts\run.bat --preset windows-x64-clang-cl hello_exe
REM breeze\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 breeze\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 breeze\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 breeze\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 breeze\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 breeze\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 breeze\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 breeze\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 breeze\scripts\build.bat?)
echo.
echo Examples:
echo breeze\scripts\run.bat hello_exe
echo breeze\scripts\run.bat --preset windows-x64-clang-cl hello_exe
echo breeze\scripts\run.bat farewell_exe -- Ale
exit /b 0
REM ---------------------------------------------------------------------
REM :resolve_build_dir <name-or-empty> <LocalVarName> [quiet]
REM
REM Asks breeze\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 breeze\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: breeze\cmake\BuildDir.cmake did not answer. 1>&2
)
:resolve_failed
endlocal
exit /b 1
:resolve_ok
endlocal & set "%~2=%_pmn_val:/=\%"
goto :eof
+367
View File
@@ -0,0 +1,367 @@
#!/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):
# breeze/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 breeze/scripts/build.sh made last, or
# the only one that exists. Must come first if given at
# all; --preset=<name> also works.
#
# Examples:
# breeze/scripts/run.sh hello_exe
# breeze/scripts/run.sh --preset linux-x64-clang hello_exe
# breeze/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 breeze/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 breeze/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 breeze/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 breeze/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 breeze/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 breeze/scripts/build.sh?)"
fi
echo
echo "Examples:"
echo " breeze/scripts/run.sh hello_exe"
echo " breeze/scripts/run.sh --preset linux-x64-clang hello_exe"
echo " breeze/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 breeze/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 breeze/scripts/build.sh${PRESET:+ --preset $PRESET}." >&2
exit 1
fi
# _pmn_pe_kind <file>: what kind of Windows program this is, read from the
# Machine field of its PE header -- the two bytes after the "PE\0\0"
# signature, which e_lfanew (the 4 bytes at offset 0x3C) points at. Prints
# x86 (32-bit), x64 (64-bit x86), or other (ARM64, a file that isn't a PE at
# all, one that can't be read). `od` rather than `file`, so nothing has to be
# installed, and byte by byte so the host's endianness doesn't matter.
_pmn_pe_kind() {
local o0 o1 o2 o3 s0 s1 s2 s3 m0 m1 off
read -r o0 o1 o2 o3 <<< "$(od -An -tu1 -j60 -N4 "$1" 2>/dev/null || true)"
if [ -z "${o3:-}" ]; then
echo other
return 0
fi
off=$(( o0 + o1 * 256 + o2 * 65536 + o3 * 16777216 ))
read -r s0 s1 s2 s3 m0 m1 <<< "$(od -An -tu1 -j"$off" -N6 "$1" 2>/dev/null || true)"
# 80 69 0 0 is "PE\0\0".
if [ "${s0:-}" = 80 ] && [ "${s1:-}" = 69 ] && [ "${s2:-}" = 0 ] && [ "${s3:-}" = 0 ] && [ -n "${m1:-}" ]; then
case $(( m0 + m1 * 256 )) in
332) echo x86; return 0 ;; # 0x014c IMAGE_FILE_MACHINE_I386
34404) echo x64; return 0 ;; # 0x8664 IMAGE_FILE_MACHINE_AMD64
esac
fi
echo other
}
# _pmn_wine_lacks_wine64 <wine>: 0 if <wine> is Debian/Ubuntu's helper script
# and this machine has that packaging's 32-bit loader but no 64-bit one -- the
# state described below -- so that a 64-bit program cannot run here whatever
# else is tried. Recognised by layout rather than by which version of wine it
# is: a script under /usr/bin (alternatives -> wine-stable or
# wine-development) beside a loader directory that holds wine but no wine64.
_pmn_wine_lacks_wine64() {
local emu real dir
emu="$(command -v "$1" 2>/dev/null)" || return 1
real="$(readlink -f "$emu" 2>/dev/null)" || real="$emu"
case "$real" in
/usr/bin/wine-development) dir=/usr/lib/wine-development ;;
/usr/bin/*) dir=/usr/lib/wine ;;
*) return 1 ;;
esac
[ "$(head -c2 "$real" 2>/dev/null)" = '#!' ] && [ -x "$dir/wine" ] && [ ! -x "$dir/wine64" ]
}
# _pmn_wine64_loader <wine>: prints a loader that can run 64-bit Windows
# programs and create a 64-bit prefix for them, given the plain `wine` the
# toolchain registered -- or fails if it knows of nothing better than <wine>.
#
# Plain `wine` isn't always that. On Debian and Ubuntu it is a helper script
# (alternatives -> wine-stable / wine-development), not a loader, and the one
# in wine 9.0's packaging runs the 32-bit loader whenever wine32 is installed
# -- which breeze/scripts/setup.sh makes sure of, for the 32-bit preset. A 32-bit
# loader with no wine64 to hand over to creates a 32-bit prefix even under
# WINEARCH=win64 and then refuses it: "WINEARCH set to win64 but '<prefix>'
# is a 32-bit installation". That is what a machine looks like after
# `apt-get install wine wine32:i386` (wine only depends on "wine64 | wine32",
# so wine64 gets left out): reproduced, with that exact output, from that
# command. The 64-bit loader is a separate file there, /usr/lib/wine/wine64,
# and nothing puts it on PATH.
_pmn_wine64_loader() {
local emu real w64
emu="$(command -v "$1" 2>/dev/null)" || return 1
real="$(readlink -f "$emu" 2>/dev/null)" || real="$emu"
# A wine64 right next to it: Arch, Fedora, WineHQ's /opt/wine-*/bin, ...
for w64 in "${real%/*}/wine64" "${emu%/*}/wine64"; do
if [ -x "$w64" ]; then
printf '%s\n' "$w64"
return 0
fi
done
case "${real##*/}" in
wine-stable) w64=/usr/lib/wine/wine64 ;;
wine-development) w64=/usr/lib/wine-development/wine64 ;;
*) return 1 ;;
esac
if [ -x "$w64" ]; then
printf '%s\n' "$w64"
return 0
fi
return 1
}
# A cross build's toolchain file (breeze/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
# Wine refuses to start at all without a valid XDG_RUNTIME_DIR -- "error:
# XDG_RUNTIME_DIR is invalid or not set in the environment" -- even for a
# console-only .exe that needs no desktop integration whatsoever; a
# session manager normally sets this, which containers (confirmed on a
# distrobox one) typically have no reason to run. Wine only checks that
# the directory exists with the right ownership/permissions, not that it
# came from a real session, so a private one under TMPDIR is enough to
# get past this. Only for wine, and only if nothing is already set -- a
# real desktop session already has a real one, left alone.
case "${RUN_PREFIX[0]:-}" in
*/wine|wine|*/wine64|wine64)
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
export XDG_RUNTIME_DIR="${TMPDIR:-/tmp}/xdg-runtime-$(id -u)"
mkdir -p "$XDG_RUNTIME_DIR" 2>/dev/null
chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null
fi
# 32-bit or 64-bit, read from the program itself. Everything below
# keeps the two apart, because a wine prefix is one or the other from
# the moment it is created and is never shared safely between them.
_pmn_kind="$(_pmn_pe_kind "$BIN_PATH")"
# A 64-bit program needs a loader that can build and run a 64-bit
# prefix, which plain `wine` isn't everywhere (see
# _pmn_wine64_loader).
if [ "$_pmn_kind" = x64 ] && [ "${RUN_PREFIX[0]##*/}" = wine ]; then
if _pmn_w64="$(_pmn_wine64_loader "${RUN_PREFIX[0]}")"; then
RUN_PREFIX[0]="$_pmn_w64"
elif _pmn_wine_lacks_wine64 "${RUN_PREFIX[0]}"; then
# Nothing below can make this work, and trying only ends in
# wine's own "WINEARCH set to win64 but ... is a 32-bit
# installation" -- which says nothing about the cause.
echo "error: ${TARGET_NAME} is a 64-bit Windows program, but this machine has only the" >&2
echo " 32-bit wine, which can neither run it nor create the 64-bit prefix it" >&2
echo " needs. Debian and Ubuntu package the 64-bit part separately:" >&2
echo " sudo apt install wine64 (breeze/scripts/setup.sh installs it too)" >&2
exit 1
fi
fi
# Running Debian's own loader directly -- chosen just above, or
# already what the build registered -- means going around the helper
# script that normally runs it, so do the two things that script does
# for wine64: point WINELOADER at it, and quiet wine's "fixme:"
# chatter with WINEDEBUG=fixme-all. Each only if nothing set it.
case "${RUN_PREFIX[0]}" in
/usr/lib/wine/wine64|/usr/lib/wine-development/wine64)
if [ -z "${WINELOADER:-}" ]; then export WINELOADER="${RUN_PREFIX[0]}"; fi
if [ -z "${WINEDEBUG:-}" ]; then export WINEDEBUG=fixme-all; fi
;;
esac
# Default wine prefix is ~/.wine, shared with every other use of
# wine on the machine -- scoping it under .cache/ instead makes a
# fresh `rm -rf .cache/wine` the fix for anything that looks like a
# wine problem rather than a build one, and keeps this project's
# own wine state from mixing with anything else's. One prefix per
# kind of program (.cache/wine/x86, .cache/wine/x64): a prefix's
# architecture is fixed by whichever program first creates it, so
# sharing one made the outcome depend on which preset happened to
# run first, and on a wine whose 32-bit loader can only make 32-bit
# prefixes, no amount of recreating it helped. Only if WINEPREFIX
# isn't already set -- someone who's deliberately pointed it
# elsewhere gets left alone, and that includes never deleting it.
if [ -z "${WINEPREFIX:-}" ]; then
# Older versions used .cache/wine itself as the one prefix. A
# prefix has a system.reg at its top, so that is easy to tell
# apart from this layout; its content is only a disposable cache.
if [ -f "$PWD/.cache/wine/system.reg" ]; then
rm -rf "$PWD/.cache/wine"
fi
export WINEPREFIX="$PWD/.cache/wine/$_pmn_kind"
# A 64-bit prefix recorded as 32-bit (a prefix's own system.reg
# says which, in a line reading exactly "#arch=win32" or
# "#arch=win64") can't be what asked for 64-bit, and wine refuses
# to run anything against it once WINEARCH=win64 disagrees --
# so it's rebuilt, since it's this project's own cache.
if [ "$_pmn_kind" = x64 ] && [ -f "$WINEPREFIX/system.reg" ] \
&& grep -q '^#arch=win32' "$WINEPREFIX/system.reg" 2>/dev/null; then
rm -rf "$WINEPREFIX"
fi
fi
# Wine creates the last component of the prefix path and no more, so
# with no .cache/ yet (a fresh checkout) it fails: "wine: chdir to
# <prefix> : No such file or directory".
mkdir -p "$WINEPREFIX"
# A 64-bit prefix is asked for outright, so that one that can't be
# made fails at once and by name rather than as a 32-bit prefix that
# then can't run the program. Left alone for 32-bit programs: any
# prefix runs those, and forcing win32 isn't possible on every wine.
if [ "$_pmn_kind" = x64 ] && [ -z "${WINEARCH:-}" ]; then
export WINEARCH=win64
fi
;;
esac
# 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 " (breeze/scripts/build.sh --preset <name>) so the toolchain picks it up." >&2
fi
exit "$_pmn_status"
+226
View File
@@ -0,0 +1,226 @@
@echo off
REM One-time (or re-run any time) environment setup for this template.
REM Windows; on Linux and macOS it is breeze/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, the MSVC ARM64 build tools and the other MSYS2
REM toolchains (optional) -- and
REM offers to install whatever is missing, ONE TOOL AT A TIME, showing the
REM exact command before running it. Nothing is installed without
REM your yes. The work is done by breeze\scripts\_tools.ps1 (PowerShell: prompts,
REM PATH handling and winget are far more robust there than in batch).
REM 2. Fetches the pinned ninja (breeze\cmake\FetchNinja.cmake) -- used by
REM breeze\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 (breeze\cmake\FetchDoxygen.cmake) -- NOT
REM required to build the project itself (see breeze\cmake\Documentation.cmake),
REM only to run breeze\scripts\docs.bat afterward.
REM 4. Activates the pre-commit hook (breeze\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 breeze\scripts\setup.bat [--yes] [--all] [--everything] [--check] [--skip-tools]
REM breeze\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
REM --everything: on Linux it also does slow extras (AUR builds); nothing on
REM Windows needs more than --all, so it means the same here.
if /i "%~1"=="--everything" 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 breeze\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: breeze\scripts\setup.bat [--yes] [--all] [--everything] [--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, the MSVC ARM64 build tools and the other MSYS2
echo toolchains are optional^). Each
echo install shows its exact command and waits for your yes.
echo 2. Fetches the pinned ninja ^(breeze\cmake\FetchNinja.cmake^).
echo 3. Fetches the pinned Doxygen ^(breeze\cmake\FetchDoxygen.cmake^).
echo 4. Activates the pre-commit hook ^(breeze\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 --everything Same as --all here. ^(On Linux, breeze/scripts/setup.sh --everything
echo also does slow extras such as AUR builds; Windows has none.^)
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 breeze\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 (breeze\cmake\FetchNinja.cmake)
cmake -P breeze\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 breeze\scripts\build.bat needs a ninja: e.g. winget install Ninja-build.Ninja
:ninja_done
REM ---- 3. pinned Doxygen -----------------------------------------------------
echo.
echo ==^> Fetching pinned Doxygen (breeze\cmake\FetchDoxygen.cmake)
cmake -P breeze\cmake\FetchDoxygen.cmake
if errorlevel 1 goto doxygen_failed
goto doxygen_done
:doxygen_failed
echo.
echo Documentation ^(breeze\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 breeze/githooks
echo core.hooksPath = breeze/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 breeze/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. breeze\scripts\build.bat and breeze\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
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env bash
# One-time (or re-run any time) environment setup for this template. Linux
# and macOS; on Windows it is breeze\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 breeze/scripts/_tools.sh for what it knows and how.
# 2. Fetches the pinned ninja (breeze/cmake/FetchNinja.cmake) -- used by
# breeze/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 (breeze/cmake/FetchDoxygen.cmake) -- NOT
# required to build the project itself (see breeze/cmake/Documentation.cmake),
# only to run breeze/scripts/docs.sh / docs.bat afterward.
# 4. Activates the pre-commit hook (breeze/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):
# breeze/scripts/setup.sh [--yes] [--all] [--everything] [--check] [--skip-tools]
# breeze/scripts/setup.sh --help
set -euo pipefail
_usage() {
cat <<'EOF'
Usage: breeze/scripts/setup.sh [--yes] [--all] [--everything] [--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 (breeze/cmake/FetchNinja.cmake).
3. Fetches the pinned Doxygen (breeze/cmake/FetchDoxygen.cmake).
4. Activates the pre-commit hook (breeze/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). This includes saying yes to the extra-repository
questions that come with some of them: enabling pacman's
multilib, dnf's COPR for the ARM cross compilers, apt's
i386 architecture for wine32.
--everything Everything --all does, plus the third-party extras that
--all leaves for you to opt into (implies --all). On
Arch: the prebuilt 32-bit ARM cross toolchain from the
AUR -- four packages built with makepkg (git and
base-devel are installed first if missing): a download
and a few minutes, no compiling. Run interactively,
setup asks about it as its own question instead.
Elsewhere there is nothing extra, so it means the same
as --all.
--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_HEAVY=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 ;;
--everything) PMN_TOOLS_HEAVY=1; 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 breeze/scripts/setup.sh --help for usage." >&2
exit 1
;;
esac
shift
done
export PMN_TOOLS_YES PMN_TOOLS_ALL PMN_TOOLS_HEAVY 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 breeze/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 (breeze/cmake/FetchNinja.cmake)"
if ! cmake -P breeze/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 " breeze/scripts/build.sh needs a ninja: install one and put it on PATH."
else
echo " breeze/scripts/build.sh needs a ninja: install one and put it on PATH."
fi
fi
# ---- 3. pinned Doxygen -----------------------------------------------------
echo
echo "==> Fetching pinned Doxygen (breeze/cmake/FetchDoxygen.cmake)"
if ! cmake -P breeze/cmake/FetchDoxygen.cmake; then
echo
echo " Documentation (breeze/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 breeze/githooks/pre-commit
git config core.hooksPath breeze/githooks
echo " core.hooksPath = breeze/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 breeze/githooks/pre-commit && git config core.hooksPath breeze/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. breeze/scripts/build.sh and breeze/scripts/docs.sh are ready to use."
+57
View File
@@ -0,0 +1,57 @@
---
# c-api/.clang-tidy
# Place this file at the root of whatever directory holds your Part II
# (C API) surface — e.g. c-api/, public-include/, or wherever your extern "C"
# headers and shims live. clang-tidy always uses the closest .clang-tidy file
# up the directory tree, so files under this directory pick up these
# snake_case naming rules instead of the PascalCase/camelCase rules in the
# repo-root .clang-tidy, without any extra flags or per-file configuration.
#
# Formatting (tabs, Allman braces, line length) still comes from the single
# root-level .clang-format — only naming differs between Part I and Part II,
# per style guide §15 (Indentation and Braces).
InheritParentConfig: true
Checks: >
readability-identifier-naming,
bugprone-exception-escape
CheckOptions:
# --- C API naming — style guide §13 ---
# Every public symbol uses snake_case with a library/module prefix
# (e.g. net_manager_connect, NET_MAX_RETRIES). clang-tidy can enforce the
# casing; the prefix itself is a code-review convention.
- key: readability-identifier-naming.FunctionCase
value: lower_case
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: lower_case
- key: readability-identifier-naming.LocalVariableCase
value: lower_case
- key: readability-identifier-naming.StructCase
value: CamelCase
- key: readability-identifier-naming.EnumCase
value: CamelCase
# Enum values — ALL_CAPS_SNAKE_CASE with a library + enum prefix
# (e.g. NET_STATE_CONNECTED).
- key: readability-identifier-naming.EnumConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.GlobalConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.MacroDefinitionCase
value: UPPER_CASE
# Internal (non-exported) global/static variables — style guide §13's
# g_/s_ rows. Only applies to mutable state; GlobalConstantCase above
# still governs const globals, which stay UPPER_CASE with no prefix.
- key: readability-identifier-naming.GlobalVariableCase
value: lower_case
- key: readability-identifier-naming.GlobalVariablePrefix
value: g_
- key: readability-identifier-naming.StaticVariableCase
value: lower_case
- key: readability-identifier-naming.StaticVariablePrefix
value: s_
+168
View File
@@ -0,0 +1,168 @@
# =============================================================================
# project/Configuration.cmake
#
# The project's own settings: the one file to edit to say who this project is
# and where its output goes. Everything that makes the build work is the
# template's -- breeze/ -- and is replaced wholesale when the template is
# updated; nothing in this file is. Change a value here, or override a cache
# variable with -D<NAME>=... at configure time, instead of hunting through
# CMakeLists.txt, breeze/cmake/*.cmake -- or a script under breeze/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).
#
# What is NOT here: the target selection (DIST_TARGET, PLATFORM, ARCHITECTURE,
# COMPILER_ID). That is the template's, in breeze/cmake/TargetSelection.cmake.
#
# -----------------------------------------------------------------------
# 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, breeze/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.
#
# -----------------------------------------------------------------------
# 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 breeze/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` / breeze/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
# (breeze/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
# -----------------------------------------------------------------------
# breeze/scripts/build.sh|build.bat and breeze/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 breeze/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
# breeze/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. That is why this file holds
# settings only: everything that needs a compiler is the template's, in
# breeze/cmake/TargetSelection.cmake, which only a real configure includes (as
# it does the .gitignore sync and the other file system side effects). Keep it
# that way: anything added here must be evaluable without a compiler, must
# not touch the disk and must not message(STATUS ...) -- a script reads the
# output of its query, and a query runs far more often than a real configure.
#
# Included by CMakeLists.txt before project() (project() takes its arguments
# from the identity settings below), again by breeze/cmake/Breeze.cmake (a
# no-op, see include_guard), and directly by the `cmake -P` scripts named
# above -- a bare invocation like that never goes through CMakeLists.txt's own
# cmake_minimum_required(), so without one of its own here, this file's
# `IN_LIST` checks (and BuildDir.cmake's) would run under whatever policy
# CMake defaults to outside a project -- OLD on any CMake still old enough to
# make that a real choice (CMP0057 -- IN_LIST inside if() -- was NEW-only-by-
# default for a long stretch of CMake versions before finally being removed
# outright in CMake 4.0), which turns `if(x IN_LIST y)` into a straight parse
# error. Repeating the same version CMakeLists.txt already requires is a
# harmless no-op there; it's what makes every OTHER way into this file safe
# too.
# =============================================================================
cmake_minimum_required(VERSION 3.25)
include_guard(GLOBAL)
# -------------------------------------------------------
# Project identity
# -------------------------------------------------------
# What CMakeLists.txt hands to project(): the name, version, description and
# languages, and the C++ standard breeze/cmake/Breeze.cmake then applies.
set(PROJECT_TITLE "DemoApp")
set(PROJECT_VERSION_NUMBER "0.1.0")
set(PROJECT_SUMMARY "Demo app exercising the generator-friendly CMake module system")
set(PROJECT_LANGUAGES CXX)
set(PROJECT_CXX_STANDARD 20)
# -------------------------------------------------------
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")
# -------------------------------------------------------
# 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 breeze/cmake/FetchNinja.cmake and
# breeze/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")
# 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)")
+49
View File
@@ -0,0 +1,49 @@
# fmt: fetched from source at the pinned tag, unless an installed copy that
# is at least version 12 is found first.
#
# GIT_TAG 12.1.0 the version this template is built and tested
# with. Anything older than 11.1 does not compile
# with Clang >= 21 (including clang-cl): its
# FMT_STRING macro, used inside fmt's own sources,
# is rejected as "not a constant expression"
# (fmtlib/fmt#4807). 12.x also drops the
# fmt::format-via-<fmt/core.h> shim, which is why
# Greeting.cpp includes <fmt/format.h>.
# Deliberately not the newest release, 12.2.0:
# that one doesn't compile for 32-bit x86
# (windows-x86-mingw32) -- its fallback for a
# missing __int128 lacks operator~ -- and the
# fix, fmtlib/fmt#4813, landed on main after the
# tag. Move up once a release contains it.
# FIND_PACKAGE_ARGS 12 ... the "12" is a minimum version for an installed
# copy. Without it ANY installed fmt beats the
# pin -- distro packages are old (Ubuntu 24.04
# ships 9.1, 26.04 ships 10.1) and would quietly
# bring the Clang >= 21 failure back.
# GIT_SHALLOW TRUE every preset has its own build directory (and
# so its own _deps/), so keep each fetch small.
#
# FMT_MODULE OFF fmt 12 turns its C++20 named-module library on
# BY DEFAULT whenever CMake >= 3.28, C++20, Ninja
# >= 1.11 and Clang >= 16 / GCC >= 15 / MSVC >= 19.34 are all
# in play -- which this project's setup is, on
# most machines. That adds a module-interface
# build nothing here uses, and needs module
# dependency scanning (clang-scan-deps for
# Clang, which e.g. Ubuntu's plain `clang`
# package doesn't include). Cached without FORCE
# so an explicit -DFMT_MODULE=ON still wins.
set(FMT_MODULE OFF CACHE BOOL
"Build fmt's C++20 named-module library (not used by this project)")
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
)
# core_farewell is an objects module (source/CoreFarewell): a piece of this
# library with a module of its own. Naming it here is all it takes for its
# object files to go into demo_core and its header to reach core_lib's users.
add_lib_module(core_lib TYPE SHARED OUTPUT_NAME "demo_core" DEPENDS fmt::fmt core_farewell)
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <string>
/// @file
/// Tiny "core" module shared by every executable module in this demo.
/// Registered as a library module in source/Core/CMakeLists.txt -- see
/// that file for how its build type (STATIC/SHARED) and its fmt
/// dependency are declared. The farewell half is a piece of it kept in
/// its own objects module, source/CoreFarewell (core/Farewell.hpp).
namespace demo::core
{
/// Builds a greeting for @p name.
///
/// @param name the person (or thing) being greeted.
/// @return a formatted greeting string, e.g. "Hello, Ale! (from demo_core)".
std::string makeGreeting(const std::string& name);
}
+13
View File
@@ -0,0 +1,13 @@
#include "core/Greeting.hpp"
#include <fmt/format.h>
namespace demo::core
{
std::string makeGreeting(const std::string& name)
{
return fmt::format("Hello, {}! (from {})", name, MODULE_NAME);
}
}
+7
View File
@@ -0,0 +1,7 @@
# An objects module: compiled to object files and nothing else -- no library of
# its own. source/Core names it in its DEPENDS, so the objects end up inside
# demo_core, and because that is a library, the header below is visible to
# everything that uses core_lib. OUTPUT_NAME is only the name MODULE_NAME
# carries: the farewell still says "(from demo_core)". fmt::fmt is declared in
# source/Core/CMakeLists.txt.
add_objects_module(core_farewell OUTPUT_NAME "demo_core" DEPENDS fmt::fmt)
@@ -0,0 +1,18 @@
#pragma once
#include <string>
/// @file
/// The farewell half of the "core" module. It is a piece of core_lib that is
/// a module of its own: an objects module (source/CoreFarewell/CMakeLists.txt),
/// compiled to object files that the library in source/Core takes in.
namespace demo::core
{
/// Builds a farewell for @p name.
///
/// @param name the person (or thing) being bid farewell.
/// @return a formatted farewell string, e.g. "Farewell, Ale. (from demo_core)".
std::string makeFarewell(const std::string& name);
}
+13
View File
@@ -0,0 +1,13 @@
#include "core/Farewell.hpp"
#include <fmt/format.h>
namespace demo::core
{
std::string makeFarewell(const std::string& name)
{
return fmt::format("Farewell, {}. (from {})", name, MODULE_NAME);
}
}
+2
View File
@@ -0,0 +1,2 @@
add_vendor_subdirectory(tiny_case)
add_exe_module(farewell_exe OUTPUT_NAME "farewell" DEPENDS core_lib tiny_case)
+14
View File
@@ -0,0 +1,14 @@
#include <iostream>
#include <string>
#include "core/Farewell.hpp"
#include "tiny_case.h"
// Executable module "farewell_exe" (see FarewellApp/CMakeLists.txt).
// Run it via: scripts/run.sh farewell_exe [name]
int main(int argc, char** argv)
{
const std::string name = (argc > 1) ? argv[1] : "World";
std::cout << demo::core::makeFarewell(tiny_case_to_upper(name)) << std::endl;
return 0;
}
+2
View File
@@ -0,0 +1,2 @@
add_vendor_header_only(tiny_ansi)
add_exe_module(hello_exe OUTPUT_NAME "hello" DEPENDS core_lib tiny_ansi)
+14
View File
@@ -0,0 +1,14 @@
#include <iostream>
#include <string>
#include "core/Greeting.hpp"
#include "tiny_ansi.h"
// Executable module "hello_exe" (see HelloApp/CMakeLists.txt).
// Run it via: scripts/run.sh hello_exe [name]
int main(int argc, char** argv)
{
const std::string name = (argc > 1) ? argv[1] : "World";
std::cout << tiny_ansi::green(demo::core::makeGreeting(name)) << std::endl;
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
// Vendored header-only helper (vendor/tiny_ansi/) -- a stand-in for the
// kind of tiny, buildless third-party header you'd wrap with
// add_vendor_header_only() instead of writing yourself.
#include <string>
namespace tiny_ansi {
inline std::string green(const std::string& s) {
return "\033[32m" + s + "\033[0m";
}
} // namespace tiny_ansi
+10
View File
@@ -0,0 +1,10 @@
# A stand-in for a "real" vendored third-party library that ships its own
# build -- a plain source drop, git submodule, or git subtree checked into
# vendor/tiny_case/. Added via add_vendor_subdirectory(tiny_case) in the
# root CMakeLists.txt, not discovered by the module system (vendor/ isn't
# one of the discovery roots), and not written to conform to this
# project's conventions -- it's whatever the upstream project's
# CMakeLists.txt looks like.
cmake_minimum_required(VERSION 3.16)
add_library(tiny_case STATIC src/tiny_case.cpp)
target_include_directories(tiny_case PUBLIC include)
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include <string>
std::string tiny_case_to_upper(std::string s);
+10
View File
@@ -0,0 +1,10 @@
#include "tiny_case.h"
#include <algorithm>
#include <cctype>
std::string tiny_case_to_upper(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::toupper(c); });
return s;
}