Movimiento de "Cuadrado" veloz en mapa blanco

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

No files matched your search

+825
View File
@@ -0,0 +1,825 @@
# CMakeTemplate — a generator-friendly CMake module system
A small template project (one shared library, two executables, one
vendored dependency, one fetched dependency) that demonstrates a CMake
setup where **adding, removing, or renaming a module never requires
editing the root `CMakeLists.txt`**, output paths and project identity
come from exactly one file, and Linux, macOS, Windows/MSVC, clang-cl,
and MinGW are all first-class targets -- each with its own toolchain
file and preset, built in its own directory -- across x64, ARM64, and
(on Linux) 32-bit ARM.
Copy the structure, delete the demo code, keep the machinery.
## Quick start
```bash
scripts/setup.sh # once: offer to install missing tools, fetch pinned Ninja + Doxygen, install the git hook
scripts/build.sh # configure (if needed) + build, Release by default
scripts/run.sh hello_exe World
```
```bat
scripts\setup.bat
scripts\build.bat
scripts\run.bat hello_exe World
```
`setup` starts by checking for the tools a build needs -- Git, CMake 3.25+
and a C++ compiler, plus optional extras like Clang and clang-format --
and asks, **one tool at a time**, whether to install each missing one,
showing the exact command first. Nothing is installed without your yes;
see [scripts/setup.sh / setup.bat](#scriptssetupsh--setupbat).
```
Hello, World! (from demo_core)
```
Pick a target explicitly with a preset, and switch freely -- every
preset builds in its own directory, so none of this ever invalidates
another build:
```bash
scripts/build.sh --list-presets # what this OS offers
scripts/build.sh --preset linux-x64-gcc
scripts/build.sh --preset linux-x64-clang # leaves the GCC build alone
scripts/run.sh hello_exe World # runs whichever you built last
scripts/run.sh --preset linux-x64-gcc hello_exe World
```
## What you get
- **One source of truth.** `Configuration.cmake` owns this project's
identity, target selection, and every output path. Nothing else —
not a `.cmake` module, not a shell script, not `.gitignore` —
hardcodes a second copy of a value it defines. See
[Configuration.cmake: the source of truth](#configurationcmake-the-source-of-truth).
- **Sixteen targets, one toolchain file each.** GCC and Clang on Linux,
each on x64, ARM64, and 32-bit ARM; Clang on macOS, on Apple Silicon
and Intel; MSVC, clang-cl, and GNU-style Clang on Windows, each on x64
and ARM64, plus both MinGW flavors (x86/x64) — see
[Supported targets](#supported-targets-dist_target).
Each has a toolchain file in `cmake/toolchains/` that picks the
compiler and declares the target, and where the target isn't the
machine you're on it sets up a real cross build: the ARM Linux targets
and both MinGW ones cross-compile from a Linux machine — see
[Toolchain files and cross-compiling](#toolchain-files-and-cross-compiling).
- **Presets that never step on each other.** Every preset builds in its
own `out/build/<preset>/`, so switching between GCC and Clang, or
between two architectures, never invalidates — or deletes — the
other's build. `scripts/run`, `install` and `docs` follow whichever
you built last, or take `--preset`.
- **Zero-edit module discovery.** Drop a directory with a one-line
`CMakeLists.txt` under `source/`, `source/tests/`, or
`source/examples/`; it's built, linked, installed, tested, and
documented without touching anything else. See
[Adding a module](#adding-a-module).
- **Self-managing tooling.** `scripts/setup.sh`/`setup.bat` fetch a
pinned Ninja and Doxygen (so every machine builds and documents
identically), wire up a pre-commit hook, and offer — one tool at a
time, with your say-so for each — to install whatever else is missing
(CMake, Git, a compiler, Clang, MinGW, ...) through your package
manager: apt, dnf, pacman, Homebrew or winget.
- **A build script that takes real arguments.** Pick a preset (which
selects the right toolchain), a configuration, and forward
arbitrary `-D...` flags to CMake — safely, on both bash and
`cmd.exe` — see [scripts/build.sh / build.bat](#scriptsbuildsh--buildbat).
## Project layout
```
Configuration.cmake Source of truth: identity, DIST_TARGET, output paths
CMakeLists.txt Root build: options, compiler settings, module
discovery, install rules -- rarely needs editing
CMakePresets.json One preset per supported DIST_TARGET, each
pointing at its toolchain file and its own
build directory -- for IDEs and
scripts/build.sh|.bat --preset
StyleGuide.md The C++ style guide .clang-format/.clang-tidy enforce
cmake/
BuildHelpers.cmake add_lib_module / add_exe_module / add_test_module /
add_example_module, and module discovery
Dependencies.cmake add_vendor_header_only / add_vendor_subdirectory /
add_dependency
Documentation.cmake Per-module Doxygen wiring
PrintConfig.cmake Lets scripts/*.sh|*.bat read Configuration.cmake
BuildDir.cmake Tells scripts/*.sh|*.bat which build directory
(preset) to work on -- see "Scripts"
HostArch.cmake x64/arm64/arm32 labels from a processor string
toolchains/ One toolchain file per DIST_TARGET, plus
_common.cmake with what they share
FetchNinja.cmake, NinjaPin.cmake Pinned Ninja download
FetchDoxygen.cmake, DoxygenPin.cmake Pinned Doxygen download
scripts/ setup, build, run, install, docs, refactor
-- .sh and .bat, one-to-one. _common.sh holds
what the .sh ones share; _tools.sh (Linux/macOS)
and _tools.ps1 (Windows) are the interactive tool
installers setup runs first
source/
Core/ core_lib -- a SHARED library module
HelloApp/, FarewellApp/ Executable modules depending on core_lib
tests/, examples/ Empty by default -- see Adding a module
vendor/
tiny_case/ A vendored dependency with its own CMakeLists.txt
tiny_ansi/ A vendored header-only dependency
c-api/.clang-tidy Stricter naming rules for a C-API subtree, if you add one
.githooks/pre-commit clang-format/clang-tidy check, installed by scripts/setup
```
## Configuration.cmake: the source of truth
Every value that used to (or could) drift into a second, hand-kept-in-sync
copy lives in `Configuration.cmake`:
- **Identity** — `PROJECT_CODE_NAME` (the `cmake --install --component`
name) and `DEFAULT_BUILD_TYPE`.
- **Target selection** — `DIST_TARGET`, and the `PLATFORM` /
`ARCHITECTURE` / `COMPILER_ID` metadata derived from it.
- **Output paths** — `OUT_DIR`, `BUILD_OUTPUT_DIR`, `INSTALL_OUTPUT_DIR`,
`DOCS_OUTPUT_DIR`, `TOOLCACHE_DIR`, and the `bin`/`lib` subdirectory
names.
Retargeting this template for your own project, or changing where
things land, means editing this one file — or passing `-D<NAME>=...` at
configure time — never hunting through `CMakeLists.txt`, a `cmake/*.cmake`
module, or a script for a hardcoded duplicate.
That includes the shell scripts, which run *before* any CMake configure
exists to ask a question of. `cmake/PrintConfig.cmake` is a tiny
`cmake -P` script that loads `Configuration.cmake` and prints one value;
every script that needs one calls it instead of hardcoding a default.
(`cmake/BuildDir.cmake` is its sibling for the one question that isn't a
single value: *which* build directory a script should work on.)
This is exactly the bug it replaces: `install.sh`/`install.bat` used to
each hardcode their own literal `"demo"`, with a comment warning it
*"must match `PROJECT_CODE_NAME` in `CMakeLists.txt`"* — the kind of
comment that's really begging for the two to eventually disagree. They
don't hardcode it anymore; they ask `Configuration.cmake`.
The one thing this deliberately can't do: a bare `cmake -P` script never
runs `project()`, so it never gets real compiler detection. Anything
in `Configuration.cmake` that depends on the compiler — `DIST_TARGET`
and everything derived from it — is skipped in this "query mode" and
fails loudly if a script asks for it anyway, rather than guessing. No
script currently needs to; `cmake --install` reads what a real configure
already baked into the build directory, not these variables directly.
## Supported targets (`DIST_TARGET`)
`DIST_TARGET` is a single `<platform>-<arch>-<compiler>` string that
picks the toolchain metadata (`PLATFORM`, `ARCHITECTURE`, `COMPILER_ID`)
used throughout the build and as the `cmake --install` destination
subdirectory (`out/install/<DIST_TARGET>/...`). Each value below has a
[toolchain file](#toolchain-files-and-cross-compiling) of the same name,
`cmake/toolchains/<DIST_TARGET>.cmake`, which declares it — and which
every preset selects. Without one (a bare `cmake -S . -B build`) it's
auto-detected from whatever compiler CMake finds:
| `DIST_TARGET` | Compiler |
|--------------------------|---------------------------------------------------------|
| `linux-x64-gcc` | GCC on x86-64 Linux |
| `linux-x64-clang` | Clang on x86-64 Linux |
| `linux-arm64-gcc` | GCC on 64-bit ARM Linux (aarch64) |
| `linux-arm64-clang` | Clang on 64-bit ARM Linux (aarch64) |
| `linux-arm32-gcc` | GCC on 32-bit ARM Linux (armhf/armv7l and similar) |
| `linux-arm32-clang` | Clang on 32-bit ARM Linux |
| `macos-arm64-clang` | Clang on Apple Silicon macOS (Apple Clang or LLVM's) |
| `macos-x64-clang` | Clang on Intel macOS (or Apple Silicon, via Rosetta 2) |
| `windows-x64-msvc` | cl.exe (MSVC) targeting x64 |
| `windows-arm64-msvc` | cl.exe (MSVC) targeting ARM64 |
| `windows-x64-clang-cl` | clang-cl targeting x64 (Clang targeting the MSVC ABI) |
| `windows-arm64-clang-cl` | clang-cl targeting ARM64 |
| `windows-x64-clang` | Clang, own GNU-style driver, x64 (e.g. MSYS2 CLANG64) |
| `windows-arm64-clang` | Clang, own GNU-style driver, ARM64 (e.g. MSYS2 CLANGARM64) |
| `windows-x86-mingw32` | 32-bit GCC via MinGW |
| `windows-x64-mingw64` | 64-bit GCC via MinGW-w64 |
No macOS GCC target and no universal one: Apple's own toolchain *is*
Clang, and a universal binary is two architectures at once, which a
`DIST_TARGET` (one architecture per name) can't express — see
[macOS](#macos) for how to get a universal file anyway.
No `windows-arm32-*` and no ARM64 MinGW target: 32-bit ARM Windows has
no real toolchain support left in any of the compilers above, and
mainline MinGW-w64's GCC has no ARM64 Windows (PE/COFF) backend — ARM64
Windows toolchains in that ecosystem are LLVM-based (MSYS2's
CLANGARM64, covered by `windows-arm64-clang` above), not GCC-based.
### Toolchain files and cross-compiling
`cmake/toolchains/` has one toolchain file per target above, named after
it, and every preset in `CMakePresets.json` selects the one with its own
name. CMake reads a toolchain file *before* it detects anything, so the
compiler and target are settled first instead of being discovered and
then cross-checked after the fact. Each file:
1. **declares its `DIST_TARGET`**, so the label and the compiler travel
together and can't be mismatched;
2. **picks the compiler** — and, for clang-cl, pins `--target=` (so
`windows-x64-clang-cl` means x64 on an ARM64 machine too);
3. **where the target isn't the machine running CMake, sets up a real
cross build**: `CMAKE_SYSTEM_NAME`/`CMAKE_SYSTEM_PROCESSOR`, package
and library lookup restricted to the target's sysroot (so a host
library is never linked into a foreign-architecture binary — fmt is
fetched and built *for the target* instead), and, if one is
installed, an emulator (`qemu-user` or `wine`) registered as
`CMAKE_CROSSCOMPILING_EMULATOR`, which is what lets `ctest` and
`scripts/run.sh` run what was just built.
| Target family | Host | What the toolchain does |
|---|---|---|
| `linux-*` | matching hardware (ARM64 for `linux-arm64-*`, …) | native build with the ordinary `g++` / `clang++` |
| `linux-*` | any other Linux machine | cross build: `<triplet>-g++` (`apt install g++-aarch64-linux-gnu`, `g++-arm-linux-gnueabihf`), or `clang++ --target=<triplet>` plus that same package for the target's libraries (or `-DCMAKE_SYSROOT=...`); `qemu-user` runs the result |
| `macos-*-clang` | any Mac | not a cross build in the Linux sense: Apple's clang is one multi-architecture compiler, so the architecture is just an `-arch` flag — pinned with `CMAKE_OSX_ARCHITECTURES`, along with a minimum macOS of 11.0. See [macOS](#macos) |
| `windows-*-mingw*` | Windows (MSYS2, MinGW-w64) | native build with `<triplet>-g++` |
| `windows-*-mingw*` | Linux | cross build with mingw-w64 (`apt install g++-mingw-w64-x86-64`, `g++-mingw-w64-i686`); `wine` runs the result |
| `windows-*-msvc` | Windows | picks `cl` and fails early, with the fix, if it isn't on `PATH`. **cl.exe's architecture comes from the Visual Studio developer prompt, not from CMake** — no toolchain file can switch it — so the architecture check below verifies you opened the right one |
| `windows-*-clang-cl` | Windows | picks `clang-cl` and pins its `--target` |
| `windows-*-clang` | Windows | picks `clang++`; the MSYS2 CLANG64 / CLANGARM64 shell is what sets its target |
Windows executables are only built for Windows hosts, except MinGW,
which also cross-builds from Linux; there is no cross-compiling *to*
MSVC or clang-cl from Linux, and no cross-compiling to or from macOS
beyond the two architectures the Mac's own compiler can already emit.
Two rules about overriding a toolchain, both deliberate: an explicit
`-DCMAKE_CXX_COMPILER=...` beats the toolchain's compiler (the
architecture and label checks below then catch an override that no longer
matches the target), while a stray `CXX` environment variable does *not*
— a target called `linux-x64-gcc` shouldn't be built with whatever
`CXX=clang++` happens to be exported. And a 32-bit ARM userland on 64-bit
ARM hardware reports `aarch64` from `uname -m`, so `linux-arm32-*` would
try to cross-compile there; pass `-DCMAKE_CXX_COMPILER=g++` in that one
case (the build is labeled `arm32`, correctly).
[`cmake/toolchains/_common.cmake`](cmake/toolchains/_common.cmake)
documents everything each helper does.
### macOS
`macos-arm64-clang` and `macos-x64-clang` build with Clang: Apple Clang
from the Xcode Command Line Tools (`xcode-select --install`, which
`scripts/setup.sh` offers to run), or LLVM's own Clang if you point
`-DCMAKE_CXX_COMPILER` at it (Homebrew's: `$(brew --prefix llvm)/bin/clang++`).
Both count as "clang" here. What is specific to the Mac:
- **The architecture is pinned, not inherited.** Apple's clang is a single
multi-architecture compiler, so building x86_64 on an Apple Silicon Mac
(or the reverse) is an `-arch` flag, not a cross compiler — no sysroot,
no emulator, `CMAKE_CROSSCOMPILING` stays off. The toolchain sets
`CMAKE_OSX_ARCHITECTURES` explicitly even when it equals the machine's,
because an x86_64 `cmake` running under Rosetta 2 on an Apple Silicon Mac
would otherwise quietly build x86_64 for a target named `arm64`. The
x64 build then runs on an Apple Silicon Mac through Rosetta 2, if
installed; the reverse (arm64 on Intel) can't run.
- **Minimum macOS is 11.0** (the first release for Apple Silicon), set with
`CMAKE_OSX_DEPLOYMENT_TARGET`. Unset, it would default to the SDK's own
version and everything you build would refuse to start on any older
macOS than yours. Pass `-DCMAKE_OSX_DEPLOYMENT_TARGET=...` to change it.
Bear in mind Apple's libc++ marks some newer C++ library features
(`std::format`, floating-point `std::to_chars`, `std::pmr`, ...) as
unavailable below the macOS that introduced them; this project's fmt
dependency doesn't use any.
- **Libraries load through `@rpath`.** Every RPATH in the build is written
`$ORIGIN` (the Linux spelling) and translated to `@loader_path` for
Mach-O in one place, `_apply_rpath` in `cmake/BuildHelpers.cmake`;
shared libraries get `@rpath/...` install names (CMake's default here).
The layout is the same as everywhere else: everything flat in `bin/`.
- **Universal binaries** aren't a target. Build both presets and combine
the results: `lipo -create out/install/macos-arm64-clang/bin/hello
out/install/macos-x64-clang/bin/hello -output hello`.
- **The shell is bash 3.2.** macOS still ships it as `/bin/bash`, so every
`.sh` script here is written for it (no `mapfile`, no bash-4 expansions,
empty arrays guarded for `set -u`) and BSD `sed`/`mktemp`.
- **Pinned Ninja and Doxygen work on macOS**: Ninja's universal binary
(one asset for both chips) and Doxygen's `mac-arm` / `mac-intel` zips,
chosen by the Mac's real architecture, Rosetta included.
- **Homebrew's llvm is keg-only** (installed but not on `PATH`), and it is
where `clang-tidy` comes from; `scripts/refactor.sh` and the pre-commit
hook look there after the normal `PATH`.
How this was verified, so you know what to trust: everything above was
run for real except the Apple toolchain itself. The macOS toolchain,
`Configuration.cmake`'s detection and the RPATH translation were exercised
by cross-compiling to Mach-O from Linux (with zig's clang as the compiler,
`CMAKE_SYSTEM_NAME=Darwin`) and reading the resulting load commands
(`LC_RPATH @loader_path`, `@rpath/libdemo_core.dylib`, arm64 and x86_64); the
scripts were run under a real bash 3.2 build; the pinned downloads were
fetched and hash-verified; the installer flows ran against a fake Mac. What
no Mac has run yet is Apple's own clang and SDK — if something is off
there, that is the place to look first.
### Architecture detection
Architecture is detected differently depending on compiler family,
because `CMAKE_SYSTEM_PROCESSOR` isn't trustworthy for all of them:
Linux (GCC/Clang) reads it through `cmake/HostArch.cmake` — `uname -m`
for a native build, and whatever the toolchain file set it to for a
cross build (so an aarch64 cross build from an x64 machine is labeled
`arm64`, not `x64`) — while macOS reads `CMAKE_OSX_ARCHITECTURES`, which
its toolchain files always set (see [macOS](#macos)), and Clang's
GNU-style driver on Windows uses a
direct environment-variable check instead of `CMAKE_SYSTEM_PROCESSOR`/
`CMAKE_HOST_SYSTEM_PROCESSOR`: those report the architecture of the
`cmake.exe` *process*, so a 32-bit `cmake.exe` running under WoW64 on an
ordinary 64-bit machine reports `x86` there, machine be damned. MSVC and
clang-cl have the equivalent problem one level down (the compiler, not
cmake.exe, is what can be cross-targeting) and use
`CMAKE_CXX_COMPILER_ARCHITECTURE_ID` instead — those two routinely
differ for a same-machine cross build (an x64 Visual Studio install's
"HostX64/ARM64" tools are the normal way to target ARM64 from an x64 dev
machine), and CMake tracks the resulting mismatch as a known issue
([#25466](https://gitlab.kitware.com/cmake/cmake/-/issues/25466)).
Auto-detection tells clang-cl apart from GNU-style Clang by
`CMAKE_CXX_COMPILER_FRONTEND_VARIANT` (`MSVC` vs. `GNU` — both report
`Clang` as the compiler ID), and mingw32 apart from mingw64 by pointer
size, not by parsing the compiler's own executable name. An
unrecognized compiler *or* architecture is a configure-time
`FATAL_ERROR` naming every valid value above, not a silent guess —
auto-detection guessing "must be MSVC" for anything it doesn't
recognize is exactly what used to misidentify a MinGW build as MSVC and
install it to the wrong directory.
**Two Windows gotchas worth knowing before you hit them:**
- *"targeting X86 (32-bit)" on a 64-bit machine.* `cl.exe`/`clang-cl`
found via `PATH` is the 32-bit-hosted, 32-bit-targeting one — usually
from opening a plain shell instead of an "x64 Native Tools Command
Prompt for VS" (or a Developer PowerShell defaulted to x64). The error
says this; open the right prompt, delete that preset's build directory
(`out/build/<preset>`), and reconfigure.
- *"No pinned ninja/Doxygen binary is configured for this host platform"
on an ordinary 64-bit machine.* Almost always a 32-bit `cmake.exe`
(an entirely normal, common download) running under WoW64 — see
`cmake/HostArch.cmake`'s header for why that fools a naive host-
architecture check. Either install a 64-bit CMake, or just install
ninja yourself and put it on `PATH` — `scripts/build.sh`/`build.bat`
fall back to a system ninja either way (see below).
**A bare `-DDIST_TARGET=<value>` labels the build; it doesn't select a
compiler.** CMake still uses whatever `CMAKE_CXX_COMPILER` it would
otherwise find — your `PATH`, your environment, or an explicit
`-DCMAKE_CXX_COMPILER=...`. Passing
`-DDIST_TARGET=windows-x64-mingw64` alone, with no MinGW compiler
actually configured, does not make CMake go find one — it configures
whatever compiler it would have anyway (often MSVC, since CMake looks
for that first on Windows) and would silently mislabel the result as
MinGW. It doesn't, though: an explicit `DIST_TARGET` that disagrees with
what the real compiler auto-detects to is a configure-time
`FATAL_ERROR`, naming both what you asked for and what CMake actually
found, plus the toolchain file and preset that fix it. The same check
also catches a toolchain whose compiler was overridden afterwards (an
explicit `-DCMAKE_CXX_COMPILER=...`, or a stale cache), and a
`-DDIST_TARGET` that contradicts the toolchain's own target.
The simplest way to avoid hitting that in the first place is a
`CMakePresets.json` preset, which selects the target's toolchain file —
compiler and `DIST_TARGET` together — for exactly this reason.
[`scripts/build.sh`/`build.bat`](#scriptsbuildsh--buildbat) take a
preset name directly:
```bash
scripts/build.sh --list-presets # see every preset for your OS
scripts/build.sh --preset windows-x64-mingw64
```
or use `cmake` directly — open the project in an IDE with CMake preset
support (CLion, VS Code, Visual Studio) and pick one from the target
dropdown, or:
```bash
cmake --list-presets
cmake --preset windows-x64-mingw64 # configure
cmake --build --preset windows-x64-mingw64 # build
```
Selecting a target without a preset still works — hand CMake the same
toolchain file a preset would, e.g.
`-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/windows-x64-mingw64.cmake` — or
run from an environment (an MSYS2 MINGW64 shell, a Visual Studio
Developer Prompt) where the right compiler is already the default and
auto-detection alone is enough. Either way, the compiler itself has to
already be installed and on `PATH` — nothing here installs MinGW,
clang-cl, or MSVC for you (a toolchain file that can't find its compiler
says which package or prompt provides it). What doesn't work: giving
*both* `--preset <name>` and an explicit `-DDIST_TARGET=...` to
`scripts/build.sh`/`build.bat` at the same time — a preset's toolchain
already declares `DIST_TARGET`, so a second value alongside it is always
either redundant or contradictory, and both scripts reject the
combination up front with a one-line fix, rather than leaving it to
whichever of the two CMake happens to prefer.
### Used as a dependency of another project
Short answer: the root (outer) project always decides, automatically —
nothing extra to configure. `CMakePresets.json` presets are a
command-line concept tied to whichever directory you point `cmake -S`/
`--preset` at; they're never implicitly consulted for a directory pulled
in as a dependency (`add_subdirectory()`, `FetchContent`) unless the
outer project's own `CMakePresets.json` explicitly `"include"`s this
one, which is the outer project's choice to make, not this template's.
So:
- If the root project sets `DIST_TARGET` itself (its own preset, a
plain `-DDIST_TARGET=...`, or one of this template's toolchain files,
which declare it), that is visible everywhere in the build — including inside this template
once it's a subdirectory — so this file's `if(NOT DIST_TARGET)` check
simply sees it already set and skips straight to cross-checking it
against the real compiler, exactly like a top-level build.
- If the root project does nothing, `DIST_TARGET` here still
auto-detects normally, from whatever compiler the whole build is
using (there's only one `CMAKE_CXX_COMPILER` per build, shared across
every subdirectory) — it just does so as a plain, directory-scoped
variable rather than a cache one, so it never leaks back up into the
root project's own variables.
One related thing this template does *not* handle when nested, since
it's out of scope for a project meant to be built standalone: `OUT_DIR`
and everything under it are defined from `CMAKE_SOURCE_DIR`, which is
always the *outermost* project's source directory, not this one's — so
if you vendor this template into a larger project, its `out/`,
`.gitignore` sync, etc. would target the parent's root, not its own
subdirectory. Retarget `OUT_DIR` (and friends) explicitly if you need
this template's output layout to stay self-contained in that scenario.
**MinGW runtime note:** executables built for `windows-x86-mingw32` /
`windows-x64-mingw64` statically link `libgcc`/`libstdc++`/`libwinpthread`
so they run standalone. `core_lib` (a `SHARED` target) can't do the
same — it would collide with the executable's own statically-linked
copy of the C++ unwinder (`multiple definition of _Unwind_Resume`) — so
it keeps the default dynamic runtime instead, and the build copies the
three runtime DLLs next to it automatically (both in the build tree and
at install time). You won't notice this unless you go looking for those
DLLs next to `libdemo_core.dll`; it's mentioned here in case you do.
## Scripts
Every script has a `.sh` and a `.bat` twin with identical behavior,
prints its own usage in a header comment, and supports `-h`/`--help`
for that same usage at the command line. All of them assume the
project root as the working directory but `cd` there themselves, so run
them from anywhere inside the repo.
### `scripts/setup.sh` / `setup.bat`
Run once per clone, and again whenever you like: it is safe to re-run. In
order:
1. **Checks the tools a build needs and offers to install what's missing —
one tool at a time.** For each missing tool it says what the tool is
for, shows the exact command it would run, and waits for you:
```
CMake 3.25 or newer -- configures and drives every build
Install CMake 3.25 or newer? [Y/n/a/q] y
$ sudo apt-get install -y cmake
```
`y` installs it, `n` skips this one, `a` installs everything still to
come, `s` skips the rest of the *optional* tools, `q` stops asking
(Enter takes the default: yes for a required tool, no for an optional
one). Nothing is installed without your yes. What it knows about:
| | Required | Optional |
|---|---|---|
| **Linux** | Git, CMake 3.25+, a C++ compiler (GCC) | Clang, clang-format, clang-tidy; on apt systems also the ARM cross compilers, MinGW-w64, `qemu-user` and `wine` for the cross builds |
| **macOS** | Xcode Command Line Tools, CMake 3.25+ (Homebrew is installed first if it's needed and missing) | clang-format, clang-tidy |
| **Windows** | Git, CMake 3.25+, and a compiler: Visual Studio Build Tools *or* MinGW-w64 via MSYS2 | LLVM (clang-cl, clang-format, clang-tidy), the other MSYS2 toolchains |
It uses your platform's package manager — `apt`, `dnf` or `pacman` on
Linux, Homebrew on macOS, `winget` on Windows — and tells you what to
install by hand if it doesn't recognize yours (cross-compilers and
emulators are offered on apt systems only, the layout the toolchain
files were written and tested against). A few specifics worth knowing:
an old distro CMake (Ubuntu 22.04 ships 3.22) is detected and the
alternatives — `pipx`, `snap`, the cmake.org tarball — are listed rather
than silently accepted; on macOS a `xcode-select --install` dialog is
waited for, and Homebrew is fetched with its own documented installer;
on Windows, `winget` doesn't put CMake or LLVM on `PATH`, so setup adds
their folders to this session (so the remaining steps work) and asks
whether to add them to your user `PATH` for good, and a fresh MSYS2 is
brought up to date — twice, as MSYS2 recommends — before MinGW-w64 goes
into it.
Flags: `--yes` installs the *required* tools without asking (for
scripts and CI), `--all` also the optional ones, `--check` only reports
and exits 1 if something required is missing, `--skip-tools` skips this
step. Without a terminal and without `--yes`/`--all` it asks nothing and
installs nothing, so a CI job can never hang on a prompt.
2. **Fetches a pinned Ninja and Doxygen** into `.cache/tools/` (via
`cmake/FetchNinja.cmake` / `FetchDoxygen.cmake` — skips anything
already fetched). Where upstream ships no binary for your machine
(Doxygen on ARM Linux/Windows; Ninja on 32-bit ARM Linux) or the
download fails, setup says so and carries on — it offers to install a
Ninja through your package manager instead, and the project builds fine
without Doxygen.
3. **Activates the pre-commit hook**: points `git config core.hooksPath`
at `.githooks/`, so the check below is active from your very first
commit (optional — the hook warns, rather than blocks, if
`clang-format`/`clang-tidy` aren't installed).
The Linux/macOS side lives in `scripts/_tools.sh` (a table of tools and
their package names per package manager, plus the prompting); on Windows
`setup.bat` hands the same job to `scripts/_tools.ps1`, because prompts,
`PATH` handling and `winget` are far more robust in PowerShell than in
batch. Both are internal helpers, not entry points.
### `scripts/build.sh` / `build.bat`
```
scripts/build.sh [--preset <name>] [BUILD_TYPE] [CMAKE_ARGS...]
scripts/build.sh --list-presets
scripts/build.sh --help
```
`--preset <name>` configures with a `CMakePresets.json` preset instead
of auto-detection — the preset selects that target's toolchain file,
which picks the compiler and declares `DIST_TARGET` together, so you
don't have to pass both by hand (see
[Supported targets](#supported-targets-dist_target) above for why that
pairing matters). `--list-presets` prints every preset for your OS and
exits. `--preset=<name>` also works; whichever form is used must come
first if given at all.
**One build directory per preset.** `--preset <name>` configures into
`out/build/<name>/`; without a preset it's `out/build/default/` (the
compiler is auto-detected there, so keep to one compiler per directory —
or use presets). CMake keeps a compiler in its cache, so a directory can
only ever belong to one compiler; giving each preset its own is what lets
you flip between GCC and Clang, or between x64 and a cross-built ARM64
target, with each build staying up to date, and it's what plain
`cmake --preset` and IDEs get too (each preset's `binaryDir` is
`out/build/${presetName}`). Earlier versions of this script kept a single
`out/build/` and deleted it whenever the preset changed — if you're
upgrading, `out/build/` may still hold that old build's files; delete it
once (`scripts/run` and friends say so when that's all they find). The
install directory follows the same naming: `out/build/<preset>` builds
into `out/install/<preset>`.
The build *type* is not part of the directory name, so flipping one
preset between `Debug` and `Release` reconfigures that preset's directory
(a full rebuild of it), as before. If you flip often, derive a preset —
in `CMakeUserPresets.json`, `{"name": "gcc-debug", "inherits":
"linux-x64-gcc", "cacheVariables": {"CMAKE_BUILD_TYPE": "Debug"}}` — which
inherits the toolchain and gets its own `out/build/gcc-debug/`. Set
`BUILD_DIR=<dir>` to use an exact directory of your own instead.
Three things are rejected up front, before `cmake` even runs, each with
that same preset list shown right there instead of a bare error:
`--preset` with no value after it; `--preset` followed by something
that's clearly another flag rather than a name; and — the ambiguity
case — `--preset <name>` combined with an explicit `-DDIST_TARGET=...`
later in `CMAKE_ARGS`, since the preset's toolchain already declares
`DIST_TARGET` and a second value alongside it is never useful.
`build.sh` additionally checks the preset *name* itself against
`--list-presets` before configuring; `build.bat` instead lets `cmake`
itself catch an unrecognized name and shows the preset list as a
follow-up to that failure — different mechanism, same result, a
deliberate choice given how much less predictable pre-validating a
quoted string is in `cmd.exe` (see the `=` trap below for a taste of
that general problem).
`BUILD_TYPE` (`Debug`/`Release`/`RelWithDebInfo`/`MinSizeRel`) is
optional and defaults to `Configuration.cmake`'s `DEFAULT_BUILD_TYPE`.
Anything else is forwarded to `cmake` verbatim at configure time:
```bash
scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
scripts/build.sh -DBUILD_TESTS=ON # BUILD_TYPE omitted
```
An argument is treated as `BUILD_TYPE` if it doesn't start with `-`;
otherwise there's no `BUILD_TYPE` override and every argument is a
CMake argument.
Prefers a pinned Ninja (from `setup.sh`) over a system one, and falls
back to CMake's platform default generator with a warning — never an
error — if neither is available; run `setup.sh` for the faster,
version-pinned option. (A preset already fixes the generator to Ninja,
so with `--preset` this only controls *which* `ninja` gets used, pinned
or system, not whether one is used at all — if neither exists, the
warning says so and the preset's own configure step is left to fail on
its own.)
**The `cmd.exe` `=` trap, and why `build.bat` avoids it:** `cmd.exe`
splits a batch file's own `%1`/`%2`/... parameters (and `shift`) on
space, comma, semicolon, *and* `=` — so a naively-written script reading
`-DBUILD_TESTS=ON` through `%1` sees it as two parameters, `-DBUILD_TESTS`
and `ON`, silently. `%*` (the raw, unsplit command line) doesn't have
this problem, and neither does `FOR /F`, which only splits on space/tab
by default — `build.bat` parses arguments (including `--preset`'s own
value) that way specifically so a `-D...=...` argument survives intact.
`build.sh`'s `"$@"`/`shift` never had this problem to begin with; this
paragraph is Windows-only trivia.
### `scripts/run.sh` / `run.bat`
```
scripts/run.sh [--preset <name>] <target-name> [-- program-args...]
```
Resolves a CMake target name to its built executable via the manifest
`_write_module_manifest()` generates at configure time
(`out/build/<preset>/module_manifest-<CONFIG>.txt`) — so this script
never needs updating when a module is added, removed, or renamed. Run
with no arguments to list every known target.
Which build it runs: `--preset <name>` picks one; without it, the build
`scripts/build` made last (or the only one that exists), and it says
which on stderr (`==> Running hello_exe (out/build/linux-x64-gcc)`) so a
program's own stdout stays clean to pipe. If several builds exist and none
was made through the scripts, it asks for `--preset` instead of guessing.
A cross build runs under the emulator its toolchain registered — `qemu-user`
for ARM Linux, `wine` for MinGW-from-Linux — when one was installed at
configure time; otherwise it can only run on the target itself, and the
error says so.
### `scripts/install.sh` / `install.bat`
```
scripts/install.sh [--preset <name>] [BUILD_TYPE]
```
Installs the build made with `--preset <name>` — by default the one made
last, as for `run` — to `out/install/<DIST_TARGET>/{bin,lib}` (the same
name as the `out/build/<preset>` it came from), always passing
`--component` (read from `Configuration.cmake`'s `PROJECT_CODE_NAME`)
so a vendored or fetched dependency's own, untagged `install()` rules
are never pulled in alongside this project's own artifacts.
### `scripts/docs.sh` / `docs.bat`
```
scripts/docs.sh [--preset <name>] [module-name]
```
Builds the `docs` target (every module) or `docs_<module-name>` (just
one) and prints the path to the generated landing page,
`out/docs/index.html`. The output is shared by every preset —
documentation doesn't depend on which compiler built the code — so
`--preset` (default: the build made last) only chooses which configured
build directory the docs targets run from. A no-op with a clear message — not an error —
if Doxygen hasn't been fetched yet or `ENABLE_DOXYGEN=OFF`.
### `scripts/refactor.sh` / `refactor.bat`
```
scripts/refactor.sh [path]
```
Runs `clang-format -i` and `clang-tidy --fix` across `source/` and
`c-api/` (or just the given path) — the "fix it" counterpart to the
pre-commit hook, which only checks. Review with `git diff` afterward,
like any auto-formatter.
## Adding a module
Every module — library, executable, test, or example — lives in its
own directory with a single-purpose `CMakeLists.txt`. The root
`CMakeLists.txt` discovers `source/*`, `source/tests/*`, and
`source/examples/*` automatically (`_discover_module_subdirectories()`
in `cmake/BuildHelpers.cmake`); nothing there needs editing.
```cmake
# source/MyLib/CMakeLists.txt
add_lib_module(my_lib
TYPE SHARED # or STATIC; defaults to BUILD_SHARED_LIBS
DEPENDS fmt::fmt
)
```
```cmake
# source/MyApp/CMakeLists.txt
add_exe_module(my_app DEPENDS my_lib)
```
```cmake
# source/tests/my_lib_test/CMakeLists.txt
add_test_module(my_lib_test DEPENDS my_lib) # ctest-registered, built with -DBUILD_TESTS=ON
```
```cmake
# source/examples/my_example/CMakeLists.txt
add_example_module(my_example DEPENDS my_lib) # built with -DBUILD_EXAMPLES=ON
```
All four glob their own sources (`src/*.cpp` + `include/*.h(pp)` for a
library; `*.cpp` + `include/*.h(pp)` for an executable, recursively
from the module's own directory), apply per-target warning flags
(`/W4 /permissive-` on MSVC-family compilers, `-Wall -Wextra -Wpedantic`
elsewhere), set up `$ORIGIN`-relative `RPATH` on Linux, register the
target for install and for `scripts/run.sh`, and get their own Doxygen
target for free. `OUTPUT_NAME`, `EXTRA_SOURCES`, and `NO_DOCS` are
available on all four when the defaults don't fit — see
`cmake/BuildHelpers.cmake` for the full option list.
## Adding a dependency
Three patterns, in `cmake/Dependencies.cmake`, covering vendored and
fetched code:
```cmake
# Header-only, vendored under vendor/<name>/include
add_vendor_header_only(tiny_ansi)
# Vendored, with its own CMakeLists.txt, under vendor/<subdir>
add_vendor_subdirectory(tiny_case)
# Fetched from a Git repo (or an already-installed copy that's new enough)
add_dependency(fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 12.1.0
GIT_SHALLOW TRUE
FIND_PACKAGE_ARGS 12 NAMES fmt
)
```
Always give `FIND_PACKAGE_ARGS` a minimum version (the `12`): without one,
*any* installed copy beats your pinned `GIT_TAG` — including a distro
package years older than the pin, which is how a "pinned" dependency
quietly stops being pinned. A dependency's own CMake options are set with
`set(<OPTION> <value> CACHE ...)` *before* the call;
[`source/Core/CMakeLists.txt`](source/Core/CMakeLists.txt) is the real,
commented instance of both, including two fmt-specific traps it
documents: why the pin is 12.1.0 rather than older (Clang ≥ 21 rejects
fmt < 11.1's own `FMT_STRING` macro) or newer (12.2.0 doesn't compile for
32-bit x86), and why `FMT_MODULE` is switched off (fmt 12 otherwise
builds a C++20 module library by default, which needs a module scanner
that not every Clang install has).
Then depend on whichever target the dependency defines, exactly like
depending on another module: `add_lib_module(my_lib DEPENDS fmt::fmt)`.
All three keep the dependency's warnings out of your build and its
sources out of `compile_commands.json`, and are safe to call more than
once for the same name if two modules need the same dependency.
## Documentation generation
Every module gets a Doxygen target automatically — nothing to add to
its own `CMakeLists.txt`. `scripts/docs.sh` / `docs.bat` build all of
them and print the landing page path; see
[scripts/docs.sh / docs.bat](#scriptsdocssh--docsbat) above. Doxygen
itself is fetched by `scripts/setup.sh` (pinned, not your system's, so
every machine's output matches) — documentation targets print a
pointer to that script instead of building until it has been run once. A
pinned Doxygen exists for x64 Linux, x64 Windows and macOS (Apple Silicon
and Intel); upstream publishes none for ARM Linux or ARM Windows, so
documentation isn't available on those machines (the project builds fine
without it).
Configure it with `-DENABLE_DOXYGEN=OFF`, `-DDOXYGEN_<TAG>=...` for any
[Doxyfile tag](https://www.doxygen.nl/manual/config.html), or opt a
single module out with `NO_DOCS` on its `add_*_module(...)` call. A
module's own `README.md`, if it has one, becomes that module's Doxygen
mainpage automatically.
## Code style & tooling
This project follows [`StyleGuide.md`](StyleGuide.md),
enforced by:
- **`.clang-format`** — formatting (indentation, brace style, line
length). Comments throughout cross-reference the style guide section
each rule comes from.
- **`.clang-tidy`** (and the stricter `c-api/.clang-tidy`, for a C-API
subtree if you add one) — naming conventions and a handful of
bug-prone patterns. Comments note what each check *can't* catch, so
those gaps stay visible instead of silently unenforced.
- **`.githooks/pre-commit`** — runs both tools against staged files
before every commit, installed automatically by `scripts/setup.sh` /
`setup.bat`. Neither tool is required to have this hook active: if a
binary isn't on `PATH`, the hook warns and skips that check instead
of blocking the commit. Only an actual formatting or lint issue from
a tool that *is* installed blocks anything.
- **`scripts/refactor.sh` / `refactor.bat`** — the auto-fix counterpart
to the hook's check-only behavior.
- **`.editorconfig`** / **`.gitattributes`** — editor-level whitespace
consistency and normalized line endings (`.bat` files are kept
`CRLF` deliberately; see the file for the rest).
## Output layout
Everything generated lands under `out/`, kept out of git by an
auto-synced block in `.gitignore` (regenerated from
`Configuration.cmake`'s actual path variables on every configure, so it
can't silently drift out of sync with what's really produced):
```
out/
build/<preset>/ One CMAKE_BINARY_DIR per preset (out/build/default/
when scripts/build.sh is run without one)
bin/ Executables, .dll/.so
lib/ Static libs, import libs
install/<DIST_TARGET>/{bin,lib} cmake --install destination -- named like the
out/build/<preset> it came from
docs/<module>/html/, index.html Generated Doxygen output
dist/ Reserved for packaged output (e.g. a future CPack integration)
.cache/tools/ Pinned Ninja + Doxygen (outside out/ on purpose --
`rm -rf out/` shouldn't force a re-download)
compile_commands.json Copied to the project root every build, for clangd/clang-tidy
```