Add canon: P1204R0 project scaffolding in C++23

canon new lib|exe creates a project following P1204R0 (Canonical Project
Structure) with a .canon.toml manifest and a CMakeLists.txt whose managed
block is rendered from that manifest.

Generators return a plan; the executor checks every operation before
writing, so conflicts write nothing and --dry-run writes nothing.

Tests: unit tests next to each source file, golden copies of generated
projects, and an end-to-end build of both project kinds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 18:11:23 +02:00
commit ab5c468ace
45 changed files with 2192 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
# canon
Scaffolds C++ projects that follow [P1204R0](https://wg21.link/p1204r0)
(Canonical Project Structure). Written in C++23, no third-party dependencies.
```bash
canon new lib libhello # library: libhello/hello/hello.{hpp,cpp,test.cpp} + tests/basics
canon new exe hello # executable: hello/hello/hello.cpp
canon new lib libhello --dry-run # show the plan, write nothing
canon new exe hello --dir ~/src # create the project somewhere else
```
Every generated project builds and passes its tests out of the box:
```bash
cd libhello
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` operations) and the **executor** applies it.
The executor checks every operation first, so a conflict writes nothing, and
`--dry-run` is just "check, don't write". canon never overwrites files.
Each project gets 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. Upcoming `canon add`
commands will update the manifest and re-render the block, so running a command
twice gives the same result. Anything outside the block belongs to you.
| File | Role |
|---|---|
| `canon/canon.cpp` | `main`: wires the command line to the generators and executor |
| `canon/cli.*` | argument parsing |
| `canon/name.*` | project name rules (`lib` prefix, stem, namespace) |
| `canon/generate.*` | builds the manifest and plan for `canon new`; file templates |
| `canon/manifest.*` | the manifest model and its TOML output |
| `canon/cmake.*` | renders `CMakeLists.txt` and the managed block from a manifest |
| `canon/plan.*` | operations and plans (pure data) |
| `canon/executor.*` | checks and applies a plan |
| `canon/template.*` | `{{key}}` substitution |
## Tests
- **Unit tests**: `canon/<unit>.test.cpp`, next to the code they cover.
- **Golden tests**: `tests/golden/` holds the exact expected output for
`libhello` and `hello`. 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/` scaffolds both kinds, then configures, builds and
runs their tests. Skip it with `ctest -LE e2e`.
## Roadmap
1. ✅ `new lib`, `new exe`, plan/apply, `--dry-run`, manifest
2. `add module`, `add unit-test`: read `.canon.toml` and re-render the managed block
3. `add target`, `add test`, `add dep`, `doctor` (check a project against P1204R0)
4. User template overrides