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