Files
canon/README.md
T
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

8.2 KiB

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