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, 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.
scripts/setup.sh # once: fetch pinned Ninja + Doxygen, install the git hook
scripts/build.sh # configure (if needed) + build, Release by default
scripts/run.sh hello_exe Worldscripts\setup.bat
scripts\build.bat
scripts\run.bat hello_exe WorldHello, 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:
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- One source of truth.
Configuration.cmakeowns this project's identity, target selection, and every output path. Nothing else — not a.cmakemodule, not a shell script, not.gitignore— hardcodes a second copy of a value it defines. See Configuration.cmake: the source of truth. - Fourteen targets, one toolchain file each. GCC and Clang on Linux,
each on x64, ARM64, and 32-bit ARM; MSVC, clang-cl, and GNU-style
Clang on Windows, each on x64 and ARM64, plus both MinGW flavors
(x86/x64) — see Supported targets.
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. - 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,installanddocsfollow whichever you built last, or take--preset. - Zero-edit module discovery. Drop a directory with a one-line
CMakeLists.txtundersource/,source/tests/, orsource/examples/; it's built, linked, installed, tested, and documented without touching anything else. See Adding a module. - Self-managing tooling.
scripts/setup.sh/setup.batfetch a pinned Ninja and Doxygen (so every machine builds and documents identically) and wire up a pre-commit hook — no system-wide installs, no version drift. - 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 andcmd.exe— see scripts/build.sh / build.bat.
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)
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
Every value that used to (or could) drift into a second, hand-kept-in-sync
copy lives in Configuration.cmake:
- Identity —
PROJECT_CODE_NAME(thecmake --install --componentname) andDEFAULT_BUILD_TYPE. - Target selection —
DIST_TARGET, and thePLATFORM/ARCHITECTURE/COMPILER_IDmetadata derived from it. - Output paths —
OUT_DIR,BUILD_OUTPUT_DIR,INSTALL_OUTPUT_DIR,DOCS_OUTPUT_DIR,TOOLCACHE_DIR, and thebin/libsubdirectory 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.
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 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 |
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 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.
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:
- declares its
DIST_TARGET, so the label and the compiler travel together and can't be mismatched; - picks the compiler — and, for clang-cl, pins
--target=(sowindows-x64-clang-clmeans x64 on an ARM64 machine too); - 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-userorwine) registered asCMAKE_CROSSCOMPILING_EMULATOR, which is what letsctestandscripts/run.shrun 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 |
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.
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
documents everything each helper does.
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 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).
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-clfound viaPATHis 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 — seecmake/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 onPATH—scripts/build.sh/build.batfall 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 take a
preset name directly:
scripts/build.sh --list-presets # see every preset for your OS
scripts/build.sh --preset windows-x64-mingw64or 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:
cmake --list-presets
cmake --preset windows-x64-mingw64 # configure
cmake --build --preset windows-x64-mingw64 # buildSelecting 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.
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_TARGETitself (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'sif(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_TARGEThere still auto-detects normally, from whatever compiler the whole build is using (there's only oneCMAKE_CXX_COMPILERper 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.
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.
Run once per clone. Fetches a pinned Ninja and Doxygen into
.cache/tools/ (via cmake/FetchNinja.cmake / FetchDoxygen.cmake —
safe to re-run, and skips anything already fetched) and points
git config core.hooksPath at .githooks/, so the pre-commit check
below is active from your very first commit. Also prints whether
clang-format/clang-tidy are on PATH (optional — the hook warns,
rather than blocks, if they aren't installed).
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 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:
scripts/build.sh --preset windows-x64-mingw64 Debug -DBUILD_TESTS=ON
scripts/build.sh -DBUILD_TESTS=ON # BUILD_TYPE omittedAn 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 [--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 [--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 [--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 [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.
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.
# source/MyLib/CMakeLists.txt
add_lib_module(my_lib
TYPE SHARED # or STATIC; defaults to BUILD_SHARED_LIBS
DEPENDS fmt::fmt
)# source/MyApp/CMakeLists.txt
add_exe_module(my_app DEPENDS my_lib)# source/tests/my_lib_test/CMakeLists.txt
add_test_module(my_lib_test DEPENDS my_lib) # ctest-registered, built with -DBUILD_TESTS=ON# source/examples/my_example/CMakeLists.txt
add_example_module(my_example DEPENDS my_lib) # built with -DBUILD_EXAMPLES=ONAll 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.
Three patterns, in cmake/Dependencies.cmake, covering vendored and
fetched code:
# 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 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.
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 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.
Configure it with -DENABLE_DOXYGEN=OFF, -DDOXYGEN_<TAG>=... for any
Doxyfile tag, 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.
This project follows 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 stricterc-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 byscripts/setup.sh/setup.bat. Neither tool is required to have this hook active: if a binary isn't onPATH, 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 (.batfiles are keptCRLFdeliberately; see the file for the rest).
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