paulhorn d38e1466da Add canon add module and canon add unit-test
canon add module <path> creates <stem>/<path>.hpp, .cpp and .test.cpp
(--no-test skips the test), records the module in .canon.toml, adds the
source to the project's target and re-renders the managed block of
CMakeLists.txt. canon add unit-test adds the test later. Both work from
any directory inside the project.

- toml: a reader for the TOML subset canon writes, with line numbers in
  errors, so canon stays dependency-free
- manifest: strict parsing (unknown keys, wrong types, bad references)
- project: finds .canon.toml by walking up from the current directory
- plan/executor: update_file refuses to write if the file changed after
  it was read, and the report skips directories that already exist
- tests: shared scenarios drive both the golden and end-to-end tests;
  goldens now live in tests/golden/<scenario>/

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:24:53 +02:00
2026-09-17 18:12:06 +02:00

canon

Scaffolds C++ projects that follow P1204R0 (Canonical Project Structure), then keeps adding to them. Written in C++23, no third-party dependencies.

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 exe hello --dir ~/src     # create the project somewhere else

cd libhello
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

canon add module core --dry-run     # any command: show the changes, write nothing

add commands work from any directory inside the project. Every generated project builds and passes its tests out of the box:

cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure

Build canon

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;
  • .canon.toml is rewritten by canon, so comments you add to it are dropped.

canon reads .canon.toml strictly: unknown keys (usually typos) are errors with a line number.

File Role
canon/canon.cpp main: wires the command line to the generators and executor
canon/cli.* argument parsing
canon/name.* naming rules (lib prefix, stem, namespace, keywords)
canon/generate.* the manifest and plan for canon new; project templates
canon/add.* the plans for canon add module and canon add unit-test
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

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) that both of the following use.
  • 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 (check a project against P1204R0)
  4. User template overrides

License

MIT, see LICENSE.

S
Description
No description provided
Readme MIT 229 KiB
Languages
C++ 91.4%
CMake 8.6%