# canon Scaffolds C++ projects that follow [P1204R0](https://wg21.link/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. ```bash 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: ```bash cmake -S . -B build cmake --build build ctest --test-dir build --output-on-failure ``` ## Build canon ```bash 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 [...] [--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/.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//` 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](LICENSE).