- add target lib|exe <name>: another library or executable in a source directory named after it (P1204R0's rule applied per target); a new executable links the project's library - add test <name>: tests/<name>/driver.cpp for a library, or a run of an executable, with --arg and --expect - add dep <dependency>: link a library of this project (no cycles, no executables) or an installed package's imported target such as fmt::fmt, adding its find_package() - doctor: read-only check of the files against .canon.toml and the P1204R0 layout; errors for missing declared files or broken markers, warnings for unbuilt sources, undeclared tests, a hand-edited managed block, .h/.cc extensions and include/ or src/ directories - every add command takes --target; the CLI rejects options a command does not take - manifest: packages, test args, and validation of target names, shared source directories and dependencies - templates moved to canon/templates.hpp - tests: multi-target and exe-tests scenarios; every scenario ends with canon doctor; CANON_CHECK accepts expressions containing commas; the CLI test no longer reads a destroyed temporary Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
canon
Scaffolds C++ projects that follow P1204R0 (Canonical Project Structure), keeps adding to them, and checks them. Written in C++23, no third-party dependencies.
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 add module core --dry-run # any add command: show the changes, write nothing
add and doctor 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 doctorwarns when the block has been edited); .canon.tomlis rewritten by canon, 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.
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.tomldeclares that are missing, and aCMakeLists.txtwithout valid managed-block markers; - warnings:
.cppfiles 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, andinclude/orsrc/directories.
Hidden directories and top-level build output (build*, cmake-build-*) are
skipped.
| 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/doctor.* |
the checks behind canon doctor |
canon/templates.hpp |
the contents of generated files |
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.cmakeruns command sequences (for examplenew libfollowed by severaladdcommands, including ones that must fail) and ends each withcanon doctor. 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 withctest -LE e2e.
Roadmap
- ✅
new lib,new exe, plan/apply,--dry-run, manifest - ✅
add module,add unit-test, reading.canon.toml, managed-block updates - ✅
add target,add test,add dep,doctor - User template overrides
License
MIT, see LICENSE.