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/whoseCMakeLists.txtis one or two lines long. The rootCMakeLists.txtis 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, andsource/,vendor/andout/are yours, at the root. Updating the template means replacingbreeze/. - 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 executableshello_exeandfarewell_exe, a vendored header-only dependency (tiny_ansi), a vendored CMake dependency (tiny_case) andfmt, 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 (
refactorand the pre-commit hook), cross compilers, MinGW-w64,qemu-userandwine(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 onPATHis 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
- In
project/Configuration.cmakesetPROJECT_TITLE,PROJECT_VERSION_NUMBER,PROJECT_SUMMARY,PROJECT_LANGUAGESandPROJECT_CXX_STANDARD(the arguments ofproject()and the C++ standard),PROJECT_CODE_NAME(it becomes thePROJECT_NAME_CODEcompile definition and the install component) andDEFAULT_BUILD_TYPE. - Delete the demo modules in
source/andvendor/and add your own. - Replace the root
README.mdwith the project's own (Doxygen uses it as the main page). - 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.cmakeprints-- A=valuelines.cmake [-DPMN_NAME=<preset|default>] [-DPMN_RECORD=ON] -P breeze/cmake/BuildDir.cmakeprints-- BUILD_DIR=…(or-- ERROR=…) and, withPMN_RECORD, remembers the choice inout/build/.last. A plaincmake -Pscript can include the settings file because it holds settings only: nothing in it needs a compiler or writes a file. Target selection and the.gitignoresync 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 movingOUT_DIRupdates it. The rest of the file is never touched.
Modules
- Discovery. Every
source/<dir>/,source/tests/<dir>/andsource/examples/<dir>/that contains aCMakeLists.txtis added, withCONFIGURE_DEPENDS, so adding or removing a module reconfigures by itself. - Macros (
breeze/cmake/BuildHelpers.cmake).add_lib_module(add_static_lib_moduleandadd_shared_lib_moduleare shorthands for itsTYPE),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 theOUTPUT_NAMEit was given),PROJECT_NAME,PROJECT_NAME_CODE,PROJECT_VERand, in Debug,DEBUG; warnings (/W4 /permissive-on MSVC,-Wall -Wextra -Wpedanticelsewhere); itsDEPENDSlinkedPRIVATE(objects modules, below, are the exception for libraries); an RPATH of$ORIGIN($ORIGIN/..for tests and examples), which becomes@loader_pathfor Mach-O in one place,_apply_rpath; and a Doxygen target unlessNO_DOCS. Project-wide: C++20 without extensions, position-independent code,CMAKE_BUILD_WITH_INSTALL_RPATHoff Windows. -
Objects modules.
add_objects_modulecompiles a directory to object files and stops there: no archive, no DLL, nothing inbin/orlib/. Name it in theDEPENDSof 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 owninclude/, 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_libabove getscore_mathtoo, becausecore_ioneeds 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 oncore_libseescore_math's andcore_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 insrc/. OUTPUT_NAMEis only whatMODULE_NAMEcarries, 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/CoreFarewellis the demo:core_libtakes it in, andfarewell_exefindscore/Farewell.hppthroughcore_libalone.
- Name the direct pieces only.
-
Manifest. Each configure writes
module_manifest-<CONFIG>.txtinto the build directory, onetarget=pathline per executable;breeze/scripts/runresolves 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_differentfails 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=ONlinks 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_NAMEand go toout/install/<DIST_TARGET>/{bin,lib}(tests and examples underbin/tests,lib/tests, … whenINSTALL_TESTS/INSTALL_EXAMPLES).breeze/scripts/installalways passes--component, which keeps theinstall()rules of fetched or vendored dependencies out; a barecmake --installwould run them too. -
Docs. A
docs_<target>target per module plus an aggregatedocs, built intoout/docs/<target>/html/without/docs/index.htmllinking them. Any Doxyfile tag can be set asDOXYGEN_<TAG>. Without a usable Doxygen, or withENABLE_DOXYGEN=OFF,breeze/scripts/docsdoes 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 withpmn_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_TARGETmust agree with the toolchain's. - Without a toolchain,
TargetSelection.cmakedetects the target from the compiler CMake found. Linux: the machine's architecture (HostArch.cmake) and the compiler ID. macOS:CMAKE_OSX_ARCHITECTURES, elseCMAKE_SYSTEM_PROCESSOR; several architectures (a universal binary) is an error. Windows: MSVC and clang-cl byCMAKE_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_TARGETis 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 rootCMakePresets.json). A hiddenbase(Ninja,out/build/${presetName}) and host-OS bases withconditions (base-linux,base-macos,base-windows,base-mingwfor 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 everytry_compileagrees; a missing compiler stops with the install hint. A compiler passed with-DCMAKE_CXX_COMPILERbeats the toolchain's; a strayCXXenvironment variable does not. A 32-bit userland on a 64-bit ARM kernel reportsaarch64, so it needs-DCMAKE_CXX_COMPILER=g++. - Cross mechanics (
pmn_cross_target):CMAKE_SYSTEM_NAME/PROCESSORare 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/winefor MinGW. It is stored as the cache entryCMAKE_CROSSCOMPILING_EMULATORonce 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/runand 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--sysrootand--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-sysrootreports 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
--targetis the triple-dumpmachinereports when it is the same architecture: ARM's own GCC isarm-none-linux-gnueabihf, and Clang finds its libraries only under that name; -print-prog-name=asreturning a bareasmeans GCC found no assembler for its target and will run the host's (invalid -march= option).
- a sysroot that
- 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_INITare 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 printsTarget sysroot: …or, when there is none,Cross build without a target sysroot …;breeze/scripts/build.shrepeats the explanation when such a configure fails on something not found..cache/sysroots/is git-ignored. - What it changes.
--sysrootfor GCC and Clang;find_package/find_librarysearch 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.pcfiles only (PKG_CONFIG_SYSROOT_DIR,PKG_CONFIG_LIBDIR, andPKG_CONFIG_PATHdropped); a Debian-styleusr/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.shsays 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
-devpackages 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 withsymlinks -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
armv7tarball andlinux-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>withqemu-user-staticinstalled.
- Copy it from the target, with its
- 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 thelinux-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) pickscl; 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- targetingclis reported with the fix). clang-cl picksclang-cland pins--target=<arch>-pc-windows-msvc. MSYS2 Clang (pmn_msys_clang_toolchain) picksclang++; 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, wherewineruns the result. - Checks. For the presets that link against MSVC's runtime libraries (msvc and clang-cl),
pmn_check_msvc_targetwarns once, before CMake's compiler test fails on it, when the prompt'sVSCMD_ARG_TGT_ARCHnames another target orVCToolsInstallDir\lib\<arch>\msvcrt.libdoes 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).
winehosts. On x64 Debian/Ubuntuwineneedswine64andwine32:i386(an i386 multiarch that setup offers to enable); on Arch it lives inmultilib, 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.shholds one row per tool: id, required or optional, label, what it is for and the package for each manager._tools.ps1holds 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=asand the installed<triplet>-*packages. - Consent. Third-party extras (today only the AUR toolchain below) are never answered by
--allor by an earliera: only--everythingor aytyped 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: nomapfile, no bash-4 expansions, empty arrays guarded forset -u, BSDsedandmktemp) and.bat._common.shholds what the.shscripts share: reading config, resolving the build directory, the Homebrew llvm lookup.build.batadditionally enters the compiler environment (Windows environments); the.shscripts assume you are in the one you need, which on Linux and macOS is every shell. - Which build.
--preset NAMEselectsout/build/NAME, otherwiseBuildDir.cmakepicks the directory recorded inout/build/.last, falling back todefault.buildrecords the directory it used. buildprefers the pinned Ninja, then one onPATH, then CMake's default generator with a warning.cmd.exesplits%1,%2, … on=,,and;, which would mangle-DNAME=value, sobuild.battakes the preset and build type off the front and forwards the rest from the unsplit%*.runfinds the executable inmodule_manifest-<CONFIG>.txt, readsCMAKE_CROSSCOMPILING_EMULATORfrom the build'sCMakeCache.txtand runs through it. Under wine it setsXDG_RUNTIME_DIRand a privateWINEPREFIXin.cache/wine/(WINEARCHfor the x86 and x64 targets);run.batruns natively.installrunscmake --install --component <PROJECT_CODE_NAME>intoout/install/<DIST_TARGET>.refactorruns clang-format, then clang-tidy with--fix, oversource/andc-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,makepkgand 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 realcmd.exeis its first test there.- Toolchain checks in
_common.cmake(includingpmn_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-MsvcComponentin_tools.ps1): written without PowerShell available; only bracket and quote balance were checked. - Target sysroots (
pmn_find_sysroot,pmn_use_sysroot, the explanationbreeze/scripts/build.shadds 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 withCould NOT find X11without 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/andproject/layout: the Linux side was exercised from the finished tree and from the zip: the presets through the root file'sinclude(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, andsetup,build,run,install,docs,refactor, thecmake -Pqueries and the pre-commit hook; also replacingbreeze/with a pristine copy, aproject/Configuration.cmakethat lacks a setting, and a sysroot added or removed over an existing build directory. The changes to the.batand.ps1scripts are the moved paths and thecdto 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.deffile thatWINDOWS_EXPORT_ALL_SYMBOLSproduces, 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 insource/Core, used bysource/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.