Replace CMake template readme by CLIForge's readme.
This commit is contained in:
1 parent
fe6a77de83
commit
0766b0b990
1 file changed
+266
-380
@@ -1,386 +1,272 @@
|
||||
# CMake module-system demo
|
||||
# CLIForge
|
||||
|
||||
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
|
||||
automatically the moment Doxygen is installed, and every output path is
|
||||
configured in one place.
|
||||
A header-only C++20 engine for building CLIs whose commands are ordinary
|
||||
free functions. Parameters and options are explicitly typed where
|
||||
they're declared; `.action()` validates (never invents) that the bound
|
||||
function's arguments are consistent with what was declared, and accepts
|
||||
them by value, by reference, or by const reference.
|
||||
|
||||
```cpp
|
||||
#include <cliforge/cliforge.hpp>
|
||||
using cliforge::Engine;
|
||||
|
||||
void greet(const std::string& name, bool loud) {
|
||||
std::cout << (loud ? "HELLO, " : "Hello, ") << name << (loud ? "!!" : ".") << "\n";
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
cliforge::Engine cli("greeter");
|
||||
cli.command()
|
||||
.keyword("greet")
|
||||
.parameter<std::string>("name", "Who to greet")
|
||||
.flag("loud", 'l', "SHOUT the greeting")
|
||||
.describe("Greets someone by name")
|
||||
.action(&greet);
|
||||
return cli.run(argc, argv);
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
$ greeter greet Ada --loud
|
||||
HELLO, Ada!!
|
||||
```
|
||||
|
||||
## Try it
|
||||
|
||||
```sh
|
||||
g++ -std=c++20 -Iinclude -O2 -o packctl examples/main.cpp
|
||||
./packctl # global help
|
||||
./packctl project myapp create --template cpp --git
|
||||
./packctl build 42 2 1 -a arm64 x64 -e vim # variadic params + variadic option
|
||||
./packctl build 7 -ndi -e nano # combined short flags
|
||||
./packctl deploy web1 --env prod web2 -n web3 # loose parameters, order-agnostic
|
||||
./packctl project new -h # ambiguous prefix -> shows all 3 matches
|
||||
./packctl projct myapp delete # typo -> "did you mean"
|
||||
```
|
||||
|
||||
Or with CMake: `cmake -B build && cmake --build build`, which produces
|
||||
`packctl` (the demo) plus `type_test` / `registration_errors_test`.
|
||||
|
||||
## The model
|
||||
|
||||
A command has exactly two parts, matching the spec:
|
||||
|
||||
```
|
||||
packctl deploy web1 --env prod web2 -n web3
|
||||
\____/ \___________ unstructured ___________/
|
||||
structured flags + options + loose parameters, any order
|
||||
```
|
||||
|
||||
* **Structured part** -- an ordered, *fixed-position* sequence of literal
|
||||
**keywords** (`project`, `delete`) and typed positional **parameters**
|
||||
(`<name>`). The user's tokens must line up with this sequence exactly,
|
||||
in order. Only the *last* parameter here may be variadic. Keywords
|
||||
carry no description of their own -- `.describe()` on the command is
|
||||
what shows up in help.
|
||||
* **Unstructured part** -- **flags** (boolean switches), **options**
|
||||
(named, typed values), and additional **loose parameters**, which the
|
||||
user may give in any order relative to each other:
|
||||
* Flags/options are found by name (`--foo`/`-f`) wherever they appear.
|
||||
Short flags combine (`-ndi` == `-n -d -i`); options can't be combined
|
||||
since they need a value.
|
||||
* Whatever bare (non-dash) tokens are left over are the loose
|
||||
parameters, matched *positionally among themselves, in declaration
|
||||
order* -- regardless of where they fall relative to the flags/
|
||||
options. `deploy web1 --env prod web2 -n web3` and
|
||||
`deploy -n web1 web2 --env prod web3` both give `hosts = [web1, web2,
|
||||
web3]`.
|
||||
|
||||
A command must declare at least one keyword. `.keyword()` can only be
|
||||
called before any `.flag()`/`.option()`. `.parameter<T>()` can be called
|
||||
on *either side* of that line: before the first `.flag()`/`.option()`
|
||||
it's a structured, fixed-position parameter; after, it's a loose one.
|
||||
Either way, only the last parameter declared in its half may be variadic.
|
||||
|
||||
**Parameters are always required** (unless they're the trailing variadic
|
||||
one, which accepts zero or more). **Options are always optional** --
|
||||
`.option<T>()` must be bound to `std::optional<T>` or (if variadic)
|
||||
`std::vector<T>` in `action()`; binding it to a bare, required `T` is a
|
||||
`RegistrationError`. This mirrors ordinary CLI convention: positional
|
||||
arguments are the required ones, named flags/options never are.
|
||||
|
||||
## Explicit types, validated (not invented) at action()
|
||||
|
||||
Every `.parameter<T>()` / `.option<T>()` call requires `T` up front --
|
||||
types are never inferred from the bound function. `.action(&fn)` deduces
|
||||
`fn`'s argument types via `function_traits` and *checks* each one against
|
||||
what was already declared, matched in order:
|
||||
|
||||
```
|
||||
[ parameters, in the order you called .parameter<T>() before any flag/option ]
|
||||
++
|
||||
[ flags/options/loose-parameters, in the order you called them after ]
|
||||
```
|
||||
|
||||
| Declared as | `action()`'s argument must be |
|
||||
|---|---|
|
||||
| `flag(...)` | `bool` |
|
||||
| `parameter<T>(...)` | `T` -- by value, `const T&`, or `T&` |
|
||||
| `parameter<T>(..., variadic=true)` | `std::vector<T>` |
|
||||
| `option<T>(...)` | `std::optional<T>` (never a bare `T` -- options are always optional) |
|
||||
| `option<T>(..., variadic=true)` | `std::vector<T>` |
|
||||
| an `enum class` (either kind) | needs `.choices<T>({{"name", T::Value}, ...})` called immediately after; `T` is deduced from the initializer list |
|
||||
|
||||
A mismatch anywhere in this table (wrong arity, a flag bound to
|
||||
non-`bool`, an option bound to a bare/required type, a declared type
|
||||
that doesn't match the corresponding argument type, a variadic slot
|
||||
bound to a non-vector, ...) is a `RegistrationError` at startup -- see
|
||||
`tests/registration_errors_test.cpp`. An unsupported scalar type (say,
|
||||
`std::map<int,int>`) fails immediately at the `parameter<T>()`/
|
||||
`option<T>()` call site via `static_assert`, before `action()` is even
|
||||
reached.
|
||||
|
||||
Arguments can be taken **by value, by reference, or by const
|
||||
reference** -- whichever avoids an unnecessary copy for the type in
|
||||
question (`const std::string&`, `const std::vector<T>&`, etc. all work).
|
||||
|
||||
## Types
|
||||
|
||||
Fixed-width signed/unsigned integers (`int8_t` ... `uint64_t`), `float`,
|
||||
`double`, `std::string`, `char` (exactly one byte), `bool`
|
||||
(`true/false/1/0/yes/no/on/off`, case-insensitive), and custom
|
||||
`enum class` types via `choices<T>()`. Parsing uses `std::from_chars`
|
||||
for numbers, so `"5abc"` or `"3.14"` handed to an `int32_t` slot is
|
||||
rejected rather than silently truncated.
|
||||
|
||||
## Errors are categorized, not just "something went wrong"
|
||||
|
||||
Every runtime parse failure is a `CliError` carrying both a specific,
|
||||
actionable message and a programmatic `ErrorKind` (`.kind()`):
|
||||
|
||||
| Situation | `ErrorKind` | Example message |
|
||||
|---|---|---|
|
||||
| A `--flag`/`-x` isn't recognized | `UnknownOption` | `unknown flag/option '--arch' for this command -- did you mean '--architectures'?` |
|
||||
| More positional values than declared slots can absorb | `TooManyArguments` | `too many arguments: unexpected 'extra' (and 2 more)` |
|
||||
| A required parameter (or loose parameter) was never given | `MissingArgument` | `missing required argument '<name>': expected a value of type string` |
|
||||
| An option was named but its value token is missing | `MissingValue` | `option '--editor' requires a value` |
|
||||
| A token doesn't fit its declared type | `TypeMismatch` | `type mismatch for 'threshold': got 'abc', expected int32` |
|
||||
|
||||
## `--help` resolves ambiguity instead of hiding it
|
||||
|
||||
`--help`/`-h` (anywhere in the command line) and the `help <words...>`
|
||||
top-level command both search for *every* command consistent with what
|
||||
was typed so far -- including when a positional value has already been
|
||||
filled in. `packctl project new -h` matches `project <name> create`,
|
||||
`project <name> delete`, and `project <name>` (info) all at once, since
|
||||
`new` is a perfectly plausible `<name>` for any of them; it prints all
|
||||
three, clearly separated, rather than silently picking one. A
|
||||
fully-specified prefix (`project myapp delete --help`) narrows to the
|
||||
one actual match, exactly as before.
|
||||
|
||||
The same treatment applies to an *incomplete* command that wasn't
|
||||
explicitly asking for help: `packctl project` alone is missing
|
||||
`<name>`, and that's equally true of all three `project` commands (they
|
||||
report the identical error). Rather than arbitrarily surfacing whichever
|
||||
one happened to be registered first, every command tied for "most
|
||||
specific incomplete match" is listed:
|
||||
|
||||
```
|
||||
$ packctl project
|
||||
Error: missing required argument '<name>': expected a value of type string
|
||||
'project' matches 3 commands:
|
||||
packctl project <name> create [OPTIONS] Scaffold a new project from a template
|
||||
packctl project <name> delete [OPTIONS] Permanently delete a project by name
|
||||
packctl project <name> [OPTIONS] Show information about a project
|
||||
```
|
||||
|
||||
A single most-specific match (e.g. `packctl project x create --template`,
|
||||
missing only `--template`'s value) still reports just the one relevant
|
||||
`Usage:` line, unchanged.
|
||||
|
||||
## Keeping allocations down
|
||||
|
||||
* `TypeInfo`'s parser is a **plain stateless function pointer**, not
|
||||
`std::function` -- built-in scalar types (the overwhelming majority of
|
||||
slots) cost nothing to store or copy. Only `choices<T>()`-restricted
|
||||
slots pay for an allocation, once, at registration (a `shared_ptr` to
|
||||
a small name table), not per parse.
|
||||
* **Short flag/option names are `char`**, not `std::string` -- no
|
||||
allocation to declare one, and combined short flags (`-ndi`) are
|
||||
matched by direct `char` comparison instead of constructing a
|
||||
temporary single-character string per letter.
|
||||
* The unstructured parser (flags/options/loose-parameters) works in
|
||||
terms of `std::string_view` internally -- splitting `--name=value` or
|
||||
stripping the leading dashes off a token no longer copies; the token's
|
||||
bytes are only ever copied once, into the final `Scalar`, when a value
|
||||
actually needs to be owned.
|
||||
* `choices<T>()` no longer goes through `std::any` -- the declared C++
|
||||
type is tracked with a `std::type_index` (a couple of pointer-sized
|
||||
comparisons, no allocation) and validated directly against `action()`'s
|
||||
argument types.
|
||||
|
||||
Parsing a single command line still does a handful of small, one-shot
|
||||
`std::vector` allocations (there's no way around owning the parsed
|
||||
values), so if you're embedding cliforge in a long-running REPL parsing
|
||||
many commands per process, reusing buffers across calls would be the
|
||||
next lever -- not implemented here, since it's not needed for the
|
||||
"parse argv once and dispatch" shape most CLIs have.
|
||||
|
||||
## Design decisions worth knowing about
|
||||
|
||||
These are the places where the spec was open to interpretation; here's
|
||||
what this implementation does and why.
|
||||
|
||||
* **Keywords never start with `-`.** The spec allows keywords to start
|
||||
with digits, but a leading dash would be indistinguishable from a
|
||||
flag. Internal dashes/dots/underscores are fine (`self-update`,
|
||||
`2fa`, `v2.load` are all valid keywords).
|
||||
* **Negative numbers vs. flags.** A token is only treated as "flag/
|
||||
option syntax" if it starts with `-` and the next character *isn't a
|
||||
digit* -- so `-5`/`-3` parse as negative numeric values (structured
|
||||
variadic parameters *or* loose parameters), while `-n` still means the
|
||||
flag `n`. Applied consistently in both the structured matcher and the
|
||||
unstructured parser.
|
||||
* **Disambiguating overlapping commands.** `project <name>` and
|
||||
`project <name> delete` can both structurally match
|
||||
`project myapp delete`. Candidates are ranked by how many *literal*
|
||||
keywords matched (more specific wins), with total structured-slot
|
||||
count as a tiebreaker, and are tried in that order -- so `delete`
|
||||
correctly wins over swallowing `"delete"` into `<name>`.
|
||||
* **Error attribution when several candidates fail.** If the
|
||||
best-ranked candidate's *value* fails to parse or is missing, that's
|
||||
almost certainly the user's intended command, so its error is what
|
||||
gets reported -- not a generic "no command matches", and not a
|
||||
less-relevant error from a lower-ranked candidate that also happened
|
||||
to fail.
|
||||
* **"Did you mean?" is structural, not string-blob.** Comparing the raw
|
||||
input against a usage string like `project <name> delete` would
|
||||
score badly once you substitute a real value for `<name>`. Instead,
|
||||
suggestions walk the same keyword/parameter structure as real
|
||||
matching, treating parameter slots as wildcards and only scoring
|
||||
edit-distance on the literal keyword text -- normalized to a ratio so
|
||||
a variadic parameter silently absorbing unrelated tokens can't make
|
||||
garbage input look like a close match. Flag/option suggestions
|
||||
additionally treat a prefix relationship (`--arch` for
|
||||
`--architectures`) as a strong match even though its raw edit
|
||||
distance is large.
|
||||
* **`--name=value` is supported** alongside the spec's space-separated
|
||||
form, as a low-cost, expected CLI convention.
|
||||
* **Repeated non-variadic options: last write wins** (`--reason a
|
||||
--reason b` yields `"b"`). Variadic options/loose-parameters
|
||||
accumulate instead.
|
||||
* **Not implemented:** a `--` "end of options" separator. Given the
|
||||
negative-number heuristic already resolves the main practical
|
||||
ambiguity, this was left as a documented extension point rather than
|
||||
added speculatively.
|
||||
|
||||
## 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 automatic per-module Doxygen integration -- edit rarely
|
||||
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
|
||||
vendor/tiny_ansi/ header-only vendored dependency (no build of its own)
|
||||
vendor/tiny_case/ vendored dependency with its own CMakeLists.txt
|
||||
scripts/build.sh, build.bat configure + build
|
||||
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)
|
||||
.clang-format, .clang-tidy C++ style guide tooling (see "Code style & tooling" below)
|
||||
.editorconfig, .gitattributes -- same
|
||||
c-api/.clang-tidy naming override template for a C API subtree, if you add one
|
||||
.githooks/pre-commit formatting/lint pre-commit hook
|
||||
cpp-style-guide.md, TOOLING.md the style guide itself + tooling docs
|
||||
include/cliforge/
|
||||
value.hpp Scalar/Value/TypeInfo/ErrorKind -- the type & error system
|
||||
slot.hpp Keyword/Parameter/Flag/Option descriptor
|
||||
function_traits.hpp Return/argument-type deduction for action()
|
||||
levenshtein.hpp Edit distance + prefix-aware closest-match helper
|
||||
command.hpp Builder, matcher, unstructured parser, help text
|
||||
engine.hpp Registration, ranking/dispatch, help resolution
|
||||
cliforge.hpp Aggregator -- the only header you include
|
||||
examples/main.cpp "packctl", a demo touching every feature
|
||||
tests/
|
||||
type_test.cpp every scalar type, valid + invalid input
|
||||
registration_errors_test.cpp every builder-misuse guard (+ positive cases)
|
||||
```
|
||||
|
||||
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 just Doxygen) are cached -- kept outside `OUT_DIR` so `rm -rf out/` doesn't force a re-download |
|
||||
| `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
|
||||
```
|
||||
|
||||
## 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)
|
||||
|
||||
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 automatically --
|
||||
nothing to add to a module's own `CMakeLists.txt`, and no Doxygen install
|
||||
required on your machine. This project downloads and manages its own
|
||||
pinned Doxygen (currently **1.16.1**, see `DOXYGEN_PINNED_VERSION` in
|
||||
`cmake/Documentation.cmake`) the first time you configure with
|
||||
`ENABLE_DOXYGEN` on (the default) -- 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`.
|
||||
The download is cached under `.cache/tools/` (`TOOLCACHE_DIR`, see
|
||||
"Output paths" above), so it only happens once per machine -- a clean
|
||||
`rm -rf out/` does not force a re-download.
|
||||
|
||||
```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, since generating it on
|
||||
every compile would be wasteful. If the pinned Doxygen can't be downloaded
|
||||
(no network access, an unsupported host platform, or you passed
|
||||
`-DENABLE_DOXYGEN=OFF`), `scripts/docs.sh` prints a clear message instead
|
||||
of failing -- everything else keeps building normally either way:
|
||||
|
||||
```
|
||||
Documentation is disabled (ENABLE_DOXYGEN=OFF, or Doxygen isn't installed). Install Doxygen and reconfigure to enable it.
|
||||
```
|
||||
|
||||
**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, even though Doxygen would otherwise download fine:
|
||||
cmake -S . -B out/build -DENABLE_DOXYGEN=OFF
|
||||
|
||||
# Pin a different Doxygen version (also update the two SHA256 hashes in
|
||||
# cmake/Documentation.cmake to match that release's assets -- a stale
|
||||
# hash fails the configure loudly, on purpose, rather than silently
|
||||
# accepting an unexpected file):
|
||||
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 |
|
||||
|
||||
**One-time setup per clone** to activate the hook:
|
||||
|
||||
```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, Ninja, 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 (also needed for the pinned
|
||||
Doxygen download -- both are cached afterward, see "Output paths" and
|
||||
"Documentation" above). Nothing else to install: Doxygen is downloaded
|
||||
and pinned automatically, not a system dependency.
|
||||
[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 (see "Code style & tooling" above) -- both are
|
||||
detected, not pinned, since they're editor/workflow tools rather than
|
||||
part of the build itself.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
scripts/build.sh # Release (default)
|
||||
scripts/build.sh Debug # or Debug / RelWithDebInfo / MinSizeRel
|
||||
```
|
||||
```bat
|
||||
scripts\build.bat
|
||||
scripts\build.bat Debug
|
||||
```
|
||||
|
||||
## 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/`.
|
||||
Reference in new issue
Block a user