paulhorn a82bdf4734 Add template overrides: canon templates and templates export
Every generated source file, README.md and .gitignore now comes from a
template that a file of the same name can replace:

- <project>/.canon/templates/ for that project, which wins over
- ~/.config/canon/templates/ ($XDG_CONFIG_HOME respected) for all
  projects; canon new reads only these

canon templates lists each template, where it comes from and the
placeholders it can use. canon templates export [<file>...] [--user]
copies built-in texts as a starting point and never overwrites.

Overrides are checked strictly: an unknown file name or a placeholder
the template does not provide is an error naming the file; hidden files
are ignored. Generators take a template_set, so they stay pure.

Also:
- executor: a symlink to a directory counts as a directory (macOS /var,
  linked config directories)
- plan: add_directory stops at the filesystem root
- project: find_project_root, shared by load_project and templates
- tests: a templates scenario; scenarios run canon with their own empty
  XDG_CONFIG_HOME so personal templates cannot affect results

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

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

# 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 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 (canon doctor warns when the block has been edited);
  • .canon.toml is 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.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/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

License

MIT, see LICENSE.

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