Add project files.

This commit is contained in:
alejo committed 2026-09-12 00:08:03 -03:00
commit 3087fcea8a
71 files changed
+8325

No files matched your search

+525
View File
@@ -0,0 +1,525 @@
# CMake module-system demo
A minimal C++20 project showing a per-module CMake setup: each module owns
a tiny, self-contained `CMakeLists.txt`, the root `CMakeLists.txt`
discovers modules automatically, three ways to bring in third-party code
(vendored, installed, or fetched) stay out of your warnings and your
compile database, every module gets its own browsable documentation via a
pinned Doxygen that's entirely opt-in (never required just to build),
Ninja itself is pinned and fetched the same way with a graceful fallback
if it's unavailable, and every output path is configured in one place.
## Layout
```
CMakeLists.txt project setup, options, install(), discovery -- edit by hand
Configuration.cmake all output-path configuration -- edit to relocate anything
cmake/BuildHelpers.cmake module macros + discovery mechanism -- edit rarely
cmake/Dependencies.cmake vendoring / installed-library / FetchContent helpers -- edit rarely
cmake/Documentation.cmake per-module Doxygen integration -- only ever LOOKS for a pinned Doxygen
cmake/DoxygenPin.cmake shared version/URL/hash parameters for the pinned Doxygen
cmake/FetchDoxygen.cmake standalone downloader (run via `cmake -P`, not during normal configure)
cmake/NinjaPin.cmake shared version/URL/hash parameters for the pinned Ninja
cmake/FetchNinja.cmake standalone downloader (run via `cmake -P`, not during normal configure)
cmake/HotReload.cmake hot_reload_enable_patchable_functions() -- see source/HotReload/README.md
source/Core/CMakeLists.txt library module "core_lib" (SHARED), depends on fmt
source/HelloApp/CMakeLists.txt executable module "hello_exe", depends on vendored tiny_ansi
source/FarewellApp/CMakeLists.txt executable module "farewell_exe", depends on vendored tiny_case
source/HotReload/CMakeLists.txt library module "hotreload_lib" (STATIC) -- see source/HotReload/README.md
source/examples/HotReloadDemo/ worked example: a host + two builds of one hot-reloadable plugin
source/examples/HotReloadLiveDemo/ interactive: a persistent host + one plugin file you edit yourself
vendor/tiny_ansi/ header-only vendored dependency (no build of its own)
vendor/tiny_case/ vendored dependency with its own CMakeLists.txt
scripts/setup.sh, setup.bat one-time: fetch pinned Ninja + Doxygen, check tooling, activate git hook
scripts/build.sh, build.bat configure + build -- prefers a pinned Ninja, falls back gracefully
scripts/run.sh, run.bat run a module by its CMake target name
scripts/install.sh, install.bat install this project's own artifacts only
scripts/docs.sh, docs.bat build documentation for every module (or one) -- needs setup.sh first
scripts/refactor.sh, refactor.bat auto-fix formatting/naming across this project's own code
.clang-format, .clang-tidy C++ style guide tooling (see "Code style & tooling" below)
.editorconfig, .gitattributes -- same
.gitignore Visual Studio template + a block auto-synced from Configuration.cmake
c-api/.clang-tidy naming override template for a C API subtree -- now also applied for
real at source/HotReload/src/c-api/ and source/HotReload/include/hot_reload/
.githooks/pre-commit formatting/lint pre-commit hook
cpp-style-guide.md, TOOLING.md the style guide itself + tooling docs
```
Everything generated -- the build tree, install output, docs, and reserved
packaging output -- lives under one `out/` directory (see "Output paths"
below); nothing under `out/` is source-controlled.
Each module's `CMakeLists.txt` is (conventionally) a couple of lines:
```cmake
# source/Core/CMakeLists.txt
add_dependency(fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 11.0.2
FIND_PACKAGE_ARGS NAMES fmt
)
add_lib_module(core_lib TYPE SHARED OUTPUT_NAME "demo_core" DEPENDS fmt::fmt)
```
A module declares its own dependencies in its own `CMakeLists.txt` -- same
"minimal tool intervention" principle as module registration: nothing to
add in the root file.
## Output paths (`Configuration.cmake`)
Every generated path is a cache variable, overridable with `-D<NAME>=...`
at configure time instead of editing any file:
| Variable | Default | What it controls |
|---|---|---|
| `OUT_DIR` | `out/` | Root of everything generated; override this to relocate all of the below at once |
| `INSTALL_OUTPUT_DIR` | `out/install` | Root of the `cmake --install` tree, one subdirectory per `DIST_TARGET` |
| `DIST_OUTPUT_DIR` | `out/dist` | Reserved for packaged/archived output (e.g. a future CPack integration) -- not populated by this project yet, but created and ready |
| `DOCS_OUTPUT_DIR` | `out/docs` | Root of generated Doxygen documentation, one subdirectory per module |
| `TOOLCACHE_DIR` | `.cache/tools` | Where downloaded, pinned build tools (currently Ninja and Doxygen) are cached -- kept outside `OUT_DIR` so `rm -rf out/` doesn't force a re-download |
| `COMPILE_COMMANDS_DESTINATION` | `compile_commands.json` | Where `compile_commands.json` is copied to after every build (see "compile_commands.json / clang tooling" below) |
| `RUNTIME_OUTPUT_SUBDIR` | `bin` | Subdirectory (inside whatever the build dir is) for executables/`.dll`/`.so` |
| `LIBRARY_OUTPUT_SUBDIR` | `bin` | Subdirectory for shared-library artifacts (kept alongside executables so `$ORIGIN` RPATH resolves them) |
| `ARCHIVE_OUTPUT_SUBDIR` | `lib` | Subdirectory for static/import-library artifacts |
The build directory itself (`CMAKE_BINARY_DIR`, where `CMakeCache.txt`
lives) isn't a `Configuration.cmake` variable -- it's fixed by whatever you
pass to `-B` when configuring. `scripts/build.sh`/`build.bat` default to
`out/build`, so the resulting layout is:
```
out/
build/ CMAKE_BINARY_DIR -- out/build/bin, out/build/lib during normal builds
install/<DIST_TARGET>/{bin,lib} what `cmake --install` / scripts/install.sh populates
dist/ reserved for future packaging output
docs/<module>/html/, docs/index.html generated documentation
```
Example -- build into a different location entirely:
```bash
cmake -G Ninja -S . -B /tmp/my-build -DOUT_DIR=/tmp/my-out
cmake --build /tmp/my-build
```
### `.gitignore` stays in sync automatically
`Configuration.cmake` is the single source of truth for every path above
-- `.gitignore` doesn't hardcode its own copy of them. Every configure,
a marked block near the bottom of `.gitignore` is regenerated from the
*current* value of each path variable:
```
# >>> Configuration.cmake generated paths -- BEGIN (do not edit by hand; edit Configuration.cmake and reconfigure) >>>
.cache/tools/
compile_commands.json
out/
out/dist/
out/docs/
out/install/
# <<< Configuration.cmake generated paths -- END <<<
```
Change `OUT_DIR` (or any of the others, permanently in `Configuration.cmake`
or per-configure with `-D...=...`) and this block updates to match on the
next configure -- nothing to keep in sync by hand. Everything outside the
block is untouched: `.gitignore` ships based on GitHub's
[`VisualStudio.gitignore`](https://github.com/github/gitignore/blob/main/VisualStudio.gitignore)
template, and that content is never rewritten. A path is only added if it
resolves *inside* the repo -- pointing `TOOLCACHE_DIR` somewhere else
entirely just omits it, since `.gitignore` has nothing useful to say about
a location outside the repository it lives in. The file is only rewritten
when the computed block actually differs from what's already there, so a
normal reconfigure doesn't spuriously touch its contents or mtime.
## Three ways to bring in a dependency (`cmake/Dependencies.cmake`)
| Helper | For | Example |
|---|---|---|
| `add_vendor_header_only(NAME [INCLUDE_DIR <dir>])` | A buildless header-only drop under `vendor/<NAME>/include` | `vendor/tiny_ansi/` |
| `add_vendor_subdirectory(SUBDIR)` | A vendored dependency with its own `CMakeLists.txt`, under `vendor/<SUBDIR>` | `vendor/tiny_case/` |
| `add_dependency(NAME <FetchContent_Declare args>)` | An installed package (`FIND_PACKAGE_ARGS ...`) with a fetch-from-source fallback | `fmt` in `source/Core/` |
All three are safe to call more than once for the same name (e.g. two
modules that need the same dependency) and mark the dependency's headers
`SYSTEM`, so `-Wall -Wextra -Wpedantic`/`/W4` -- which we only ever apply
per-target inside our own module macros, never globally -- doesn't fire on
code you don't control. Their targets are also dropped from
`compile_commands.json` (see below).
`add_dependency` is CMake's native find-then-fetch integration: it tries
`find_package()` first (an already-installed apt/vcpkg/conan copy) via
`FIND_PACKAGE_ARGS`, and only downloads + builds from source if nothing is
found. Omit `FIND_PACKAGE_ARGS` to always fetch. Requires network access
the first time a dependency isn't already installed.
A module `DEPENDS` on whatever target the chosen path defines (`fmt::fmt`,
`tiny_case`, `tiny_ansi`, ...) exactly like depending on another module --
`add_lib_module`/`add_exe_module` don't need to know or care which of the
three supplied it.
## Documentation (automatic per module, pinned Doxygen, entirely opt-in)
Every module registered via `add_lib_module`/`add_exe_module`/
`add_test_module`/`add_example_module` gets its own
[Doxygen](https://www.doxygen.nl/) documentation target for free -- nothing
to add to a module's own `CMakeLists.txt`. This project uses a pinned
Doxygen (currently **1.16.1**, see `DOXYGEN_PINNED_VERSION` in
`cmake/DoxygenPin.cmake`) -- it deliberately never looks for or uses a
system-installed `doxygen`, so every machine building this project
generates identical output regardless of what (if anything) is on `PATH`.
**Building the project never needs Doxygen.** `cmake/Documentation.cmake`
only ever *looks* for an already-fetched pinned Doxygen under
`.cache/tools/` (`TOOLCACHE_DIR`) -- it never downloads anything itself
during a normal configure, so `scripts/build.sh` has no Doxygen/network
dependency at all. Fetching it is a separate, explicit, one-time step:
```bash
scripts/setup.sh # see "Setup" below -- fetches the pinned Doxygen once
```
Until you've run that, `scripts/docs.sh` prints a clear message instead of
building anything -- everything else (the actual C++ project) builds
completely normally either way:
```
Documentation isn't set up yet (or ENABLE_DOXYGEN=OFF). Run scripts/setup.sh (or setup.bat) once to fetch the pinned Doxygen, then reconfigure.
```
Once set up:
```bash
scripts/docs.sh # build docs for every module, prints where to open it
scripts/docs.sh core_lib # build docs for just one module (docs_<target>)
```
```bat
scripts\docs.bat
scripts\docs.bat core_lib
```
Output goes to `out/docs/<module>/html/index.html` (a sibling of
`out/build/`, not nested inside it -- see "Output paths" above), plus a
landing page at `out/docs/index.html` linking to every module.
Documentation is **not** built as part of a normal `scripts/build.sh` --
it's opt-in, the same way tests and examples are.
**Writing docs for a module** is just normal Doxygen comments in that
module's headers -- e.g. `source/Core/include/core/Greeting.hpp`:
```cpp
/// Builds a greeting for @p name.
///
/// @param name the person (or thing) being greeted.
/// @return a formatted greeting string, e.g. "Hello, Ale! (from demo_core)".
std::string makeGreeting(const std::string& name);
```
Even with zero comments, a module's docs are still generated and useful --
file lists, function/class signatures, and (if [Graphviz](https://graphviz.org/)
is installed on your system -- this one enhancement, unlike Doxygen
itself, is still detected rather than pinned, since it's optional) call
graphs are produced regardless, since `DOXYGEN_EXTRACT_ALL` is on by
default. Comments just make it richer. Drop a `README.md` in a module's
own directory and it's automatically used as that module's Doxygen main
page.
**Configuring it** -- nothing is required, but everything is overridable:
```bash
# Disable entirely (docs targets print a message instead of building):
cmake -S . -B out/build -DENABLE_DOXYGEN=OFF
# Pin a different Doxygen version (also update the two SHA256 hashes in
# cmake/DoxygenPin.cmake to match that release's assets -- a stale hash
# fails scripts/setup.sh loudly, on purpose, rather than silently
# accepting an unexpected file -- then re-run scripts/setup.sh):
cmake -S . -B out/build -DDOXYGEN_PINNED_VERSION=1.17.0
# Any Doxyfile tag can be set as a CMake variable, e.g. stricter output:
cmake -S . -B out/build -DDOXYGEN_WARN_IF_UNDOCUMENTED=YES -DDOXYGEN_QUIET=NO
```
A single module can opt out with `NO_DOCS`:
```cmake
# some/internal/module/CMakeLists.txt
add_lib_module(internal_only TYPE STATIC NO_DOCS)
```
Vendored and fetched dependencies (`cmake/Dependencies.cmake`) are never
auto-documented -- same reasoning as their `compile_commands.json`
exclusion, it isn't your code.
## compile_commands.json / clang tooling
`CMAKE_EXPORT_COMPILE_COMMANDS` is on by default, and every build copies
`compile_commands.json` from the build directory to the project root so
clangd/clang-tidy/IWYU find it without extra configuration (CMake only
ever writes it into the build directory; most editors look for it at the
repo root). Vendored and fetched dependencies are excluded from it, so it
only reflects code this project actually owns. The project also builds
cleanly with clang directly:
```bash
cmake -G Ninja -S . -B out/build -DCMAKE_CXX_COMPILER=clang++ -DDIST_TARGET=linux-x64-clang
cmake --build out/build
```
## Code style & tooling
This project follows [`cpp-style-guide.md`](cpp-style-guide.md) (tabs,
Allman braces, `PascalCase` file names, `camelCase` functions/variables,
`m_`/`s_`/`g_` member/static/global prefixes -- see the file itself for
the full rules and rationale), enforced by the tooling described in
[`TOOLING.md`](TOOLING.md):
| File | Enforces |
|---|---|
| `.clang-format` | All formatting: tabs, Allman braces, pointer alignment, include order, line length |
| `.clang-tidy` | Naming conventions and a subset of language-feature rules, for this project's own C++ code |
| `c-api/.clang-tidy` | A template naming override (`snake_case`) for a C API subtree, if/when this project has one -- see below |
| `.editorconfig` | Baseline tabs/whitespace for editors that don't run clang-format live |
| `.gitattributes` | Forces LF line endings for source files on every OS |
| `.githooks/pre-commit` | Runs clang-format + clang-tidy on staged files before each commit; warns and skips (never blocks) if a tool isn't installed |
**Fixing violations automatically**, instead of hunting them down by hand:
```bash
scripts/refactor.sh # every file under source/ and c-api/ by default
scripts/refactor.sh source/Core # just one file or directory
```
```bat
scripts\refactor.bat
scripts\refactor.bat source\Core
```
Runs `clang-format -i` (always) and `clang-tidy --fix` (if
`compile_commands.json` exists -- build once first if it doesn't) across
the given scope. The pre-commit hook only *checks*; this is the "just fix
it" counterpart -- review the result with `git diff` afterward, same as
any auto-formatter. This is exactly how this project's own code
(`source/`) was brought into compliance when the toolkit was integrated --
see the renames noted further down.
With no argument, only `source/` and `c-api/` are ever scanned -- `out/`,
`.cache/`, `vendor/`, and `.git/` are never walked, so this can't
accidentally spend minutes reformatting/analyzing a fetched dependency's
entire source tree (verified against `out/build/_deps/fmt-src/`
specifically, since that's what triggered this exact problem before the
fix). Passing an explicit scope takes it as given: `refactor.sh` still
excludes `vendor`/`out`/`.cache`/`.git` defensively if your scope happens
to overlap with them; `refactor.bat` does not (its Windows batch
equivalent of that filter turned out to be unreliable to verify, so it
was removed rather than shipped uncertain -- pick a scope that doesn't
overlap with those directories, which is the normal case anyway, e.g.
`source\Core`).
**One-time setup per clone** to activate the hook -- `scripts/setup.sh` /
`setup.bat` does this for you (see "Setup" below) along with fetching the
pinned Doxygen and checking for clang-format/clang-tidy; the two commands
it runs for the hook specifically are:
```bash
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
```
**clang-tidy needs `compile_commands.json`** to resolve `#include`s --
this project already generates and copies it to the repo root on every
build (see above), so as long as you've built at least once, the hook's
clang-tidy check works with no extra setup.
Two adjustments made when integrating the toolkit into this specific
project (not part of the toolkit as provided -- see `TOOLING.md` in this
repo for the toolkit's own documentation of everything else):
- **`vendor/` is excluded** from the pre-commit hook's staged-file check
(`git diff ... ':!vendor/*'`). Vendored code (`cmake/Dependencies.cmake`)
isn't this project's code, so it isn't held to this style guide -- same
reasoning as its `compile_commands.json` and warning-flag exclusions
elsewhere in this project.
- **`.clang-tidy` gained one option**, `LocalConstantCase: camelBack`. The
style guide's own text says only *global/static* `const`/`constexpr`
values are "constants" for naming purposes -- everything else stays
under the `camelCase` Local Variables rule. Without this option, a plain
local `const` (e.g. `const std::string name = ...;` inside a function)
silently fell through to the general `PascalCase` constant rule instead,
which would have flagged completely ordinary code. Verified against a
small isolated test file: with the fix, a local `const` is accepted in
`camelCase` while a `static const`/`constexpr` still correctly requires
`PascalCase`.
`c-api/.clang-tidy` is included as a template, positioned exactly as the
toolkit ships it (`c-api/.clang-tidy`) -- this project doesn't currently
have a C API (Part II) surface. If you add one, move this file to that
subtree's root (`c-api/`, `public-include/`, wherever your `extern "C"`
headers live); `clang-tidy` always uses the closest `.clang-tidy` file up
the directory tree, so files under that directory automatically pick up
the C API's `snake_case` naming instead of this project's `camelCase`.
This project's own code was reformatted and renamed to comply when the
toolkit was integrated -- e.g. `Greeting.hpp`/`.cpp` (was `greeting.h`/
`.cpp`) and `makeGreeting`/`makeFarewell` (was `make_greeting`/
`make_farewell`) in `source/Core/`. `main.cpp` in each app module became
`Main.cpp` for the same reason (`PascalCase` file names) -- the style
guide doesn't call out an entry-point exception, so none was assumed;
rename back to lowercase if you'd rather treat that specific convention
as an exception.
## How module discovery works
The root `CMakeLists.txt` calls `_discover_module_subdirectories()` on
three roots, in this order:
| Root | What it holds |
|---|---|
| `source/*` | library AND app/executable modules, side by side |
| `source/tests/*` | test modules |
| `source/examples/*` | example modules |
(`source/tests` and `source/examples` aren't picked up by the first call
-- they have no `CMakeLists.txt` directly inside them, only their own
children do.)
Any immediate child directory that contains its own `CMakeLists.txt` gets
`add_subdirectory()`'d automatically (`vendor/` is not one of these roots
-- vendored dependencies are opted into explicitly by the modules that
need them, not auto-built by default). This uses `CONFIGURE_DEPENDS`, so
the *next build* reconfigures on its own after you add or remove a module
directory -- no manual `cmake` re-run needed. A module may `DEPENDS` on a
module discovered later in this list; order doesn't affect linking (CMake
resolves target names across the whole project at generate time --
verified, not assumed).
## Requirements
CMake >= 3.25, a C++20 compiler (GCC/Clang/MSVC), `git` (for
FetchContent's `GIT_REPOSITORY`-based dependencies like `fmt`), and
network access the first time you configure (for `fmt`, cached afterward
under `out/build/_deps`). Notably **not** in this list: Ninja and Doxygen
are both pinned and fetched automatically (see "Setup" below), not system
dependencies, and `scripts/build.sh` warns rather than fails if Ninja
isn't available at all (see "Build" below).
[Graphviz](https://graphviz.org/) is optional for Doxygen call graphs, and
[clang-format](https://clang.llvm.org/docs/ClangFormat.html)/
[clang-tidy](https://clang.llvm.org/extra/clang-tidy/) are optional for
the pre-commit hook and `scripts/refactor.sh` (see "Code style & tooling"
above) -- both are detected, not pinned, since they're editor/workflow
tools rather than part of the build itself. `scripts/setup.sh` (below)
checks for both and prints install guidance if either is missing.
## Setup
Optional, and separate from building (see "Requirements", "Build", and
"Documentation" above) -- run once per clone, or any time you want to
pick up a newer pinned version after bumping `NINJA_PINNED_VERSION` or
`DOXYGEN_PINNED_VERSION`:
```bash
scripts/setup.sh
```
```bat
scripts\setup.bat
```
Does four things, each independent of the others:
1. Fetches the pinned Ninja (`cmake/FetchNinja.cmake`) -- used by
`scripts/build.sh`/`build.bat` if present (see "Build" below); not
required, a system Ninja works fine too.
2. Fetches the pinned Doxygen (`cmake/FetchDoxygen.cmake`) -- needed for
`scripts/docs.sh`, not for building the project.
3. Checks for `clang-format`/`clang-tidy` on `PATH` and prints
OS-appropriate install guidance if either is missing (never
auto-installs -- that would need assumptions about your package
manager and elevated privileges this script shouldn't assume it has).
4. Activates the pre-commit hook for this clone, if it's a git repository
(`git config core.hooksPath .githooks`).
Safe to re-run any time -- each step is a no-op if there's nothing to do
(e.g. the pinned Ninja/Doxygen are already cached).
## Build
```bash
scripts/build.sh # Release (default)
scripts/build.sh Debug # or Debug / RelWithDebInfo / MinSizeRel
```
```bat
scripts\build.bat
scripts\build.bat Debug
```
Picks a generator in this order, each falling back to the next:
1. **A pinned Ninja**, if `scripts/setup.sh`/`setup.bat` has fetched one --
passed to CMake explicitly via `-DCMAKE_MAKE_PROGRAM=...` so it's used
even if a different Ninja also happens to be on `PATH` (same "always
the pinned copy" preference as Doxygen -- see "Documentation" above).
2. **A system Ninja**, if one is on `PATH` and no pinned copy was found.
3. **CMake's own default generator** for the platform, if neither Ninja is
available anywhere -- `scripts/build.sh`/`build.bat` print a warning
and continue rather than fail. (`CMakeLists.txt` separately warns too,
once CMake reports which generator it actually picked -- this is the
pre-existing "This project targets Ninja" message, unrelated to this
script.)
Verified concretely, not assumed: with neither a pinned nor a system Ninja
present anywhere, `scripts/build.sh` still successfully configures, builds,
and produces working binaries -- it just uses whatever CMake's default
generator is for the platform (Unix Makefiles on Linux, typically NMake or
a Visual Studio generator on Windows) instead.
## Run
```bash
scripts/run.sh hello_exe
scripts/run.sh farewell_exe -- Ale
scripts/run.sh # no args: lists available targets
```
```bat
scripts\run.bat hello_exe
scripts\run.bat farewell_exe -- Ale
```
`run.sh`/`run.bat` resolve the CMake **target id** (not the binary's
`OUTPUT_NAME`) to the actual binary via a manifest CMake generates at
configure time, so these scripts never need editing when a module is
added, removed, or renamed.
## Install
```bash
scripts/install.sh
```
```bat
scripts\install.bat
```
Always use the install script (or pass `--component demo` yourself if
calling `cmake --install` directly). A vendored/fetched dependency's own
`CMakeLists.txt` usually has `install()` rules of its own with no
component tag; installing without `--component` would also run those,
typically dumping files under `CMAKE_INSTALL_PREFIX` (e.g. `/usr/local`).
Tagging this project's own `install(TARGETS ...)` calls with `COMPONENT
demo` and always filtering on it is what keeps `cmake --install` scoped to
just this project's own artifacts.
## Adding a module
1. Create a directory under the right root (see the discovery table above).
2. Add a `CMakeLists.txt` inside it with one `add_lib_module` /
`add_exe_module` / `add_test_module` / `add_example_module` call, plus
any `add_vendor_*`/`add_dependency` calls it needs.
3. Add your `.cpp`/`.h` files inside that same directory.
4. Rebuild (`scripts/build.sh`) -- no other file needs touching.
```cmake
# source/MyTool/CMakeLists.txt
add_exe_module(my_tool_exe OUTPUT_NAME "my-tool" DEPENDS core_lib)
```
`scripts/run.sh my_tool_exe` picks it up automatically once it's built,
and `scripts/docs.sh my_tool_exe` documents it automatically too -- both
with zero edits anywhere outside `source/MyTool/`.