6981f55f28
canon sync rewrites the managed block of CMakeLists.txt from .canon.toml, for after either was edited by hand. It prints a unified diff of the change (also with --dry-run), writes only CMakeLists.txt so comments in .canon.toml survive, does nothing if the block already matches, and warns when the build will still fail because .canon.toml declares files that do not exist. doctor's hand-edited-block warning now points to it. - diff: a small line-based unified diff, no external tools - tests: a sync scenario (hand-edited manifest, --dry-run leaves the file untouched, sync from a subdirectory, no-op second run, hand edits in the block replaced); unit tests for plan_sync and unified_diff Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
186 lines
8.2 KiB
Markdown
186 lines
8.2 KiB
Markdown
# canon
|
|
|
|
Scaffolds C++ projects that follow [P1204R0](https://wg21.link/p1204r0)
|
|
(Canonical Project Structure), keeps adding to them, and checks them. The files
|
|
it generates come from templates you can replace. Written in
|
|
C++23, no third-party dependencies.
|
|
|
|
```bash
|
|
canon new lib libhello # libhello/hello/hello.{hpp,cpp,test.cpp} + tests/basics
|
|
canon new exe hello # hello/hello/hello.cpp
|
|
canon new exe hello --dir ~/src # create the project somewhere else
|
|
|
|
cd libhello
|
|
|
|
# Modules: related .hpp/.cpp/.test.cpp files
|
|
canon add module core # hello/core.hpp, core.cpp, core.test.cpp
|
|
canon add module details/utility # hello/details/utility.*, namespace hello::details
|
|
canon add module parser --no-test # no .test.cpp ...
|
|
canon add unit-test parser # ... until you want one
|
|
|
|
# Targets: more libraries or executables, each in its own source directory
|
|
canon add target exe hello-cli # hello-cli/hello-cli.cpp, links libhello
|
|
canon add module args --target hello-cli
|
|
canon add target lib libextra # extra/extra.{hpp,cpp,test.cpp}
|
|
|
|
# Functional tests
|
|
canon add test edge-cases # tests/edge-cases/driver.cpp, linked with libhello
|
|
canon add test greets --target hello-cli --arg Paul --expect "Hello, Paul!"
|
|
|
|
# Dependencies
|
|
canon add dep libextra # libhello links libextra
|
|
canon add dep fmt::fmt --target hello-cli # find_package(fmt) + link
|
|
canon add dep Boost::filesystem --package Boost # when the package name differs
|
|
|
|
# Checks
|
|
canon doctor # compare the files with .canon.toml and P1204R0
|
|
canon sync # rewrite CMakeLists.txt's managed block from .canon.toml
|
|
|
|
# Templates
|
|
canon templates # what each generated file is made from
|
|
canon templates export module.hpp # copy the built-in text to .canon/templates/ to edit
|
|
canon templates export --user # ... or all of them to ~/.config/canon/templates/
|
|
|
|
canon add module core --dry-run # any add command: show the changes, write nothing
|
|
```
|
|
|
|
`add`, `doctor`, `sync` and `templates` work from any directory inside the
|
|
project. Every generated project builds and passes its tests out of the box:
|
|
|
|
```bash
|
|
cmake -S . -B build
|
|
cmake --build build
|
|
ctest --test-dir build --output-on-failure
|
|
```
|
|
|
|
## Build canon
|
|
|
|
```bash
|
|
cmake -S . -B build
|
|
cmake --build build
|
|
ctest --test-dir build --output-on-failure
|
|
cmake --install build --prefix ~/.local # puts canon in ~/.local/bin
|
|
```
|
|
|
|
## How it works
|
|
|
|
Generators never touch the filesystem. They return a **plan**: a list of
|
|
`make_directory`, `create_file` and `update_file` operations. The **executor**
|
|
checks every operation before writing anything, so a conflict writes nothing and
|
|
`--dry-run` just skips the writing. canon never overwrites existing files, and
|
|
it refuses to update a file that changed after it was read.
|
|
|
|
Each project has a **`.canon.toml` manifest** recording what canon declared.
|
|
The block between `# >>> canon:managed >>>` and `# <<< canon:managed <<<` in
|
|
`CMakeLists.txt` is rendered entirely from that manifest. An `add` command
|
|
updates the manifest and re-renders the whole block, so:
|
|
|
|
- anything **outside** the block is yours and is never touched;
|
|
- anything you edit **inside** the block is replaced on the next `add` or
|
|
`sync` (`canon doctor` warns when the block has been edited);
|
|
- `add` commands rewrite `.canon.toml`, so comments you add to it are dropped.
|
|
|
|
canon reads `.canon.toml` strictly: unknown keys (usually typos), invalid
|
|
names, targets sharing a source directory and links to executables are errors
|
|
with a line number.
|
|
|
|
### sync
|
|
|
|
After editing `.canon.toml` by hand (say, to add a source or change
|
|
`cxx-standard`), run `canon sync` to bring the managed block in line. It shows a
|
|
diff of the block, with `--dry-run` or without, since hand edits inside the
|
|
block are replaced. It only writes `CMakeLists.txt`, so comments in
|
|
`.canon.toml` survive, and it does nothing if the block already matches. If
|
|
`.canon.toml` now declares files that do not exist, it says so, because CMake
|
|
would fail on them.
|
|
|
|
### Targets and P1204R0
|
|
|
|
P1204R0 describes a project as one library or one executable with a source
|
|
directory named after it. `canon add target` applies the same rule to each extra
|
|
target: `libextra` lives in `extra/`, `hello-cli` in `hello-cli/`. A new
|
|
executable links the project's library automatically; a new library is linked
|
|
explicitly with `canon add dep`.
|
|
|
|
### doctor
|
|
|
|
`canon doctor` never changes anything. It reports:
|
|
|
|
- **errors** (exit status 1): files `.canon.toml` declares that are missing, and
|
|
a `CMakeLists.txt` without valid managed-block markers;
|
|
- **warnings**: `.cpp` files in a source directory that no target builds, unit
|
|
or functional tests that are not declared, a hand-edited managed block,
|
|
`.h`/`.cc`-style extensions, and `include/` or `src/` directories.
|
|
|
|
Hidden directories and top-level build output (`build*`, `cmake-build-*`) are
|
|
skipped.
|
|
|
|
### Templates
|
|
|
|
Every generated source file, `README.md` and `.gitignore` comes from a
|
|
template with `{{placeholders}}` such as `{{namespace}}`. To change one, put a
|
|
file with the template's name in a template directory. canon looks in, most
|
|
specific first:
|
|
|
|
1. `.canon/templates/` in the project (commit it to share it with your team);
|
|
2. `~/.config/canon/templates/` (or `$XDG_CONFIG_HOME/canon/templates/`) for
|
|
all your projects. `canon new` only reads these, since the project does not
|
|
exist yet.
|
|
|
|
`canon templates` lists every template, where it currently comes from and which
|
|
placeholders it can use. `canon templates export [<file>...] [--user]` copies
|
|
the built-in text as a starting point and never overwrites an existing copy.
|
|
|
|
Templates are checked strictly: a file whose name is not a template (a typo such
|
|
as `libary.hpp`) or a placeholder the template does not provide stops the
|
|
command with an error naming the file. Hidden files such as `.DS_Store` are
|
|
ignored. `CMakeLists.txt` and `.canon.toml` are not templates: canon renders
|
|
them from the manifest.
|
|
|
|
| File | Role |
|
|
|---|---|
|
|
| `canon/canon.cpp` | `main`: wires the command line to the generators, executor and doctor |
|
|
| `canon/cli.*` | argument parsing, including which options each command takes |
|
|
| `canon/name.*` | naming rules (`lib` prefix, stem, namespace, keywords) |
|
|
| `canon/generate.*` | the manifest and plan for `canon new` |
|
|
| `canon/add.*` | the plans for every `canon add` command |
|
|
| `canon/sync.*` | the plan for `canon sync` |
|
|
| `canon/diff.*` | the unified diff `canon sync` shows |
|
|
| `canon/doctor.*` | the checks behind `canon doctor` |
|
|
| `canon/templates.*` | built-in templates, overrides from template directories, export |
|
|
| `canon/project.*` | finds and loads an existing project |
|
|
| `canon/manifest.*` | the manifest model, written to and read from TOML |
|
|
| `canon/toml.*` | the small TOML subset canon reads and writes |
|
|
| `canon/cmake.*` | renders `CMakeLists.txt` and replaces its managed block |
|
|
| `canon/plan.*` | operations and plans (pure data) |
|
|
| `canon/executor.*` | checks and applies a plan |
|
|
| `canon/template.*` | `{{key}}` substitution and placeholder checks |
|
|
|
|
## Tests
|
|
|
|
- **Unit tests**: `canon/<unit>.test.cpp`, next to the code they cover.
|
|
- **Scenarios**: `tests/scenarios.cmake` runs command sequences (for example
|
|
`new lib` followed by several `add` commands, including ones that must fail)
|
|
and ends each with `canon doctor`. canon runs with its own empty
|
|
`XDG_CONFIG_HOME`, so your personal templates never affect the results. Both
|
|
of the following use them.
|
|
- **Golden tests**: `tests/golden/<scenario>/` holds the exact expected result of
|
|
each scenario. After an intentional template change, regenerate the copies and
|
|
review the diff:
|
|
`cmake -DCANON=build/canon -DWORK_DIR=build/tests/golden -DUPDATE=ON -P tests/golden/check.cmake`
|
|
- **End to end**: `tests/e2e/` runs each scenario, then configures, builds and
|
|
tests the result. Skip it with `ctest -LE e2e`.
|
|
|
|
## Roadmap
|
|
|
|
1. ✅ `new lib`, `new exe`, plan/apply, `--dry-run`, manifest
|
|
2. ✅ `add module`, `add unit-test`, reading `.canon.toml`, managed-block updates
|
|
3. ✅ `add target`, `add test`, `add dep`, `doctor`
|
|
4. ✅ Template overrides: `templates`, `templates export`, project and user templates
|
|
|
|
Since then: ✅ `sync`.
|
|
|
|
## License
|
|
|
|
MIT, see [LICENSE](LICENSE).
|