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>
canon
Scaffolds C++ projects that follow 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.
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:
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
addorsync(canon doctorwarns when the block has been edited); addcommands 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.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.
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:
.canon/templates/in the project (commit it to share it with your team);~/.config/canon/templates/(or$XDG_CONFIG_HOME/canon/templates/) for all your projects.canon newonly 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.cmakeruns command sequences (for examplenew libfollowed by severaladdcommands, including ones that must fail) and ends each withcanon doctor. canon runs with its own emptyXDG_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 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 - ✅ Template overrides:
templates,templates export, project and user templates
Since then: ✅ sync.
License
MIT, see LICENSE.