a82bdf4734
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>
130 lines
5.6 KiB
CMake
130 lines
5.6 KiB
CMake
# canon command sequences shared by the golden and end-to-end tests. Each
|
|
# scenario builds one project inside <work_dir>/<scenario>/. Requires CANON.
|
|
#
|
|
# canon runs with XDG_CONFIG_HOME set to <work_dir>/<scenario>-config, so user
|
|
# templates in your own ~/.config never leak into the results.
|
|
|
|
set(CANON_SCENARIOS new-lib new-exe add-lib add-exe multi-target exe-tests templates)
|
|
|
|
# Scenarios run canon from other directories, so a relative path would break.
|
|
get_filename_component(CANON "${CANON}" ABSOLUTE)
|
|
|
|
# Runs canon in working_dir and fails the script if it fails.
|
|
function(canon_run working_dir)
|
|
execute_process(
|
|
COMMAND ${CMAKE_COMMAND} -E env XDG_CONFIG_HOME=${config_home} ${CANON} ${ARGN}
|
|
WORKING_DIRECTORY ${working_dir}
|
|
RESULT_VARIABLE status
|
|
OUTPUT_VARIABLE output
|
|
ERROR_VARIABLE errors)
|
|
if(NOT status EQUAL 0)
|
|
string(JOIN " " args ${ARGN})
|
|
message(FATAL_ERROR "canon ${args} failed (${status}) in ${working_dir}:\n${output}${errors}")
|
|
endif()
|
|
endfunction()
|
|
|
|
# Runs canon in working_dir and fails the script if it succeeds.
|
|
function(canon_run_fails working_dir)
|
|
execute_process(
|
|
COMMAND ${CMAKE_COMMAND} -E env XDG_CONFIG_HOME=${config_home} ${CANON} ${ARGN}
|
|
WORKING_DIRECTORY ${working_dir}
|
|
RESULT_VARIABLE status
|
|
OUTPUT_QUIET ERROR_QUIET)
|
|
if(status EQUAL 0)
|
|
string(JOIN " " args ${ARGN})
|
|
message(FATAL_ERROR "canon ${args} succeeded in ${working_dir} but should have failed")
|
|
endif()
|
|
endfunction()
|
|
|
|
# Runs the scenario and sets out_var to the project directory it produced.
|
|
function(canon_scenario scenario work_dir out_var)
|
|
set(dir ${work_dir}/${scenario})
|
|
file(REMOVE_RECURSE ${dir})
|
|
file(MAKE_DIRECTORY ${dir})
|
|
set(config_home ${work_dir}/${scenario}-config) # seen by canon_run
|
|
file(REMOVE_RECURSE ${config_home})
|
|
file(MAKE_DIRECTORY ${config_home})
|
|
|
|
if(scenario STREQUAL "new-lib")
|
|
canon_run(${dir} new lib libhello)
|
|
canon_run_fails(${dir} new lib libhello) # never overwrites
|
|
set(project ${dir}/libhello)
|
|
|
|
elseif(scenario STREQUAL "new-exe")
|
|
canon_run(${dir} new exe hello)
|
|
set(project ${dir}/hello)
|
|
|
|
elseif(scenario STREQUAL "add-lib")
|
|
canon_run(${dir} new lib libhello)
|
|
set(project ${dir}/libhello)
|
|
canon_run(${project} add module core)
|
|
canon_run(${project}/hello add module details/utility --no-test) # from a subdirectory
|
|
canon_run(${project} add unit-test details/utility)
|
|
canon_run(${project} add module parser --dry-run) # must write nothing
|
|
canon_run_fails(${project} add module core) # already declared
|
|
canon_run_fails(${project} add unit-test core) # already has one
|
|
canon_run_fails(${project} add module Bad) # invalid path
|
|
|
|
elseif(scenario STREQUAL "add-exe")
|
|
canon_run(${dir} new exe hello)
|
|
set(project ${dir}/hello)
|
|
canon_run(${project} add module greeting)
|
|
|
|
elseif(scenario STREQUAL "multi-target")
|
|
canon_run(${dir} new lib libhello)
|
|
set(project ${dir}/libhello)
|
|
canon_run(${project} add target exe hello-cli) # links libhello
|
|
canon_run(${project} add module args --target hello-cli)
|
|
canon_run(${project} add target lib libextra)
|
|
canon_run(${project} add dep libextra) # libhello -> libextra
|
|
canon_run(${project} add dep Threads::Threads --target hello-cli)
|
|
canon_run(${project} add test edge-cases)
|
|
canon_run_fails(${project} add dep libhello --target libextra) # cycle
|
|
canon_run_fails(${project} add dep hello-cli) # executable
|
|
canon_run_fails(${project} add dep fmt) # neither
|
|
canon_run_fails(${project} add target exe hello) # hello/ is taken
|
|
|
|
elseif(scenario STREQUAL "exe-tests")
|
|
canon_run(${dir} new exe hello)
|
|
set(project ${dir}/hello)
|
|
canon_run(${project} add test greets-paul --arg Paul --expect "^Hello, Paul!")
|
|
canon_run(${project} add test spaces --arg "two words" --expect "Hello, two words!")
|
|
canon_run_fails(${project} add test greets-paul)
|
|
|
|
elseif(scenario STREQUAL "templates")
|
|
# User templates apply to every new project: a license line on library and module headers.
|
|
canon_run(${dir} templates export library.hpp module.hpp --user)
|
|
foreach(template library.hpp module.hpp)
|
|
set(file ${config_home}/canon/templates/${template})
|
|
file(READ ${file} text)
|
|
file(WRITE ${file} "// SPDX-License-Identifier: MIT\n${text}")
|
|
endforeach()
|
|
canon_run(${dir} new lib libhello)
|
|
set(project ${dir}/libhello)
|
|
|
|
# A project template wins over a user template.
|
|
canon_run(${project} templates export module.hpp --dry-run) # must write nothing
|
|
canon_run(${project} templates export module.hpp)
|
|
file(READ ${project}/.canon/templates/module.hpp text)
|
|
file(WRITE ${project}/.canon/templates/module.hpp "// Part of {{namespace}}.\n${text}")
|
|
canon_run(${project} add module details/core)
|
|
canon_run(${project} templates)
|
|
canon_run_fails(${project} templates export module.hpp) # never overwrites
|
|
|
|
# Broken templates are errors, not silently skipped.
|
|
file(WRITE ${project}/.canon/templates/module.cpp "{{nmae}}\n")
|
|
canon_run_fails(${project} add module broken)
|
|
canon_run_fails(${project} templates)
|
|
file(REMOVE ${project}/.canon/templates/module.cpp)
|
|
file(WRITE ${config_home}/canon/templates/modul.hpp "")
|
|
canon_run_fails(${project} add module broken)
|
|
file(REMOVE ${config_home}/canon/templates/modul.hpp)
|
|
|
|
else()
|
|
message(FATAL_ERROR "unknown scenario '${scenario}'")
|
|
endif()
|
|
|
|
canon_run(${project} doctor) # every scenario ends healthy
|
|
set(${out_var} ${project} PARENT_SCOPE)
|
|
endfunction()
|