Files
2026-10-24 09:53:48 -03:00
..
2026-10-24 09:53:48 -03:00
2026-10-24 09:53:48 -03:00
2026-10-24 09:53:48 -03:00
2026-10-24 09:53:48 -03:00
2026-10-24 09:53:48 -03:00
2026-10-24 09:53:48 -03:00
2026-10-24 09:53:48 -03:00

CMakeTemplate

A C++20 project template for CMake 3.25+ that builds the same way on Linux, macOS and Windows.

  • Adding code means adding a folder. A module is a directory under source/ whose CMakeLists.txt is one or two lines long. The root CMakeLists.txt is never edited.
  • The template is one folder. breeze/ holds everything that makes it work: the build system, toolchains, scripts and these docs. project/ holds this project's own settings, and source/, vendor/ and out/ are yours, at the root. Updating the template means replacing breeze/.
  • Sixteen targets, one preset each. Linux (x64, arm64, arm32 × GCC, Clang), macOS (arm64, x64), Windows (x64, arm64 × MSVC, clang-cl, Clang; x64 and x86 MinGW-w64). Every target has its own toolchain file and its own build directory, so switching targets never rebuilds anything.
  • One setup script checks your machine and offers to install what is missing, one tool at a time, nothing without your yes.
  • A working demo to copy from: the shared library core_lib (with a piece of it, core_farewell, in an objects module of its own), the executables hello_exe and farewell_exe, a vendored header-only dependency (tiny_ansi), a vendored CMake dependency (tiny_case) and fmt, fetched from Git.

Part 1, using the template: Requirements · Quick start · Choosing a target · Everyday commands · Adding code · Settings · Making it yours · Updating the template · Troubleshooting

Part 2, how it works: Layout · The settings file · Modules · Dependencies · Targets and toolchains · Setup internals · Script internals · Style and tooling · Embedding · Verification status


Part 1: Using the template

Requirements

  • Required: Git, CMake 3.25 or newer, and a C++20 compiler (GCC, Clang, MSVC or MinGW-w64).
  • Optional, each only for what uses it: Clang, clang-format and clang-tidy (refactor and the pre-commit hook), cross compilers, MinGW-w64, qemu-user and wine (to run cross-built programs), Doxygen.
  • Fetched for you into .cache/tools/: Ninja 1.13.2 and Doxygen 1.16.1, pinned so every machine builds with the same versions. A Ninja already on PATH is the fallback; the project builds without Doxygen.
  • Setup installs tools through apt, dnf and pacman (Linux), Homebrew (macOS) and winget (Windows). On anything else it reports what is missing and leaves the installing to you.

Quick start

breeze/scripts/setup.sh                  # once per clone; asks before installing anything
breeze/scripts/build.sh                  # configure and build (Release)
breeze/scripts/run.sh hello_exe World    # Hello, World! (from demo_core)

On Windows use breeze\scripts\setup.bat, breeze\scripts\build.bat and breeze\scripts\run.bat hello_exe World, from cmd or PowerShell.

To build for a specific target, name its preset: breeze/scripts/build.sh --preset linux-x64-clang. run then follows the build you made last, or takes --preset too.

Choosing a target

A target is a preset named <os>-<arch>-<compiler>. breeze/scripts/build.sh --list-presets shows the ones your OS offers. Without --preset the first compiler CMake finds is used and the build goes to out/build/default/.

Presets Build Needs
linux-x64-gcc linux-x64-clang linux-arm64-gcc linux-arm64-clang linux-arm32-gcc linux-arm32-clang Linux on x86-64, AArch64, 32-bit ARM On matching hardware, just the compiler. Otherwise a cross compiler for that architecture; the Clang presets also need Clang.
macos-arm64-clang macos-x64-clang macOS on Apple Silicon, Intel Xcode Command Line Tools. Either Mac builds both; x64 programs run on Apple Silicon under Rosetta 2.
windows-x64-msvc windows-arm64-msvc Windows, MSVC Visual Studio or Build Tools with the C++ workload. ARM64 also needs the ARM64 build tools component; from an x64 machine that is a cross build (breeze\scripts\setup.bat offers to add it).
windows-x64-clang-cl windows-arm64-clang-cl Windows, clang-cl (MSVC ABI) The above plus clang-cl: LLVM, or Visual Studio's Clang component.
windows-x64-clang windows-arm64-clang Windows, MSYS2 Clang (MinGW ABI) MSYS2's CLANG64 (x64) or CLANGARM64 (ARM64) Clang package. MSYS2 has CLANGARM64 only on ARM64 Windows, so windows-arm64-clang needs such a machine; from x64 use the ARM64 MSVC or clang-cl presets.
windows-x64-mingw64 windows-x86-mingw32 Windows, MinGW-w64 GCC MSYS2 with MinGW-w64 GCC. Also cross-builds from Linux.

Cross builds. A Linux preset for another architecture compiles with the matching <triplet>-g++ (aarch64-linux-gnu, arm-linux-gnueabihf, x86_64-linux-gnu); the MinGW presets also cross-build from Linux. Setup installs these where your package manager has them (see What setup can install). If qemu-user (Linux targets) or wine (Windows targets) is installed when you configure, breeze/scripts/run and ctest use it to run the result.

Windows ARM64 from an x64 machine is a cross build too: windows-arm64-msvc and windows-arm64-clang-cl compile with Visual Studio's x64-hosted ARM64 tools (vcvarsall x64_arm64), which need the ARM64 build tools component, and the programs cannot run on the build machine.

Windows environments. MSVC and clang-cl only work inside a Visual Studio developer environment, and MSYS2's compilers only with their folder on PATH. breeze\scripts\build.bat enters the right one for the preset by itself, for that run only, so no "Native Tools Command Prompt" or MSYS2 shell is needed; PMN_NO_ENV=1 turns it off (details). Plain cmake --preset ... and IDEs don't do this: start them from the matching prompt.

Everyday commands

Every script exists as .sh and .bat with the same options, answers -h / --help, and works from any directory in the repository. --preset NAME (or --preset=NAME) must come first.

