paulhorn ab5c468ace Add canon: P1204R0 project scaffolding in C++23
canon new lib|exe creates a project following P1204R0 (Canonical Project
Structure) with a .canon.toml manifest and a CMakeLists.txt whose managed
block is rendered from that manifest.

Generators return a plan; the executor checks every operation before
writing, so conflicts write nothing and --dry-run writes nothing.

Tests: unit tests next to each source file, golden copies of generated
projects, and an end-to-end build of both project kinds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:11:23 +02:00

canon

Scaffolds C++ projects that follow P1204R0 (Canonical Project Structure). Written in C++23, no third-party dependencies.

canon new lib libhello            # library: libhello/hello/hello.{hpp,cpp,test.cpp} + tests/basics
canon new exe hello               # executable: hello/hello/hello.cpp
canon new lib libhello --dry-run  # show the plan, write nothing
canon new exe hello --dir ~/src   # create the project somewhere else

Every generated project builds and passes its tests out of the box:

cd libhello
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 operations) and the executor applies it. The executor checks every operation first, so a conflict writes nothing, and --dry-run is just "check, don't write". canon never overwrites files.

Each project gets 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. Upcoming canon add commands will update the manifest and re-render the block, so running a command twice gives the same result. Anything outside the block belongs to you.

File Role
canon/canon.cpp main: wires the command line to the generators and executor
canon/cli.* argument parsing
canon/name.* project name rules (lib prefix, stem, namespace)
canon/generate.* builds the manifest and plan for canon new; file templates
canon/manifest.* the manifest model and its TOML output
canon/cmake.* renders CMakeLists.txt and the managed block from a manifest
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.
  • Golden tests: tests/golden/ holds the exact expected output for libhello and hello. 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/ scaffolds both kinds, then configures, builds and runs their tests. Skip it with ctest -LE e2e.

Roadmap

  1. ✅ new lib, new exe, plan/apply, --dry-run, manifest
  2. add module, add unit-test: read .canon.toml and re-render the managed block
  3. add target, add test, add dep, doctor (check a project against P1204R0)
  4. User template overrides
S
Description
No description provided
Readme MIT 229 KiB
Languages
C++ 91.4%
CMake 8.6%