Files
paulhorn 6981f55f28 Add canon sync
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>
2026-09-17 19:09:35 +02:00

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).