Script Usage Does
setup [--yes] [--all] [--everything] [--check] [--skip-tools] Checks and installs tools, fetches pinned Ninja and Doxygen, activates the pre-commit hook.
build [--preset NAME] [BUILD_TYPE] [CMAKE_ARGS…]
--list-presets
Configures and builds into out/build/<preset>. BUILD_TYPE is Debug, Release (default), RelWithDebInfo or MinSizeRel; further arguments go to CMake as they are, e.g. -DBUILD_TESTS=ON.
run [--preset NAME] TARGET [ARGS…] Runs an executable by CMake target name; without a target it lists them. Cross-built programs run under qemu or wine when available.
install [--preset NAME] [BUILD_TYPE] Installs this project's targets to out/install/<preset>/{bin,lib}.
docs [--preset NAME] [MODULE] Builds the Doxygen docs of every module, or one, into out/docs/.
refactor [PATH] Runs clang-format, then clang-tidy --fix (which needs a previous build for its compile database), over source/ and c-api/, or just PATH. Review with git diff.

run, install and docs act on the build made last unless you pass --preset; BUILD_DIR selects an exact directory instead. The build type is not part of the directory name, so changing it within a preset reconfigures that directory.

Setup flags. --yes installs the required tools without asking (for CI), --all the optional ones too, --everything also the third-party extras (today: Arch's prebuilt 32-bit ARM toolchain from the AUR), --check only reports and exits 1 if a required tool is missing, --skip-tools goes straight to the downloads. At each question: y installs, n skips that tool, a accepts everything left, s skips the remaining optional tools, q stops asking. With no terminal and no flag nothing is installed.

CMake directly.

cmake --preset linux-x64-gcc
cmake --build --preset linux-x64-gcc
ctest --test-dir out/build/linux-x64-gcc        # tests need -DBUILD_TESTS=ON

IDEs with preset support (CLion, VS Code, Visual Studio) list the presets for your OS; personal ones go in CMakeUserPresets.json. A bare -DDIST_TARGET=… only labels a build, it does not choose a compiler, and a label that disagrees with the compiler stops the configure: pick targets with presets.

Adding code

Create a directory with a CMakeLists.txt; the next build finds it.

# source/MyLib/CMakeLists.txt      library: sources in src/, headers in include/
add_lib_module(my_lib TYPE SHARED DEPENDS fmt::fmt)

# source/MyPiece/CMakeLists.txt    objects: compiled, no library of its own (a piece of one)
add_objects_module(my_piece)     # taken in by naming it: add_lib_module(my_lib DEPENDS my_piece)

# source/MyApp/CMakeLists.txt      executable: *.cpp in the module
add_exe_module(my_app DEPENDS my_lib)

# source/tests/my_test/CMakeLists.txt        built with -DBUILD_TESTS=ON, registered with CTest
add_test_module(my_test DEPENDS my_lib)

# source/examples/my_example/CMakeLists.txt  built with -DBUILD_EXAMPLES=ON
add_example_module(my_example DEPENDS my_lib)

Options: OUTPUT_NAME <file name> (for an objects module, which has no file, only the name MODULE_NAME carries), DEPENDS <targets>, EXTRA_SOURCES <files>, NO_DOCS. Third-party code comes in three ways, called from the CMakeLists of the module that needs it:

add_vendor_header_only(tiny_ansi)       # vendor/tiny_ansi/include, header-only
add_vendor_subdirectory(tiny_case)      # vendor/tiny_case/CMakeLists.txt
add_dependency(fmt                      # an installed copy if new enough, else fetched
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git  GIT_TAG 12.1.0
    FIND_PACKAGE_ARGS 12 NAMES fmt)

What one module declares, every module can use: source/CoreFarewell depends on the fmt::fmt that source/Core declares, whether fmt was fetched or an installed copy was found.

Every module also gets Doxygen docs (NO_DOCS opts out; a README.md in the module's directory becomes its main page).

Settings

Pass options to build as CMake arguments, e.g. breeze/scripts/build.sh -DBUILD_TESTS=ON.

Option Default Effect
BUILD_TESTS, BUILD_EXAMPLES OFF Build the modules under source/tests and source/examples.
INSTALL_TESTS, INSTALL_EXAMPLES OFF Also install them (implies the matching BUILD_ option).
BUILD_SHARED_LIBS OFF Library type for modules that don't say TYPE.
ENABLE_DOXYGEN ON Per-module documentation; -DDOXYGEN_<TAG>=… sets any Doxyfile tag.
PMN_MINGW_STATIC_RUNTIME OFF MinGW: link the GCC runtime into executables instead of shipping its DLLs. Unsafe when an executable uses a shared module that passes STL types across its interface.

The project's own settings (its name, version, languages and C++ standard, PROJECT_CODE_NAME, DEFAULT_BUILD_TYPE, OUT_DIR and the directories under it) live in project/Configuration.cmake; the cache variables among them can also be overridden with -D.

Making it yours

  1. In project/Configuration.cmake set PROJECT_TITLE, PROJECT_VERSION_NUMBER, PROJECT_SUMMARY, PROJECT_LANGUAGES and PROJECT_CXX_STANDARD (the arguments of project() and the C++ standard), PROJECT_CODE_NAME (it becomes the PROJECT_NAME_CODE compile definition and the install component) and DEFAULT_BUILD_TYPE.
  2. Delete the demo modules in source/ and vendor/ and add your own.
  3. Replace the root README.md with the project's own (Doxygen uses it as the main page).
  4. Run breeze/scripts/setup.

Nothing in breeze/ needs an edit. The root CMakeLists.txt and CMakePresets.json are yours to extend (project-wide CMake of your own, or presets that inherit from the template's) and need nothing to get started.

Updating the template

The template is the breeze/ folder and nothing else; no file of the project's lives in it. To update, replace the folder with the new version's and run breeze/scripts/setup again (it points git's core.hooksPath at breeze/githooks and re-checks your tools).

The project's (an update never touches them) The template's (replaced as a whole)
project/, source/, vendor/, c-api/; the root CMakeLists.txt, CMakePresets.json and README.md breeze/

Four kinds of file sit at the root, outside breeze/, because the tools that read them look there: the dot files (.clang-format, .clang-tidy and c-api/.clang-tidy, .editorconfig, .gitattributes) and .gitignore, whose marked block every configure rewrites. A new template version may change them: compare them with the new copies. A new version may also read a setting your project/Configuration.cmake doesn't have yet; the configure then stops and names it, and the new version's own project/Configuration.cmake shows its default.

With the breeze CLI the update is breeze sdk update (what is available), breeze sdk upgrade (the newest version of your major version) or breeze sdk migrate <version>: it replaces breeze/ for you (keeping the old one in .breeze/backup/), adds the settings the new version has, and moves a project in the old flat layout to this one. The SDK's version is breeze/VERSION, one line, and a release of the template is a tag (v1.2.0) whose breeze/VERSION says the same.

Moving a project that has the old flat layout (cmake/, scripts/ and Configuration.cmake at the root): put in breeze/, project/ and the new root CMakeLists.txt, CMakePresets.json and README.md, and delete the old cmake/, scripts/, .githooks/, StyleGuide.md and Configuration.cmake. Carry your values over into project/Configuration.cmake: from the old Configuration.cmake (PROJECT_CODE_NAME, DEFAULT_BUILD_TYPE, any output path you changed) and from the old CMakeLists.txt (the project() arguments and the C++ standard). Run breeze/scripts/setup once, which moves core.hooksPath from .githooks to breeze/githooks, and delete out/build/: the old build directories point at files that are gone.

Troubleshooting

Symptom Cause and fix
Windows: CMake says cl is missing or targets the wrong architecture CMake ran outside the right Visual Studio developer prompt. Use breeze\scripts\build.bat, or open the matching prompt, then delete the build directory.
Windows: a MinGW or -clang preset can't find its compiler MSYS2's toolchain folder isn't on PATH. breeze\scripts\build.bat adds it; set MSYS2_ROOT if MSYS2 isn't in <system drive>\msys64.
Windows: cannot open msvcrt.lib, could not open 'msvcrtd.lib', or an x64 cl used for an ARM64 preset The Visual Studio component for that target isn't installed. For ARM64 it is "MSVC … C++ ARM64/ARM64EC build tools (Latest)", which an x64 machine needs to cross-compile. breeze\scripts\setup.bat offers to add it, and breeze\scripts\build.bat stops with this explanation before CMake runs.
windows-arm64-clang on an x64 machine says it needs MSYS2's CLANGARM64 MSYS2 provides CLANGARM64 only on ARM64 Windows. Build ARM64 from x64 with windows-arm64-msvc or windows-arm64-clang-cl.
linux-arm32-* fails with invalid -march= option or Scrt1.o: file not recognized The cross compiler, binutils and C library come from different package sets. breeze/scripts/setup.sh reports this as [part]; on Arch x64 it replaces them with a matching prebuilt set.
A cross build stops with Could NOT find X11 (or OpenGL, ALSA, …), though the host has it Cross builds look only in the target's root, which has the C/C++ runtime and nothing else; the host's libraries are for the wrong architecture. Give the build a target sysroot: a root filesystem for the target with those libraries in .cache/sysroots/linux-<arch>, or -DCMAKE_SYSROOT=<dir>. See Cross builds and system libraries.
breeze/scripts/run can't run a cross-built program No emulator was installed when the build directory was configured. Install qemu-user or wine and run breeze/scripts/build again.
wine complains about its prefix Delete .cache/wine/; it is rebuilt on the next run.
"No pinned Ninja / Doxygen binary is configured for this machine" Pinned downloads exist for x64 and arm64 on Windows, Linux and macOS (Doxygen: x64 Linux and Windows, and macOS). A 32-bit cmake.exe on 64-bit Windows looks the same. Use 64-bit CMake, or put Ninja / Doxygen on PATH (-DDOXYGEN_EXECUTABLE=…).
DIST_TARGET mismatch error -DDIST_TARGET=… disagrees with the compiler CMake chose. Use --preset, or drop the option.
linux-arm32-gcc looks for a cross compiler on a 32-bit userland running on a 64-bit ARM kernel uname -m reports aarch64 there. Pass -DCMAKE_CXX_COMPILER=g++.
The pre-commit hook rejects a commit clang-format or clang-tidy found problems in staged C/C++ files. Run breeze/scripts/refactor and review the diff.

Part 2: How it works

Repository layout

CMakeLists.txt             the project's entry point: project(), then breeze/ (never edited)
CMakePresets.json          includes breeze/CMakePresets.json; the project's own presets go here
README.md                  the project's own
project/
  Configuration.cmake      the project's settings: identity, output paths. One source of truth
breeze/                    the template: replace this folder to update it
  README.md  StyleGuide.md
  VERSION                  the SDK's version, one line (the `breeze` CLI reads it)
  CMakePresets.json        16 configure + 16 build presets, plus hidden base presets
  cmake/
    Breeze.cmake           the build system: options, output dirs, module discovery, install rules
    TargetSelection.cmake  DIST_TARGET -> PLATFORM, ARCHITECTURE, COMPILER_ID
    GitignoreSync.cmake    keeps the generated block of .gitignore current
    BuildHelpers.cmake     module macros, output dirs, RPATH, runtime DLLs, manifest
    Dependencies.cmake     add_vendor_header_only, add_vendor_subdirectory, add_dependency
    Documentation.cmake    per-module Doxygen targets            (+ DoxygenPin, FetchDoxygen)
    NinjaPin.cmake         pinned Ninja                          (+ FetchNinja)
    HostArch.cmake         machine architecture -> x64 / arm64 / arm32
    PrintConfig.cmake      script mode: print project/Configuration.cmake values
    BuildDir.cmake         script mode: which build directory a script uses
    CopyRuntimeDlls.cmake  post-build copy of runtime DLLs
    toolchains/            one file per target, plus _common.cmake
  scripts/                 setup build run install docs refactor, each .sh + .bat
                           helpers: _common.sh  _tools.sh  _tools.ps1  _winenv.bat
  githooks/pre-commit      clang-format + clang-tidy on staged files
source/                    Core/ (core_lib)  CoreFarewell/ (core_farewell, objects)  HelloApp/  FarewellApp/
                           [tests/  examples/]
vendor/                    tiny_case/ (CMake project)  tiny_ansi/ (header-only)
c-api/                     .clang-tidy for a C API surface
.clang-format  .clang-tidy  .editorconfig  .gitattributes  .gitignore

Generated, git-ignored: out/ (build/<preset>/, install/<target>/, docs/, dist/ reserved) and .cache/ (tools/, wine/, sysroots/).

The settings file

project/Configuration.cmake defines the project's identity and every output path. The output settings are cache variables, so -D overrides any; the identity values are plain variables, because the root CMakeLists.txt passes them to project(). The scripts never duplicate a value: they read it through cmake -P helpers.

Variable Default Meaning
PROJECT_TITLE PROJECT_VERSION_NUMBER PROJECT_SUMMARY PROJECT_LANGUAGES DemoApp 0.1.0 … CXX The name, VERSION, DESCRIPTION and LANGUAGES of project().
PROJECT_CXX_STANDARD 20 CMAKE_CXX_STANDARD; the standard is required and compiler extensions are off.
PROJECT_CODE_NAME demo Compile definition PROJECT_NAME_CODE; the cmake --install component.
DEFAULT_BUILD_TYPE Release Used by CMake and the scripts when no type is given.
OUT_DIR <source>/out Root of everything generated.
BUILD_OUTPUT_DIR INSTALL_OUTPUT_DIR DOCS_OUTPUT_DIR DIST_OUTPUT_DIR OUT_DIR/build /install /docs /dist One build directory per preset, one install directory per DIST_TARGET; dist is reserved.
TOOLCACHE_DIR <source>/.cache/tools Pinned Ninja and Doxygen.
RUNTIME_OUTPUT_SUBDIR LIBRARY_OUTPUT_SUBDIR ARCHIVE_OUTPUT_SUBDIR bin bin lib Layout inside a build or install tree.
COMPILE_COMMANDS_DESTINATION <source>/compile_commands.json Copy of the compile database for clangd and clang-tidy, refreshed after each build.

DIST_TARGET, PLATFORM, ARCHITECTURE and COMPILER_ID are not settings: the template derives them (breeze/cmake/TargetSelection.cmake, see Target selection).

  • Script mode. cmake "-DPRINT_VARS=A;B" -P breeze/cmake/PrintConfig.cmake prints -- A=value lines. cmake [-DPMN_NAME=<preset|default>] [-DPMN_RECORD=ON] -P breeze/cmake/BuildDir.cmake prints -- BUILD_DIR=… (or -- ERROR=…) and, with PMN_RECORD, remembers the choice in out/build/.last. A plain cmake -P script can include the settings file because it holds settings only: nothing in it needs a compiler or writes a file. Target selection and the .gitignore sync are the template's, and only a real configure runs them.
  • .gitignore. A marked block is regenerated on every real configure from the output paths, so moving OUT_DIR updates it. The rest of the file is never touched.

Modules

  • Discovery. Every source/<dir>/, source/tests/<dir>/ and source/examples/<dir>/ that contains a CMakeLists.txt is added, with CONFIGURE_DEPENDS, so adding or removing a module reconfigures by itself.
  • Macros (breeze/cmake/BuildHelpers.cmake). add_lib_module (add_static_lib_module and add_shared_lib_module are shorthands for its TYPE), add_objects_module, add_exe_module, add_test_module, add_example_module.
Macro Sources (recursive globs) Output Notes
add_lib_module src/*.cpp, include/*.h(pp) shared: bin/, static: lib/ include/ is public. Without TYPE, BUILD_SHARED_LIBS decides.
add_objects_module src/*.cpp, include/*.h(pp) nothing of its own: the objects go into whatever names it in DEPENDS include/ is public. See "Objects modules" below.
add_exe_module *.cpp, include/*.h(pp) bin/
add_test_module same bin/tests/ Registered with CTest and all_tests; EXCLUDE_FROM_ALL unless BUILD_TESTS.
add_example_module same bin/examples/ Same, with BUILD_EXAMPLES.
  • What each module target gets. The compile definitions MODULE_NAME (its output name; for an objects module the OUTPUT_NAME it was given), PROJECT_NAME, PROJECT_NAME_CODE, PROJECT_VER and, in Debug, DEBUG; warnings (/W4 /permissive- on MSVC, -Wall -Wextra -Wpedantic elsewhere); its DEPENDS linked PRIVATE (objects modules, below, are the exception for libraries); an RPATH of $ORIGIN ($ORIGIN/.. for tests and examples), which becomes @loader_path for Mach-O in one place, _apply_rpath; and a Doxygen target unless NO_DOCS. Project-wide: C++20 without extensions, position-independent code, CMAKE_BUILD_WITH_INSTALL_RPATH off Windows.

  • Objects modules. add_objects_module compiles a directory to object files and stops there: no archive, no DLL, nothing in bin/ or lib/. Name it in the DEPENDS of a library, an executable, a test, an example or another objects module, and its objects become part of that target. That makes it the way to split a big library into pieces: each piece is a module of its own, with its own include/, and what remains of the library is a line naming them.

    # source/CoreMath/CMakeLists.txt
    add_objects_module(core_math OUTPUT_NAME "core")
    # source/CoreIo/CMakeLists.txt
    add_objects_module(core_io OUTPUT_NAME "core" DEPENDS core_math)
    # source/Core/CMakeLists.txt        the library: its own sources, plus the pieces
    add_lib_module(core_lib TYPE SHARED OUTPUT_NAME "core" DEPENDS core_io)
    
    • Name the direct pieces only. core_lib above gets core_math too, because core_io needs it: CMake links an object library's objects into the target that names it and no further, so the template follows the objects modules behind the ones you name (_resolve_object_modules, run once all modules are discovered, so directory order doesn't matter). Cycles stop the configure.
    • Headers. A piece's include/ is public, and a library passes its pieces' headers on: whoever depends on core_lib sees core_math's and core_io's headers. Those consumers must not name the pieces again, which would link a second copy of the objects into them (two copies of the library's state, when it is shared). An executable takes pieces privately. Keep a piece's private headers in src/.
    • OUTPUT_NAME is only what MODULE_NAME carries, so that the pieces of one library all identify as it. What a piece links itself, a static library for instance, is reached at the final link.
    • What they are not. Not installed, not run, not in the manifest; always position-independent, because they may end up in a shared library. Tests and examples can name a piece to test it without the rest of the library. source/CoreFarewell is the demo: core_lib takes it in, and farewell_exe finds core/Farewell.hpp through core_lib alone.
  • Manifest. Each configure writes module_manifest-<CONFIG>.txt into the build directory, one target=path line per executable; breeze/scripts/run resolves target names through it.

  • Runtime DLLs (Windows). Shared modules an executable depends on are copied next to it after the build (CopyRuntimeDlls.cmake: cmake -E copy_if_different fails on the empty list that $<TARGET_RUNTIME_DLLS:…> is for most targets). For MinGW the GCC runtime (libgcc_s_*-1.dll, libstdc++-6.dll, libwinpthread-1.dll) is located through the compiler and copied the same way, and installed beside the executables. PMN_MINGW_STATIC_RUNTIME=ON links executables statically instead; that is unsafe when they use a shared module whose interface passes STL types (separate heaps and unwinders), hence off by default.

  • Install. Targets are tagged with the component PROJECT_CODE_NAME and go to out/install/<DIST_TARGET>/{bin,lib} (tests and examples under bin/tests, lib/tests, … when INSTALL_TESTS / INSTALL_EXAMPLES). breeze/scripts/install always passes --component, which keeps the install() rules of fetched or vendored dependencies out; a bare cmake --install would run them too.

  • Docs. A docs_<target> target per module plus an aggregate docs, built into out/docs/<target>/html/ with out/docs/index.html linking them. Any Doxyfile tag can be set as DOXYGEN_<TAG>. Without a usable Doxygen, or with ENABLE_DOXYGEN=OFF, breeze/scripts/docs does nothing and says why; -DDOXYGEN_PINNED_VERSION=… replaces the pinned version.

Dependencies

breeze/cmake/Dependencies.cmake offers add_vendor_header_only(NAME [INCLUDE_DIR dir]) (an INTERFACE target over vendor/<name>/include), add_vendor_subdirectory(SUBDIR) (a source drop, submodule or subtree with its own CMakeLists.txt) and add_dependency(NAME <FetchContent_Declare args>) (find_package first when FIND_PACKAGE_ARGS is given, a fetch otherwise). All three keep the dependency's warnings out of your build (SYSTEM) and its targets out of compile_commands.json, and all are no-ops when repeated for the same name, so two modules may ask for the same dependency. The targets are usable from every module, wherever the call was made: an installed copy's imported targets are directory-scoped by default, so add_dependency makes them global for the call (CMAKE_FIND_PACKAGE_TARGETS_GLOBAL), and a sibling module's DEPENDS fmt::fmt finds them. Always give FIND_PACKAGE_ARGS a minimum version: without one any installed copy beats the pinned tag. A dependency's own options are set (set(<OPTION> … CACHE …)) before the call; source/Core/CMakeLists.txt is the commented example. In cross builds find_package only looks in the target's root, so a host's libfmt is never picked up -- and neither is anything the target's root lacks, which is what a target sysroot is for (see Cross builds and system libraries).

Targets and toolchains

Target selection

DIST_TARGET is the one label of a build, <platform>-<arch>-<compiler>; PLATFORM, ARCHITECTURE (x64, arm64, arm32, x86) and COMPILER_ID (gcc, clang, msvc, clang-cl, mingw64, mingw32) follow from it.

  • A toolchain file (breeze/cmake/toolchains/<target>.cmake, three lines each) declares it with pmn_toolchain_target(<target>) and calls one family macro: pmn_linux_toolchain(arch gcc|clang), pmn_macos_toolchain(arch), pmn_msvc_toolchain(arch), pmn_clang_cl_toolchain(arch), pmn_msys_clang_toolchain(), pmn_mingw_toolchain(arch). An explicit -DDIST_TARGET must agree with the toolchain's.
  • Without a toolchain, TargetSelection.cmake detects the target from the compiler CMake found. Linux: the machine's architecture (HostArch.cmake) and the compiler ID. macOS: CMAKE_OSX_ARCHITECTURES, else CMAKE_SYSTEM_PROCESSOR; several architectures (a universal binary) is an error. Windows: MSVC and clang-cl by CMAKE_CXX_COMPILER_ARCHITECTURE_ID, GNU-style Clang by the host architecture, GCC as MinGW by pointer size (there is no ARM MinGW target: MinGW-w64's GCC has no ARM64 Windows backend).
  • An explicit -DDIST_TARGET is cross-checked against that detection, and a disagreement stops the configure with the toolchain file or preset to use. An undetectable architecture also stops it, with the list of valid values.
  • Presets (breeze/CMakePresets.json, version 6, included by the root CMakePresets.json). A hidden base (Ninja, out/build/${presetName}) and host-OS bases with conditions (base-linux, base-macos, base-windows, base-mingw for Windows or Linux hosts) sit under 16 visible configure presets and 16 build presets with the same names. There are no test presets.

Linux

pmn_linux_toolchain decides by comparing the requested architecture with the machine's.

  • Native uses the ordinary g++ / clang++. Cross uses <triplet>-g++ / <triplet>-gcc, resolved to absolute paths so every try_compile agrees; a missing compiler stops with the install hint. A compiler passed with -DCMAKE_CXX_COMPILER beats the toolchain's; a stray CXX environment variable does not. A 32-bit userland on a 64-bit ARM kernel reports aarch64, so it needs -DCMAKE_CXX_COMPILER=g++.
  • Cross mechanics (pmn_cross_target): CMAKE_SYSTEM_NAME / PROCESSOR are set, programs come from the host, and libraries, headers and packages only from the target sysroot (if there is one, see below) and /usr/<triplet>.
  • Emulator (pmn_cross_emulator): qemu-<arch> -L <root> for Linux targets, wine64 / wine for MinGW. It is stored as the cache entry CMAKE_CROSSCOMPILING_EMULATOR once found, so an emulator installed later is picked up by the next configure and a stored choice stays until you change the entry or delete the build directory. breeze/scripts/run and CTest read it from there.
  • Clang cross builds use clang++ --target=<triple> with the cross g++'s libstdc++ and linker. When that g++ reports a real sysroot (dnf's COPR toolchains, ARM's own), Clang also gets --sysroot and --gcc-toolchain=/usr; flat layouts (Debian's /usr/<triplet>, pacman's) need neither.
  • Checks. The toolchain asks the installed cross g++ about itself instead of trusting a convention, because a cross compiler is only as good as the binutils and C library installed beside it. Each check is a warning with the likely cause, printed once, and configure carries on:
    • a sysroot that -print-sysroot reports but that isn't on disk is dropped (Clang would otherwise fall back to the host's crt files: Scrt1.o: file not recognized);
    • Clang's --target is the triple -dumpmachine reports when it is the same architecture: ARM's own GCC is arm-none-linux-gnueabihf, and Clang finds its libraries only under that name;
    • -print-prog-name=as returning a bare as means GCC found no assembler for its target and will run the host's (invalid -march= option).
  • Idempotence. CMake reads a toolchain file again for every scope that needs it (project(), each enabled language, try_compile), so everything here must be cheap and leave the same result when repeated; flags appended to *_FLAGS_INIT are added only if absent.

Cross builds and system libraries

A cross compiler package ships the target's C/C++ runtime and nothing else, and a cross build looks only in the target's own root, never at the host's libraries (they are for the wrong architecture). So a dependency that needs another system library -- raylib's GLFW wants X11, others OpenGL or ALSA -- stops with Could NOT find X11 even though the host has it. The fix is a target sysroot: a root filesystem for the target (usr/include, usr/lib, …) with those libraries installed.

  • Where it is looked for. -DCMAKE_SYSROOT=<dir> (also in a preset), else .cache/sysroots/linux-<arch> (linux-arm64, linux-arm32, linux-x64) if that directory exists: one per architecture, shared by the gcc and clang presets, so several cross presets build one after another with no options. The configure prints Target sysroot: … or, when there is none, Cross build without a target sysroot …; breeze/scripts/build.sh repeats the explanation when such a configure fails on something not found. .cache/sysroots/ is git-ignored.
  • What it changes. --sysroot for GCC and Clang; find_package/find_library search it before the compiler's own root; qemu runs the result against it (-L) when it holds the target's dynamic loader; pkg-config answers from its .pc files only (PKG_CONFIG_SYSROOT_DIR, PKG_CONFIG_LIBDIR, and PKG_CONFIG_PATH dropped); a Debian-style usr/lib/<triplet> layout is found too.
  • Before the first configure, or a fresh build directory after. A build directory keeps what CMake detected about the compiler the first time it was configured, so adding, changing or removing a sysroot afterwards stops the configure and names the directory to delete (rm -rf out/build/<preset>); breeze/scripts/build.sh says the same when a configure fails on a missing library.
  • Getting one. Whatever gives you the target's own libraries and headers:
    • Copy it from the target, with its -dev packages installed: rsync -aR pi@raspberrypi:/lib pi@raspberrypi:/usr/include pi@raspberrypi:/usr/lib .cache/sysroots/linux-arm64/ (absolute symlinks that come along can be made relative with symlinks -rc).
    • Arch host: the Arch Linux ARM root filesystem (archlinuxarm.org/about/downloads; check the signature listed there), with the packages installed through qemu. For 32-bit ARM use the armv7 tarball and linux-arm32.
      sudo pacman -S --needed qemu-user-static qemu-user-static-binfmt arch-install-scripts libarchive
      sudo systemctl restart systemd-binfmt
      mkdir -p .cache/sysroots/linux-arm64
      sudo bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C .cache/sysroots/linux-arm64
      sudo arch-chroot .cache/sysroots/linux-arm64 /bin/bash -c \
        'pacman-key --init && pacman-key --populate archlinuxarm && pacman -Syu --noconfirm libx11 libxrandr libxinerama libxcursor libxi mesa'
      
    • Debian or Ubuntu host: debootstrap --arch=arm64 --include=libx11-dev,libxrandr-dev,libxinerama-dev,libxcursor-dev,libxi-dev,libgl-dev <suite> .cache/sysroots/linux-arm64 <mirror> with qemu-user-static installed.
  • Or build natively. Run the build inside that same root filesystem under qemu (arch-chroot, chroot) or on the device itself: an aarch64 machine takes the linux-arm64-* presets as native builds, with no sysroot involved.
  • If linking picks the wrong C library. The sysroot's glibc is used, not the cross package's. A sysroot much older than the cross compiler's own glibc can fail to link; update the sysroot.

macOS

Apple's clang is one multi-architecture compiler, so building x86_64 on Apple Silicon, or the reverse, is an -arch flag, not a cross build: no sysroot, no emulator, CMAKE_CROSSCOMPILING stays off. pmn_macos_toolchain pins CMAKE_OSX_ARCHITECTURES explicitly, even when it equals the machine's (an x86_64 cmake under Rosetta would otherwise quietly build x86_64 for an arm64 target), and sets CMAKE_OSX_DEPLOYMENT_TARGET to 11.0 unless you pass another; Apple's libc++ marks newer library features (std::format, std::pmr, …) unavailable below the macOS that introduced them. Universal binaries are not a target: build both presets and lipo -create the results. Apple's clang (Command Line Tools) and LLVM's own, e.g. Homebrew's, both count as Clang; Homebrew's llvm is keg-only and is where clang-tidy comes from, so breeze/scripts/refactor.sh and the hook look in /opt/homebrew/opt/llvm/bin and /usr/local/opt/llvm/bin after PATH.

Windows

  • MSVC (pmn_msvc_toolchain) picks cl; its architecture comes from the developer environment it runs in, not from CMake, so the architecture check is what notices the wrong prompt (a 32-bit- targeting cl is reported with the fix). clang-cl picks clang-cl and pins --target=<arch>-pc-windows-msvc. MSYS2 Clang (pmn_msys_clang_toolchain) picks clang++; CLANG64 / CLANGARM64 decides its target. MinGW (pmn_mingw_toolchain) uses <triplet>-g++ (x86_64-w64-mingw32, i686-w64-mingw32), natively on Windows or as a cross build from Linux, where wine runs the result.
  • Checks. For the presets that link against MSVC's runtime libraries (msvc and clang-cl), pmn_check_msvc_target warns once, before CMake's compiler test fails on it, when the prompt's VSCMD_ARG_TGT_ARCH names another target or VCToolsInstallDir\lib\<arch>\msvcrt.lib does not exist, i.e. the target's Visual Studio component is not installed.
  • Runtime. MinGW executables use a dynamic GCC runtime whose DLLs are copied beside them (see Modules).
  • wine hosts. On x64 Debian/Ubuntu wine needs wine64 and wine32:i386 (an i386 multiarch that setup offers to enable); on Arch it lives in multilib, which setup offers to enable. Wine is not offered on ARM64 hosts.

Windows environments

breeze\scripts\build.bat calls breeze\scripts\_winenv.bat <preset> right before configuring. The environment lives only in that run (setlocal); the user's own cmake is put back in front of PATH afterwards, because Visual Studio's environment adds its own, often older, one.

Presets Environment entered
windows-x64-msvc windows-arm64-msvc vcvarsall.bat of the newest Visual Studio or Build Tools that has the target's component, found with vswhere (the x86/x64 tools, plus the ARM64 tools for arm64, so an older Visual Studio that has them beats a newer one that doesn't), with x64, x64_arm64, arm64 or arm64_x64 according to the target and the machine (x86_x64 and x86_arm64 on 32-bit Windows). It then checks that lib\<target>\msvcrt.lib exists under VCToolsInstallDir and stops with the component to add if not: with the target's component missing, vcvarsall still reports the target and leaves the host's x64 cl on PATH, so neither its exit status nor VSCMD_ARG_TGT_ARCH is proof.
windows-x64-clang-cl windows-arm64-clang-cl The same, then clang-cl on PATH if absent: Program Files\LLVM\bin, else Visual Studio's own copy (VC\Tools\Llvm\x64\bin, …\ARM64\bin, …\bin).
windows-x64-mingw64 windows-x86-mingw32 <MSYS2>\mingw64\bin or mingw32\bin first on PATH, unless the preset's compiler is already on it.
windows-x64-clang windows-arm64-clang <MSYS2>\clang64\bin or clangarm64\bin first on PATH, always: a clang++ from elsewhere targets another ABI. clangarm64 exists only on ARM64 Windows; on x64 the helper says so and points at the cross-building presets.

<MSYS2> is MSYS2_ROOT, else %SystemDrive%\msys64. Left alone: a shell that already is a developer prompt for the right target (VSCMD_ARG_TGT_ARCH and cl on PATH), presets it doesn't know, and everything when PMN_NO_ENV is set. vcvarsall is judged by what it leaves behind, not by its exit status; a failure prints its output and the likely fix. The helper is flat batch on purpose (single-line if and goto, no parenthesized blocks), because the paths it expands contain (x86).

Setup internals

breeze/scripts/setup.sh and setup.bat run four steps: check and offer the tools, fetch the pinned Ninja, fetch the pinned Doxygen, activate the pre-commit hook (git config core.hooksPath breeze/githooks). A step that cannot finish (no pinned binary for this machine, no network) says so and the others still run.

  • Catalog. _tools.sh holds one row per tool: id, required or optional, label, what it is for and the package for each manager. _tools.ps1 holds the Windows equivalent. For each missing tool setup says what it is for, shows the exact command and waits; required tools default to yes, optional ones to no.
  • Cross compilers must work, not merely exist. An AArch64, 32-bit ARM or x86-64 cross compiler counts as installed only if it compiles and links a small C++ program (which needs the target's assembler, headers, libstdc++, crt1.o and libc). One that can't is reported as [part] with the compiler's first error line and offered for repair. After an install the test runs again and, on a failure, prints -dumpmachine, -print-sysroot, -print-prog-name=as and the installed <triplet>-* packages.
  • Consent. Third-party extras (today only the AUR toolchain below) are never answered by --all or by an earlier a: only --everything or a y typed at their own question.

What setup can install

Tool apt dnf pacman Homebrew
Git, CMake git cmake git cmake git cmake git cmake
C++ compiler g++ gcc-c++ gcc Xcode Command Line Tools
Clang clang clang clang Apple's, from the tools
clang-format clang-format clang-tools-extra clang clang-format
clang-tidy clang-tidy clang-tools-extra clang llvm
AArch64 cross g++-aarch64-linux-gnu aarch64-linux-gnu-{binutils,gcc,glibc} (COPR) aarch64-linux-gnu-gcc (x64 hosts) —
32-bit ARM cross g++-arm-linux-gnueabihf arm-linux-gnueabihf-{binutils,gcc,glibc} (COPR) AUR, below (x64 hosts) —
x86-64 cross g++-x86-64-linux-gnu — — —
MinGW-w64 g++-mingw-w64-x86-64 g++-mingw-w64-i686 mingw64-gcc-c++ mingw32-gcc-c++ mingw-w64-gcc —
qemu-user qemu-user qemu-user qemu-user —
wine wine wine64 wine32:i386 wine wine (multilib) —
Ninja ninja-build ninja-build ninja ninja

Ninja is only offered where no pinned download exists (32-bit ARM Linux). Fedora's own cross GCC packages ship no libstdc++, so on dnf the ARM compilers come from lantw44's COPR, enabled only after a separate confirmation. On Windows, setup uses winget for Git, CMake, Visual Studio 2022 Build Tools (C++ workload), LLVM and MSYS2; adds the "MSVC ARM64 build tools" component (Microsoft.VisualStudio.Component.VC.Tools.ARM64) to an existing Visual Studio through the Visual Studio Installer's modify command, which asks for administrator rights; and uses MSYS2's pacman for mingw-w64-x86_64-gcc, mingw-w64-i686-gcc and mingw-w64-clang-x86_64-clang; it can add the tools' folders to the user PATH. On macOS it runs xcode-select --install and, if you agree, Homebrew's installer.

32-bit ARM on Arch has no official package. The AUR carries ARM's own prebuilt GNU toolchain (x86-64 hosts only) as four packages, arm-linux-gnueabihf-{binutils,glibc-headers,glibc,gcc}-bin. Setup builds each from its AUR repository with makepkg (sources are downloaded once into ~/.cache/pmn-aur-arm32/) and installs all four in one transaction, pacman -U --noconfirm --needed --ask 4. --ask 4 pre-answers yes to "X and Y are in conflict. Remove Y?" (pacman's default, and --noconfirm's, is no); that is how a source-built binutils or glibc is replaced, and it applies to any conflicting package in the transaction, which the prompt says. Leftover gcc-stage1 / gcc-stage2 packages are removed first, and nothing is removed until all four have built. Installing arm-linux-gnueabihf-gcc-bin alone through an AUR helper does not work: its dependencies are generic names (binutils, glibc, glibc-headers), which a helper resolves to the source-built packages (hours of compiling), leaving ARM's prebuilt GCC beside a binutils and glibc that don't fit it.

Script internals

  • Same options, two languages. Each script is .sh (bash 3.2-compatible, macOS's default shell: no mapfile, no bash-4 expansions, empty arrays guarded for set -u, BSD sed and mktemp) and .bat. _common.sh holds what the .sh scripts share: reading config, resolving the build directory, the Homebrew llvm lookup. build.bat additionally enters the compiler environment (Windows environments); the .sh scripts assume you are in the one you need, which on Linux and macOS is every shell.
  • Which build. --preset NAME selects out/build/NAME, otherwise BuildDir.cmake picks the directory recorded in out/build/.last, falling back to default. build records the directory it used.
  • build prefers the pinned Ninja, then one on PATH, then CMake's default generator with a warning. cmd.exe splits %1, %2, … on =, , and ;, which would mangle -DNAME=value, so build.bat takes the preset and build type off the front and forwards the rest from the unsplit %*.
  • run finds the executable in module_manifest-<CONFIG>.txt, reads CMAKE_CROSSCOMPILING_EMULATOR from the build's CMakeCache.txt and runs through it. Under wine it sets XDG_RUNTIME_DIR and a private WINEPREFIX in .cache/wine/ (WINEARCH for the x86 and x64 targets); run.bat runs natively.
  • install runs cmake --install --component <PROJECT_CODE_NAME> into out/install/<DIST_TARGET>.
  • refactor runs clang-format, then clang-tidy with --fix, over source/ and c-api/.

Style and tooling

StyleGuide.md has two parts: C++ (PascalCase and camelCase naming) and a C API surface (snake_case). Formatting (tabs, Allman braces, line length) comes from the single root .clang-format; naming from the root .clang-tidy, and c-api/.clang-tidy swaps in the C rules for files under c-api/. .editorconfig and .gitattributes keep editors and line endings in line (.bat and .ps1 stay CRLF). The pre-commit hook runs clang-format and clang-tidy on staged C/C++ files and skips with a warning when the tools are missing.

Embedding

Used through add_subdirectory, the root project decides: OUT_DIR derives from the outermost CMAKE_SOURCE_DIR, and its toolchain and presets apply.

Verification status

  • Linux and Windows presets, Arch x64: the 32-bit ARM presets build with the prebuilt AUR toolchain (reported by the maintainer). The Arch install and repair logic was also exercised against stand-in pacman, git, makepkg and cross-compiler programs: broken compiler, declined, --all, --everything, a failing build leaving the machine untouched.
  • _winenv.bat: exercised with an emulator of the batch subset it uses against a fake machine (every preset, 32-bit, x64 and ARM64 hosts, already-correct prompts, an x64 machine without the ARM64 tools, damaged installs, and each failure path). Its first run on a real cmd.exe is its first test there.
  • Toolchain checks in _common.cmake (including pmn_check_msvc_target): written without a CMake available to run them; the Arch 32-bit ARM configure was their first real run, and the Windows ones are untested.
  • Setup's ARM64 component row (Get-MsvcArm64Status, Install-MsvcComponent in _tools.ps1): written without PowerShell available; only bracket and quote balance were checked.
  • Target sysroots (pmn_find_sysroot, pmn_use_sysroot, the explanation breeze/scripts/build.sh adds to a failed cross configure): exercised on Linux x64 with CMake 3.28 and Ubuntu's cross compilers, against a stand-in sysroot (the cross libc, real X11 headers, and empty stub libraries compiled for the target), not against Arch Linux ARM or Raspberry Pi OS packages. arm64 and arm32, gcc and clang, found by directory and by -DCMAKE_SYSROOT, flat and Debian-style multiarch layouts, pkg-config kept to the sysroot, a sysroot without a loader, a missing one, a wrong one; a raylib 5.5 game built for arm64 stops with Could NOT find X11 without a sysroot and builds with one. The native x64 and MinGW presets were rebuilt unchanged. The recipes above for making a sysroot (rsync, Arch Linux ARM, debootstrap) have not been run, and neither a Mac nor Windows has run any of this; the new code is reached only by Linux cross builds.
  • The breeze/ and project/ layout: the Linux side was exercised from the finished tree and from the zip: the presets through the root file's include (and a project preset that inherits one), native x64 gcc, arm64 gcc and clang and arm32 gcc with and without a sysroot, MinGW from Linux, a raylib game for arm64, and setup, build, run, install, docs, refactor, the cmake -P queries and the pre-commit hook; also replacing breeze/ with a pristine copy, a project/Configuration.cmake that lacks a setting, and a sysroot added or removed over an existing build directory. The changes to the .bat and .ps1 scripts are the moved paths and the cd to the root, nothing else (each changed line was compared with the old file; CRLF endings are intact); nothing was run on Windows or on a Mac.
  • Objects modules: built and run on Linux with gcc and clang for x64, arm64 and arm32 (the ARM ones under qemu), and with MinGW from Linux (built and linked; the DLL exports the pieces' symbols, nothing was run). A scratch tree covered pieces three deep, pieces behind a static library, a static and a shared library, an executable, a test and an example taking pieces, consumers discovered before their pieces, a raw library name in DEPENDS, and a cycle. Each piece appears once in the archive and on the link line. Not run: MSVC and macOS. For MSVC, CMake's own generator source shows the absorbed objects feeding the .def file that WINDOWS_EXPORT_ALL_SYMBOLS produces, so the DLL should export them; that is a reading, not a run.
  • A dependency declared by one module and used by another (fmt::fmt, declared in source/Core, used by source/CoreFarewell): built with fmt 12.1.0 installed (static and shared) and not installed, with CMake 3.25.2, 3.27.9, 3.28.3, 3.31.6 and 4.1.2, plus a module that CMake reaches before the one that declares it. Cross builds ignore the host's copy and fetch one for the target.
  • macOS: the toolchain, TargetSelection.cmake's detection and the RPATH translation were exercised by cross-compiling to Mach-O from Linux and reading the load commands (LC_RPATH @loader_path, @rpath/libdemo_core.dylib, arm64 and x86_64); the scripts ran under a real bash 3.2. No Mac has run Apple's own clang and SDK yet, so look there first if something is off.