Skip to content

Repository files navigation

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, 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

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 World
scripts\setup.bat
scripts\build.bat
scripts\run.bat hello_exe World
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:

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.
  • 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, 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.
  • Self-managing tooling. scripts/setup.sh/setup.bat fetch 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 and cmd.exe — see scripts/build.sh / build.bat.

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)

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 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.

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
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 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 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-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 take a preset name directly:

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:

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. 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 / 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 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 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.

# 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=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:

# 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.

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 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.

Code style & tooling

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 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

Releases

Packages

Contributors

Languages