paulhorn 651fa4d89b Add canon add target, add test, add dep and doctor
- 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>
2026-09-17 18:38:59 +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. 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 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.

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.cmake runs command sequences (for example new lib followed by several add commands, including ones that must fail) and ends each with canon 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 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. User template overrides

License

MIT, see LICENSE.

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