From caf4bdbc51a1aae7c38114dba36c762ebfe1c100 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 13:41:23 -0400 Subject: [PATCH 01/12] Migrate template engine from Cookiecutter to Copier Replace the Cookiecutter-based template under cookiecutter/ with a Copier-based template under template/. The new layout uses copier.yml for declarative configuration and .jinja suffixed files for template rendering. Key changes: - Delete cookiecutter/ directory and its hooks/config - Add copier.yml with project questions, defaults, and post-copy tasks - Create template/ with Jinja2 versions of all project files - Add copier/check_copier.sh for round-trip validation - Add template/.copier-answers.yml.jinja so generated projects retain Copier metadata for future template updates - Update stamp.sh to use Copier instead of Cookiecutter - Document migration path in README and CONTRIBUTING The .copier-answers.yml persisted in generated projects enables copier update for template-driven maintenance going forward. --- .gitattributes | 3 +- .github/workflows/catch2_exemplar_test.yml | 12 +- ...{cookiecutter_test.yml => copier_test.yml} | 14 +- .github/workflows/todo_exemplar_test.yml | 12 +- .pre-commit-config.yaml | 2 +- CONTRIBUTING.md | 41 +++ README.md | 64 +++- cookiecutter/check_cookiecutter.sh | 101 ------ cookiecutter/cookiecutter.json | 16 - cookiecutter/hooks/post_gen_project.py | 26 -- .../CMakeLists.txt | 94 ----- .../{{cookiecutter.project_name}}/README.md | 267 -------------- .../examples/identity_direct_usage.cpp | 24 -- .../CMakeLists.txt | 27 -- .../{{cookiecutter.project_name}}/config.hpp | 12 - .../config_generated.hpp.in | 8 - .../{{cookiecutter.project_name}}.cppm | 11 - .../{{cookiecutter.project_name}}.hpp | 20 - .../CMakeLists.txt | 33 -- .../identity.test.cpp | 117 ------ copier.yml | 86 +++++ copier/check_copier.sh | 146 ++++++++ stamp.sh | 44 ++- .../.beman-tidy.yaml.jinja | 2 +- .../.clang-format.jinja | 0 template/.copier-answers.yml.jinja | 8 + .../.gitattributes.jinja | 3 +- .../.github/CODEOWNERS.jinja | 4 +- .../implementation-deficiency.md.jinja | 0 .../infrastructure-issues.md.jinja | 0 .../ISSUE_TEMPLATE/paper-discussion.md.jinja | 0 .../.github/pull_request_template.md.jinja | 0 .../.github/workflows/ci_tests.yml.jinja | 16 +- .../workflows/pre-commit-check.yml.jinja | 0 .../workflows/pre-commit-update.yml.jinja | 2 +- .../.github/workflows/vcpkg-release.yml.jinja | 2 +- .../.gitignore => template/.gitignore.jinja | 0 .../.markdownlint.yaml.jinja | 0 .../.pre-commit-config.yaml.jinja | 4 +- template/CMakeLists.txt.jinja | 94 +++++ .../CMakePresets.json.jinja | 2 +- .../CONTRIBUTING.md.jinja | 62 +++- .../LICENSE => template/LICENSE.jinja | 0 template/README.md.jinja | 342 ++++++++++++++++++ .../examples/CMakeLists.txt.jinja | 14 +- .../identity_as_default_projection.cpp.jinja | 10 +- .../examples/identity_direct_usage.cpp.jinja | 24 ++ .../{{ project_name }}/CMakeLists.txt.jinja | 27 ++ .../beman/{{ project_name }}/config.hpp.jinja | 12 + .../config_generated.hpp.in.jinja | 8 + .../{{ project_name }}/identity.hpp.jinja | 26 +- .../{{ project_name }}.cppm.jinja | 11 + .../{{ project_name }}.hpp.jinja | 20 + .../infra/.beman_submodule.jinja | 0 .../infra/.github/CODEOWNERS.jinja | 0 .../.github/workflows/pre-commit.yml.jinja | 3 + ...le-beman-create-issue-when-fault.yml.jinja | 3 +- .../infra/.gitignore.jinja | 0 .../infra/.pre-commit-config.yaml.jinja | 0 .../LICENSE => template/infra/LICENSE.jinja | 0 .../infra/README.md.jinja | 0 .../infra/cmake/BuildTelemetry.cmake.jinja | 0 .../cmake/BuildTelemetryConfig.cmake.jinja | 0 .../infra/cmake/Config.cmake.in.jinja | 0 .../cmake/appleclang-toolchain.cmake.jinja | 0 .../cmake/beman-install-library.cmake.jinja | 0 ...enable-experimental-import-std.cmake.jinja | 0 .../infra/cmake/gnu-toolchain.cmake.jinja | 0 .../cmake/llvm-libc++-toolchain.cmake.jinja | 0 .../infra/cmake/llvm-toolchain.cmake.jinja | 0 .../infra/cmake/msvc-toolchain.cmake.jinja | 0 .../infra/cmake/telemetry.sh.jinja | 0 .../infra/cmake/use-fetch-content.cmake.jinja | 0 .../lockfile.json.jinja | 4 +- .../port/portfile.cmake.in.jinja | 12 +- .../port/vcpkg.json.in.jinja | 8 +- .../{{ project_name }}/CMakeLists.txt.jinja | 33 ++ .../identity.test.cpp.jinja | 117 ++++++ .../vcpkg-configuration.json.jinja | 0 .../vcpkg.json => template/vcpkg.json.jinja | 8 +- 80 files changed, 1199 insertions(+), 862 deletions(-) rename .github/workflows/{cookiecutter_test.yml => copier_test.yml} (52%) delete mode 100755 cookiecutter/check_cookiecutter.sh delete mode 100644 cookiecutter/cookiecutter.json delete mode 100755 cookiecutter/hooks/post_gen_project.py delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/CMakeLists.txt delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/README.md delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/examples/identity_direct_usage.cpp delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/CMakeLists.txt delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/config.hpp delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/config_generated.hpp.in delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}.cppm delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}.hpp delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/tests/beman/{{cookiecutter.project_name}}/CMakeLists.txt delete mode 100644 cookiecutter/{{cookiecutter.project_name}}/tests/beman/{{cookiecutter.project_name}}/identity.test.cpp create mode 100644 copier.yml create mode 100755 copier/check_copier.sh rename cookiecutter/{{cookiecutter.project_name}}/.beman-tidy.yaml => template/.beman-tidy.yaml.jinja (91%) rename cookiecutter/{{cookiecutter.project_name}}/.clang-format => template/.clang-format.jinja (100%) create mode 100644 template/.copier-answers.yml.jinja rename cookiecutter/{{cookiecutter.project_name}}/.gitattributes => template/.gitattributes.jinja (66%) rename cookiecutter/{{cookiecutter.project_name}}/.github/CODEOWNERS => template/.github/CODEOWNERS.jinja (66%) rename cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/implementation-deficiency.md => template/.github/ISSUE_TEMPLATE/implementation-deficiency.md.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/infrastructure-issues.md => template/.github/ISSUE_TEMPLATE/infrastructure-issues.md.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/paper-discussion.md => template/.github/ISSUE_TEMPLATE/paper-discussion.md.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/.github/pull_request_template.md => template/.github/pull_request_template.md.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/.github/workflows/ci_tests.yml => template/.github/workflows/ci_tests.yml.jinja (88%) rename cookiecutter/{{cookiecutter.project_name}}/.github/workflows/pre-commit-check.yml => template/.github/workflows/pre-commit-check.yml.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/.github/workflows/pre-commit-update.yml => template/.github/workflows/pre-commit-update.yml.jinja (66%) rename cookiecutter/{{cookiecutter.project_name}}/.github/workflows/vcpkg-release.yml => template/.github/workflows/vcpkg-release.yml.jinja (83%) rename cookiecutter/{{cookiecutter.project_name}}/.gitignore => template/.gitignore.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/.markdownlint.yaml => template/.markdownlint.yaml.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/.pre-commit-config.yaml => template/.pre-commit-config.yaml.jinja (94%) create mode 100644 template/CMakeLists.txt.jinja rename cookiecutter/{{cookiecutter.project_name}}/CMakePresets.json => template/CMakePresets.json.jinja (99%) rename cookiecutter/{{cookiecutter.project_name}}/CONTRIBUTING.md => template/CONTRIBUTING.md.jinja (61%) rename cookiecutter/{{cookiecutter.project_name}}/LICENSE => template/LICENSE.jinja (100%) create mode 100644 template/README.md.jinja rename cookiecutter/{{cookiecutter.project_name}}/examples/CMakeLists.txt => template/examples/CMakeLists.txt.jinja (60%) rename cookiecutter/{{cookiecutter.project_name}}/examples/identity_as_default_projection.cpp => template/examples/identity_as_default_projection.cpp.jinja (87%) create mode 100644 template/examples/identity_direct_usage.cpp.jinja create mode 100644 template/include/beman/{{ project_name }}/CMakeLists.txt.jinja create mode 100644 template/include/beman/{{ project_name }}/config.hpp.jinja create mode 100644 template/include/beman/{{ project_name }}/config_generated.hpp.in.jinja rename cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/identity.hpp => template/include/beman/{{ project_name }}/identity.hpp.jinja (52%) create mode 100644 template/include/beman/{{ project_name }}/{{ project_name }}.cppm.jinja create mode 100644 template/include/beman/{{ project_name }}/{{ project_name }}.hpp.jinja rename cookiecutter/{{cookiecutter.project_name}}/infra/.beman_submodule => template/infra/.beman_submodule.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/.github/CODEOWNERS => template/infra/.github/CODEOWNERS.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/.github/workflows/pre-commit.yml => template/infra/.github/workflows/pre-commit.yml.jinja (98%) rename cookiecutter/{{cookiecutter.project_name}}/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml => template/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml.jinja (98%) rename cookiecutter/{{cookiecutter.project_name}}/infra/.gitignore => template/infra/.gitignore.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/.pre-commit-config.yaml => template/infra/.pre-commit-config.yaml.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/LICENSE => template/infra/LICENSE.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/README.md => template/infra/README.md.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/BuildTelemetry.cmake => template/infra/cmake/BuildTelemetry.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/BuildTelemetryConfig.cmake => template/infra/cmake/BuildTelemetryConfig.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/Config.cmake.in => template/infra/cmake/Config.cmake.in.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/appleclang-toolchain.cmake => template/infra/cmake/appleclang-toolchain.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/beman-install-library.cmake => template/infra/cmake/beman-install-library.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/enable-experimental-import-std.cmake => template/infra/cmake/enable-experimental-import-std.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/gnu-toolchain.cmake => template/infra/cmake/gnu-toolchain.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/llvm-libc++-toolchain.cmake => template/infra/cmake/llvm-libc++-toolchain.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/llvm-toolchain.cmake => template/infra/cmake/llvm-toolchain.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/msvc-toolchain.cmake => template/infra/cmake/msvc-toolchain.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/telemetry.sh => template/infra/cmake/telemetry.sh.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/infra/cmake/use-fetch-content.cmake => template/infra/cmake/use-fetch-content.cmake.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/lockfile.json => template/lockfile.json.jinja (82%) rename cookiecutter/{{cookiecutter.project_name}}/port/portfile.cmake.in => template/port/portfile.cmake.in.jinja (61%) rename cookiecutter/{{cookiecutter.project_name}}/port/vcpkg.json.in => template/port/vcpkg.json.in.jinja (50%) create mode 100644 template/tests/beman/{{ project_name }}/CMakeLists.txt.jinja create mode 100644 template/tests/beman/{{ project_name }}/identity.test.cpp.jinja rename cookiecutter/{{cookiecutter.project_name}}/vcpkg-configuration.json => template/vcpkg-configuration.json.jinja (100%) rename cookiecutter/{{cookiecutter.project_name}}/vcpkg.json => template/vcpkg.json.jinja (54%) diff --git a/.gitattributes b/.gitattributes index 793dce7d..9a7df7dd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ infra/** linguist-vendored -cookiecutter/** linguist-vendored +template/** linguist-vendored +copier/** linguist-vendored *.bib -linguist-detectable *.tex -linguist-detectable papers/* linguist-documentation diff --git a/.github/workflows/catch2_exemplar_test.yml b/.github/workflows/catch2_exemplar_test.yml index 60ee2576..bb0b73d2 100644 --- a/.github/workflows/catch2_exemplar_test.yml +++ b/.github/workflows/catch2_exemplar_test.yml @@ -17,12 +17,12 @@ jobs: uses: actions/checkout@v6 - name: Test catch2 exemplar run: | - cd cookiecutter - source ./check_cookiecutter.sh - cookiecutter_venv_path=$(mktemp --directory --dry-run) - setup_venv "$cookiecutter_venv_path" - stamp "$PWD" "./catch2_exemplar" "catch2" "true" - cd catch2_exemplar/exemplar + cd copier + source ./check_copier.sh + copier_venv_path=$(mktemp --directory --dry-run) + setup_venv "$copier_venv_path" + stamp "$repo_root" "./catch2_exemplar" "catch2" "true" + cd catch2_exemplar cmake -B build -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=./infra/cmake/use-fetch-content.cmake -DCMAKE_CXX_STANDARD=20 -DCMAKE_INSTALL_PREFIX=$PWD/dist cmake --build build ctest --test-dir build diff --git a/.github/workflows/cookiecutter_test.yml b/.github/workflows/copier_test.yml similarity index 52% rename from .github/workflows/cookiecutter_test.yml rename to .github/workflows/copier_test.yml index 04cdd05f..78b087a2 100644 --- a/.github/workflows/cookiecutter_test.yml +++ b/.github/workflows/copier_test.yml @@ -1,19 +1,23 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -name: Cookiecutter Test +name: Copier Test on: push: branches: - main pull_request: + workflow_dispatch: + +permissions: + contents: read jobs: - cookiecutter-test: + copier-test: runs-on: ubuntu-latest - name: "Check cookiecutter for consistency" + name: "Check copier for consistency" steps: - name: Checkout uses: actions/checkout@v6 - - name: beman cookiecutter consistency check + - name: beman copier consistency check run: | - ./cookiecutter/check_cookiecutter.sh + ./copier/check_copier.sh diff --git a/.github/workflows/todo_exemplar_test.yml b/.github/workflows/todo_exemplar_test.yml index e7b4c33b..38bf7aa7 100644 --- a/.github/workflows/todo_exemplar_test.yml +++ b/.github/workflows/todo_exemplar_test.yml @@ -17,12 +17,12 @@ jobs: uses: actions/checkout@v6 - name: Test static exemplar run: | - cd cookiecutter - source ./check_cookiecutter.sh - cookiecutter_venv_path=$(mktemp --directory --dry-run) - setup_venv "$cookiecutter_venv_path" - stamp "$PWD" "./static_exemplar" "gtest" "false" - cd static_exemplar/exemplar + cd copier + source ./check_copier.sh + copier_venv_path=$(mktemp --directory --dry-run) + setup_venv "$copier_venv_path" + stamp "$repo_root" "./static_exemplar" "gtest" "false" + cd static_exemplar grep -r 'identity' . && exit 1 find . -name '*identity*' | grep . && exit 1 cmake -B build -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=./infra/cmake/use-fetch-content.cmake -DCMAKE_CXX_STANDARD=20 -DCMAKE_INSTALL_PREFIX=$PWD/dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5de08b6d..093f6eed 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,4 +40,4 @@ repos: hooks: - id: codespell -exclude: 'cookiecutter/|infra/|port/' +exclude: 'template/|copier/|infra/|port/' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42e2f3d8..9b9a85ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,6 +106,47 @@ ctest --test-dir build The file `./lockfile.json` configures the list of dependencies and versions that will be acquired by FetchContent. +## Developing The Copier Template + +This repository doubles as both the exemplar library and the Copier template used to stamp +new Beman libraries. + +The template workflow is organized as follows: + +* `copier.yml` defines the Copier questions, defaults, validation rules, and post-copy + tasks. +* `template/` contains the files rendered into generated projects. +* `stamp.sh` is the user-facing wrapper for stamping a forked exemplar repository into a + new project. +* `./copier/check_copier.sh` validates that exemplar regenerates cleanly and that a + non-exemplar project does not leak exemplar-specific names like `identity`. + +When testing local template changes, do not render directly from the Git checkout. The +Copier helper scripts intentionally render from a temporary snapshot with `.git`, `build`, +and `.venv` excluded so that validation uses the current worktree contents. + +Run the template checks with: + +```shell +./copier/check_copier.sh +``` + +If you need to smoke-test generated projects manually, start from the same validated flow: + +```shell +tmpdir=$(mktemp -d) +python3 -m venv "$tmpdir/venv" +"$tmpdir/venv/bin/python3" -m pip install copier +src=$(mktemp -d) +rsync -a --exclude .git --exclude build --exclude .venv ./ "$src/" +"$tmpdir/venv/bin/copier" copy --trust --defaults \ + -d template_src_path=https://github.com/bemanproject/exemplar.git \ + -d template_commit="$(git rev-parse HEAD)" \ + "$src" "$tmpdir/project" +``` + +Then configure and build the generated project as usual. + ## Project-specific configure arguments Project-specific options are prefixed with `BEMAN_EXEMPLAR`. diff --git a/README.md b/README.md index 10db53f4..e5d50476 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,72 @@ $ ./stamp.sh [3/6] minimum_cpp_build_version (20): [4/6] paper (PnnnnRr): P9999R9 [5/6] description (Short project description.): - [6/6] Select unit_test_library - 1 - gtest - 2 - catch2 - Choose from [1/2] (1): + unit_test_library? Format: str + Choices: + - gtest + - catch2 + > gtest Switched to a new branch 'stamp' Successfully stamped out exemplar template to the new branch 'stamp'. Try 'git push origin stamp' to push the branch upstream, then create a pull request. ``` +`stamp.sh` is a convenience wrapper around `copier copy`. It creates an isolated virtual +environment, renders from a temporary snapshot of the repository, runs `pre-commit`, and +commits the stamped result onto a `stamp` branch. The stamped tree now includes +`.copier-answers.yml`, so future `copier update` runs know which exemplar revision they +started from. + +## Rebasing An Older Exemplar Clone Onto Copier + +If your repository predates the Copier migration and was created as a plain GitHub +template copy, bootstrap a fresh stamped baseline at the original exemplar split point, +then rebase your project commits onto that baseline. + +Start from a clean worktree and make sure you can identify the branch you want to +migrate; the examples below assume `main`. + +```shell +git remote add exemplar https://github.com/bemanproject/exemplar.git +git fetch exemplar +base=$(git merge-base main exemplar/main) +git branch pre-copier-backup main +git switch --detach "$base" + +tmpdir=$(mktemp -d) +python3 -m venv "$tmpdir/venv" +"$tmpdir/venv/bin/python3" -m pip install copier pre-commit +"$tmpdir/venv/bin/copier" copy --trust --overwrite \ + -d project_name=your_project_name \ + -d maintainer=your_github_username \ + -d minimum_cpp_build_version=20 \ + -d paper=PnnnnRr \ + -d description="Short project description." \ + -d unit_test_library=gtest \ + -d generating_exemplar=false \ + -d owner=bemanproject \ + https://github.com/bemanproject/exemplar.git \ + . +"$tmpdir/venv/bin/pre-commit" run --all-files || true +git add . +git switch -c stamp +git commit -m "Bootstrap Copier template" + +git switch main +git rebase --rebase-merges --onto stamp "$base" main +``` + +After the rebase, commit the generated `.copier-answers.yml`. That file is what enables +future template updates with: + +```shell +copier update --trust +``` + +If you want to track a fork of exemplar rather than `bemanproject/exemplar`, edit +`_src_path` in `.copier-answers.yml` before your first `copier update`. + From there, you can simply fill in all the remaining parts of the repository that are labeled 'todo'. diff --git a/cookiecutter/check_cookiecutter.sh b/cookiecutter/check_cookiecutter.sh deleted file mode 100755 index 394cca7e..00000000 --- a/cookiecutter/check_cookiecutter.sh +++ /dev/null @@ -1,101 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -#!/usr/bin/env bash - -set -euo pipefail - -declare script_dir=$(realpath $(dirname "$BASH_SOURCE")) - -function stamp() { - local cookiecutter_dir="$1" ; shift - local output_dir="$1" ; shift - local unit_test_library="$1" ; shift - local generating_exemplar="$1" ; shift - python3 \ - -m cookiecutter \ - --no-input \ - --output-dir "$output_dir" \ - "$cookiecutter_dir" \ - project_name="exemplar" \ - minimum_cpp_build_version="17" \ - paper="P0898R3" \ - description="A Beman Library Exemplar" \ - unit_test_library="$unit_test_library" \ - _generating_exemplar="$generating_exemplar" \ - _ci_tests_cron="30 15 * * 6" \ - _pre_commit_update_cron="0 16 * * 0" -} - -function check_consistency() { - local out_dir_path - out_dir_path=$(mktemp --directory --dry-run) - cd /tmp - stamp "$script_dir" "$out_dir_path" "gtest" "true" - cp "$script_dir"/../.github/workflows/cookiecutter_test.yml "$out_dir_path"/exemplar/.github/workflows - cp "$script_dir"/../.github/workflows/catch2_exemplar_test.yml "$out_dir_path"/exemplar/.github/workflows - cp "$script_dir"/../.github/workflows/todo_exemplar_test.yml "$out_dir_path"/exemplar/.github/workflows - mkdir "$out_dir_path"/exemplar/images - cp "$script_dir"/../images/use-this-template.png "$out_dir_path"/exemplar/images/use-this-template.png - cp "$script_dir"/../stamp.sh "$out_dir_path"/exemplar/stamp.sh - local diff_path - diff_path=$(mktemp) - diff -r "$script_dir/.." "$out_dir_path/exemplar" \ - | grep -v -e 'cookiecutter$' -e '.git$' > "$diff_path" || true - rm -rf "$out_dir_path" - if [[ $(wc -l "$diff_path" | cut -d' ' -f1) -gt 0 ]] ; then - echo "Discrepancy between exemplar and cookiecutter:" >&2 - cat "$diff_path" - rm "$diff_path" - exit 1 - fi - rm "$diff_path" -} - -function check_templating() { - local out_dir_path - out_dir_path=$(mktemp --directory --dry-run) - cd /tmp - python3 \ - -m cookiecutter \ - --no-input \ - --output-dir "$out_dir_path" \ - "$script_dir" \ - project_name="RLZrmX9NfS" \ - minimum_cpp_build_version="17" \ - paper="P0898R3" \ - description="A Beman Library RLZrmX9NfS" \ - _generating_exemplar="false" \ - _ci_tests_cron="30 15 * * 6" \ - _pre_commit_update_cron="0 16 * * 0" - rm -rf "$out_dir_path/RLZrmX9NfS/infra" - local grep_path - grep_path=$(mktemp) - grep \ - --dereference-recursive --context=5 --color=always \ - -e "exemplar" -e "identity" "$out_dir_path/RLZrmX9NfS" > "$grep_path" || true - rm -rf "$out_dir_path" - if [[ $(wc -l "$grep_path" | cut -d' ' -f1) -gt 0 ]] ; then - echo "Untemplated \"exemplar\" or \"identity\" in cookiecutter:" >&2 - cat "$grep_path" - rm "$grep_path" - exit 1 - fi - rm "$grep_path" -} - -function setup_venv() { - local path="$1" ; shift - python3 -m venv "$cookiecutter_venv_path" - source "$cookiecutter_venv_path/bin/activate" - python3 -m pip install cookiecutter >& /dev/null -} - -function main() { - local cookiecutter_venv_path - cookiecutter_venv_path=$(mktemp --directory --dry-run) - setup_venv "$cookiecutter_venv_path" - check_consistency - check_templating - rm -rf "$cookiecutter_venv_path" -} - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] || main "$@" diff --git a/cookiecutter/cookiecutter.json b/cookiecutter/cookiecutter.json deleted file mode 100644 index bb406fe8..00000000 --- a/cookiecutter/cookiecutter.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "project_name": "my_project_name", - "maintainer": "your_github_username", - "minimum_cpp_build_version": "20", - "paper": "PnnnnRr", - "description": "Short project description.", - "unit_test_library": ["gtest", "catch2"], - "_generating_exemplar": false, - "_owner": "bemanproject", - "_ci_tests_cron": "", - "_pre_commit_update_cron": "", - "_copy_without_render": [ - "infra" - ], - "_jinja2_env_vars": {"trim_blocks": true} -} diff --git a/cookiecutter/hooks/post_gen_project.py b/cookiecutter/hooks/post_gen_project.py deleted file mode 100755 index 9965c4c3..00000000 --- a/cookiecutter/hooks/post_gen_project.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -import shutil -import subprocess -from pathlib import Path -import os - -project_name = "{{ cookiecutter.project_name }}" -generating_exemplar = "{{ cookiecutter._generating_exemplar }}" == "True" - -if not generating_exemplar: - os.rename("include/beman/" + project_name + "/identity.hpp", "include/beman/" + project_name + "/todo.hpp") - os.rename("examples/identity_direct_usage.cpp", "examples/todo.cpp") - os.remove("examples/identity_as_default_projection.cpp") - os.rename("tests/beman/" + project_name + "/identity.test.cpp", "tests/beman/" + project_name + "/todo.test.cpp") - - # Record the exemplar commit this project was stamped from. - result = subprocess.run( - ["git", "ls-remote", "https://github.com/bemanproject/exemplar.git", "HEAD"], - capture_output=True, - text=True, - check=True, - ) - sha = result.stdout.split()[0] - Path(".exemplar_version").write_text(sha + "\n") diff --git a/cookiecutter/{{cookiecutter.project_name}}/CMakeLists.txt b/cookiecutter/{{cookiecutter.project_name}}/CMakeLists.txt deleted file mode 100644 index ed4789bd..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/CMakeLists.txt +++ /dev/null @@ -1,94 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -cmake_minimum_required(VERSION 3.30...4.3) - -include(infra/cmake/enable-experimental-import-std.cmake) - -project( - beman.{{cookiecutter.project_name}} - DESCRIPTION "{{cookiecutter.description}}" - LANGUAGES CXX -{% if cookiecutter._generating_exemplar %} - VERSION 2.4.1 -{% else %} - VERSION 0.1.0 -{% endif %} -) - -# [CMAKE.SKIP_TESTS] -option( - BEMAN_{{cookiecutter.project_name.upper()}}_BUILD_TESTS - "Enable building tests and test infrastructure. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." - ${PROJECT_IS_TOP_LEVEL} -) - -# [CMAKE.SKIP_EXAMPLES] -option( - BEMAN_{{cookiecutter.project_name.upper()}}_BUILD_EXAMPLES - "Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." - ${PROJECT_IS_TOP_LEVEL} -) - -option(BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES "Provide beman.{{cookiecutter.project_name}} as a C++ module" OFF) - -if(BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES) - set(CMAKE_CXX_SCAN_FOR_MODULES ON) -endif() - -configure_file( - "${PROJECT_SOURCE_DIR}/include/beman/{{cookiecutter.project_name}}/config_generated.hpp.in" - "${PROJECT_BINARY_DIR}/include/beman/{{cookiecutter.project_name}}/config_generated.hpp" - @ONLY -) - -# for find of beman_install_library and configure_build_telemetry -include(infra/cmake/beman-install-library.cmake) -include(infra/cmake/BuildTelemetryConfig.cmake) - -if(BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES) - add_library(beman.{{cookiecutter.project_name}} STATIC) -else() - add_library(beman.{{cookiecutter.project_name}} INTERFACE) -endif() -add_library(beman::{{cookiecutter.project_name}} ALIAS beman.{{cookiecutter.project_name}}) - -if(BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES) - target_sources( - beman.{{cookiecutter.project_name}} - PUBLIC - FILE_SET CXX_MODULES - FILE_SET HEADERS - BASE_DIRS - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_BINARY_DIR}/include" - ) - set_target_properties(beman.{{cookiecutter.project_name}} PROPERTIES CXX_MODULE_STD ON) - target_compile_features(beman.{{cookiecutter.project_name}} PUBLIC cxx_std_23) -else() - target_sources( - beman.{{cookiecutter.project_name}} - PUBLIC - FILE_SET HEADERS - BASE_DIRS - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_BINARY_DIR}/include" - ) - set_target_properties( - beman.{{cookiecutter.project_name}} - PROPERTIES VERIFY_INTERFACE_HEADER_SETS ${PROJECT_IS_TOP_LEVEL} - ) -endif() - -add_subdirectory(include/beman/{{cookiecutter.project_name}}) - -beman_install_library(beman.{{cookiecutter.project_name}} TARGETS beman.{{cookiecutter.project_name}}) -configure_build_telemetry() - -if(BEMAN_{{cookiecutter.project_name.upper()}}_BUILD_TESTS) - enable_testing() - add_subdirectory(tests/beman/{{cookiecutter.project_name}}) -endif() - -if(BEMAN_{{cookiecutter.project_name.upper()}}_BUILD_EXAMPLES) - add_subdirectory(examples) -endif() diff --git a/cookiecutter/{{cookiecutter.project_name}}/README.md b/cookiecutter/{{cookiecutter.project_name}}/README.md deleted file mode 100644 index 527c9086..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/README.md +++ /dev/null @@ -1,267 +0,0 @@ -{% if cookiecutter._generating_exemplar %} -# How to Use This Template - -To create a new Beman library, first click the "Use this template" dropdown in the -top-right and select "Create a new repository": - -
- -
- -This will create a new repository that's an exact copy of exemplar. The next step is to -customize it for your use case. - -To do so, execute the bash script `stamp.sh`. This script will prompt for parameters like -the new library's name, paper number, and description. Then it will replace your exemplar -copy with a stamped-out template containing these parameters and create a corresponding -git commit and branch: - -``` -$ ./stamp.sh - [1/6] project_name (my_project_name): example_library - [2/6] maintainer (your_github_username): your_username - [3/6] minimum_cpp_build_version (20): - [4/6] paper (PnnnnRr): P9999R9 - [5/6] description (Short project description.): - [6/6] Select unit_test_library - 1 - gtest - 2 - catch2 - Choose from [1/2] (1): -Switched to a new branch 'stamp' -Successfully stamped out exemplar template to the new branch 'stamp'. -Try 'git push origin stamp' to push the branch upstream, -then create a pull request. -``` - -From there, you can simply fill in all the remaining parts of the repository that are -labeled 'todo'. - -What follow is an example of a Beman library README. - -{% endif %} -# beman.{{cookiecutter.project_name}}: {{cookiecutter.description}} - - - - -[![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg)](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#the-beman-library-maturity-model) -[![Continuous Integration Tests](https://github.com/{{cookiecutter._owner}}/{{cookiecutter.project_name}}/actions/workflows/ci_tests.yml/badge.svg)](https://github.com/{{cookiecutter._owner}}/{{cookiecutter.project_name}}/actions/workflows/ci_tests.yml) -[![Lint Check (pre-commit)](https://github.com/{{cookiecutter._owner}}/{{cookiecutter.project_name}}/actions/workflows/pre-commit-check.yml/badge.svg)](https://github.com/{{cookiecutter._owner}}/{{cookiecutter.project_name}}/actions/workflows/pre-commit-check.yml) -[![Coverage](https://coveralls.io/repos/github/{{cookiecutter._owner}}/{{cookiecutter.project_name}}/badge.svg?branch=main)](https://coveralls.io/github/{{cookiecutter._owner}}/{{cookiecutter.project_name}}?branch=main) -![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg) -{% if cookiecutter._generating_exemplar %}[![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/4qEPK87va){% endif %} - - - -{% if cookiecutter._generating_exemplar %} -`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md). -This can be used as a template for those intending to write Beman libraries. -It may also find use as a minimal and modern C++ project structure. -{% else %} -`beman.{{cookiecutter.project_name}}` is (... TODO: description). -{% endif %} - -{% if cookiecutter._generating_exemplar %} -**Implements**: `std::identity` proposed in [Standard Library Concepts ({{cookiecutter.paper}})](https://wg21.link/{{cookiecutter.paper}}). -{% else %} -**Implements**: `std::todo` proposed in [TODO ({{cookiecutter.paper}})](https://wg21.link/{{cookiecutter.paper}}). -{% endif %} - -**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#under-development-and-not-yet-ready-for-production-use) - -## License - -`beman.{{cookiecutter.project_name}}` is licensed under the Apache License v2.0 with LLVM Exceptions. - -## Usage - -{% if cookiecutter._generating_exemplar %} -`std::identity` is a function object type whose `operator()` returns its argument unchanged. -`std::identity` serves as the default projection in constrained algorithms. -Its direct usage is usually not needed. - -### Usage: default projection in constrained algorithms - -The following code snippet illustrates how we can achieve a default projection using `beman::{{cookiecutter.project_name}}::identity`: - -```cpp -#include - -namespace exe = beman::{{cookiecutter.project_name}}; - -// Class with a pair of values. -struct Pair -{ - int n; - std::string s; - - // Output the pair in the form {n, s}. - // Used by the range-printer if no custom projection is provided (default: identity projection). - friend std::ostream &operator<<(std::ostream &os, const Pair &p) - { - return os << "Pair" << '{' << p.n << ", " << p.s << '}'; - } -}; - -// A range-printer that can print projected (modified) elements of a range. -// All the elements of the range are printed in the form {element1, element2, ...}. -// e.g., pairs with identity: Pair{1, one}, Pair{2, two}, Pair{3, three} -// e.g., pairs with custom projection: {1:one, 2:two, 3:three} -template -void print(const std::string_view rem, R &&range, Projection projection = exe::identity>) -{ - std::cout << rem << '{'; - std::ranges::for_each( - range, - [O = 0](const auto &o) mutable - { std::cout << (O++ ? ", " : "") << o; }, - projection); - std::cout << "}\n"; -}; - -int main() -{ - // A vector of pairs to print. - const std::vector pairs = { - {1, "one"}, - {2, "two"}, - {3, "three"}, - }; - - // Print the pairs using the default projection. - print("\tpairs with beman: ", pairs); - - return 0; -} - -``` - -{% else %} -TODO - -{% endif %} -Full runnable examples can be found in [`examples/`](examples/). - -## Dependencies - -### Build Environment - -This project requires at least the following to build: - -* A C++ compiler that conforms to the C++{{cookiecutter.minimum_cpp_build_version}} standard or greater -* CMake 3.30 or later -* (Test Only) GoogleTest - -You can disable building tests by setting CMake option `BEMAN_{{cookiecutter.project_name.upper()}}_BUILD_TESTS` to -`OFF` when configuring the project. - -### Supported Platforms - -| Compiler | Version | C++ Standards | Standard Library | -|------------|---------|---------------|-------------------| -| GCC | 16-13 | C++26-C++17 | libstdc++ | -| GCC | 12-11 | C++23-C++17 | libstdc++ | -| Clang | 22-19 | C++26-C++17 | libstdc++, libc++ | -| Clang | 18 | C++26-C++17 | libc++ | -| Clang | 18 | C++23-C++17 | libstdc++ | -| Clang | 17 | C++26-C++17 | libc++ | -| Clang | 17 | C++20, C++17 | libstdc++ | -| AppleClang | latest | C++26-C++17 | libc++ | -| MSVC | latest | C++23 | MSVC STL | - -## Development - -See the [Contributing Guidelines](CONTRIBUTING.md). - -## Integrate beman.{{cookiecutter.project_name}} into your project - -### Build - -You can build {{cookiecutter.project_name}} using a CMake workflow preset: - -```bash -cmake --workflow --preset gcc-release -``` - -To list available workflow presets, you can invoke: - -```bash -cmake --list-presets=workflow -``` - -For details on building beman.{{cookiecutter.project_name}} without using a CMake preset, refer to the -[Contributing Guidelines](CONTRIBUTING.md). - -### Installation - -#### Vcpkg - -The preferred way to install {{cookiecutter.project_name}} is via vcpkg. To do so, after installing vcpkg -itself, you need to add support for the Beman project's [vcpkg -registry](https://github.com/bemanproject/vcpkg-registry) by configuring a -`vcpkg-configuration.json` file (which {{cookiecutter.project_name}} [provides](vcpkg-configuration.json)). - -Then, simply run `vcpkg install beman-{{cookiecutter.project_name.replace("_", "-")}}`. - -#### Manual - -To install beman.{{cookiecutter.project_name}} globally after building with the `gcc-release` preset, you can -run: - -```bash -sudo cmake --install build/gcc-release -``` - -Alternatively, to install to a prefix, for example `/opt/beman`, you can run: - -```bash -sudo cmake --install build/gcc-release --prefix /opt/beman -``` - -This will generate the following directory structure: - -```txt -/opt/beman -├── include -│ └── beman -│ └── {{cookiecutter.project_name}} -│ ├── {{cookiecutter.project_name}}.hpp -│ └── ... -└── lib - └── cmake - └── beman.{{cookiecutter.project_name}} - ├── beman.{{cookiecutter.project_name}}-config-version.cmake - ├── beman.{{cookiecutter.project_name}}-config.cmake - └── beman.{{cookiecutter.project_name}}-targets.cmake -``` - -### CMake Configuration - -If you installed beman.{{cookiecutter.project_name}} to a prefix, you can specify that prefix to your CMake -project using `CMAKE_PREFIX_PATH`; for example, `-DCMAKE_PREFIX_PATH=/opt/beman`. - -You need to bring in the `beman.{{cookiecutter.project_name}}` package to define the `beman::{{cookiecutter.project_name}}` CMake -target: - -```cmake -find_package(beman.{{cookiecutter.project_name}} REQUIRED) -``` - -You will then need to add `beman::{{cookiecutter.project_name}}` to the link libraries of any libraries or -executables that include `beman.{{cookiecutter.project_name}}` headers. - -```cmake -target_link_libraries(yourlib PUBLIC beman::{{cookiecutter.project_name}}) -``` - -### Using beman.{{cookiecutter.project_name}} - -To use `beman.{{cookiecutter.project_name}}` in your C++ project, -include an appropriate `beman.{{cookiecutter.project_name}}` header from your source code. - -```c++ -#include -``` diff --git a/cookiecutter/{{cookiecutter.project_name}}/examples/identity_direct_usage.cpp b/cookiecutter/{{cookiecutter.project_name}}/examples/identity_direct_usage.cpp deleted file mode 100644 index 0284d36d..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/examples/identity_direct_usage.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{% set identity = "identity" if cookiecutter._generating_exemplar else "todo" %} - -#include -#include - -{% if cookiecutter._generating_exemplar %} -#if BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES() -import std; -#else - #include -#endif - -namespace exe = beman::{{cookiecutter.project_name}}; - -int main() { - std::cout << exe::identity()(2024) << '\n'; - return 0; -} -{% else %} -int main() { - // TODO -} -{% endif %} diff --git a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/CMakeLists.txt b/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/CMakeLists.txt deleted file mode 100644 index 352eb052..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{% set identity = "identity" if cookiecutter._generating_exemplar else "todo" %} - -if(BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES) - target_sources( - beman.{{cookiecutter.project_name}} - PUBLIC - FILE_SET CXX_MODULES FILES {{cookiecutter.project_name}}.cppm - FILE_SET HEADERS - FILES - config.hpp - {{cookiecutter.project_name}}.hpp - {{identity}}.hpp - "${PROJECT_BINARY_DIR}/include/beman/{{cookiecutter.project_name}}/config_generated.hpp" - ) -else() - target_sources( - beman.{{cookiecutter.project_name}} - PUBLIC - FILE_SET HEADERS - FILES - config.hpp - {{cookiecutter.project_name}}.hpp - {{identity}}.hpp - "${PROJECT_BINARY_DIR}/include/beman/{{cookiecutter.project_name}}/config_generated.hpp" - ) -endif() diff --git a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/config.hpp b/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/config.hpp deleted file mode 100644 index 7b86494a..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/config.hpp +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -#ifndef BEMAN_{{cookiecutter.project_name.upper()}}_CONFIG_HPP -#define BEMAN_{{cookiecutter.project_name.upper()}}_CONFIG_HPP - -#if !defined(__has_include) || __has_include() - #include -#else - #define BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES() 0 -#endif - -#endif diff --git a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/config_generated.hpp.in b/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/config_generated.hpp.in deleted file mode 100644 index 90941a1f..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/config_generated.hpp.in +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -#ifndef BEMAN_{{cookiecutter.project_name.upper()}}_CONFIG_GENERATED_HPP -#define BEMAN_{{cookiecutter.project_name.upper()}}_CONFIG_GENERATED_HPP - -#cmakedefine01 BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES() - -#endif diff --git a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}.cppm b/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}.cppm deleted file mode 100644 index 96b15487..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}.cppm +++ /dev/null @@ -1,11 +0,0 @@ -export module beman.{{cookiecutter.project_name}}; - -import std; - -#define BEMAN_{{cookiecutter.project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT -export { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Winclude-angled-in-module-purview" -#include -#pragma clang diagnostic pop -} diff --git a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}.hpp b/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}.hpp deleted file mode 100644 index 7df9cb80..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{% set identity = "identity" if cookiecutter._generating_exemplar else "todo" %} - -#ifndef BEMAN_{{cookiecutter.project_name.upper()}}_{{cookiecutter.project_name.upper()}}_HPP -#define BEMAN_{{cookiecutter.project_name.upper()}}_{{cookiecutter.project_name.upper()}}_HPP - -#include - -#if BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES() && !defined(BEMAN_{{cookiecutter.project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT) - -import beman.{{cookiecutter.project_name}}; - -#else - - #include - -#endif // BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES() && - // !defined(BEMAN_{{cookiecutter.project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT) - -#endif // BEMAN_{{cookiecutter.project_name.upper()}}_{{cookiecutter.project_name.upper()}}_HPP diff --git a/cookiecutter/{{cookiecutter.project_name}}/tests/beman/{{cookiecutter.project_name}}/CMakeLists.txt b/cookiecutter/{{cookiecutter.project_name}}/tests/beman/{{cookiecutter.project_name}}/CMakeLists.txt deleted file mode 100644 index 4450cd8d..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/tests/beman/{{cookiecutter.project_name}}/CMakeLists.txt +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{% set identity = "identity" if cookiecutter._generating_exemplar else "todo" %} - -{% if cookiecutter.unit_test_library == "gtest" %} -find_package(GTest REQUIRED) -{% elif cookiecutter.unit_test_library == "catch2" %} -find_package(Catch2 3 REQUIRED) -{% endif %} - -add_executable(beman.{{cookiecutter.project_name}}.tests.{{identity}}) -target_sources(beman.{{cookiecutter.project_name}}.tests.{{identity}} PRIVATE {{identity}}.test.cpp) -target_link_libraries( - beman.{{cookiecutter.project_name}}.tests.{{identity}} -{% if cookiecutter.unit_test_library == "gtest" %} - PRIVATE beman::{{cookiecutter.project_name}} GTest::gtest_main -{% elif cookiecutter.unit_test_library == "catch2" %} - PRIVATE beman::{{cookiecutter.project_name}} Catch2::Catch2WithMain -{% endif %} -) -if(BEMAN_EXEMPLAR_USE_MODULES) - set_target_properties( - beman.{{cookiecutter.project_name}}.tests.{{identity}} - PROPERTIES CXX_MODULE_STD ON - ) -endif() - -{% if cookiecutter.unit_test_library == "gtest" %} -include(GoogleTest) -gtest_discover_tests(beman.{{cookiecutter.project_name}}.tests.{{identity}} DISCOVERY_TIMEOUT 60) -{% elif cookiecutter.unit_test_library == "catch2" %} -include(Catch) -catch_discover_tests(beman.{{cookiecutter.project_name}}.tests.{{identity}}) -{% endif %} diff --git a/cookiecutter/{{cookiecutter.project_name}}/tests/beman/{{cookiecutter.project_name}}/identity.test.cpp b/cookiecutter/{{cookiecutter.project_name}}/tests/beman/{{cookiecutter.project_name}}/identity.test.cpp deleted file mode 100644 index 2b4c0697..00000000 --- a/cookiecutter/{{cookiecutter.project_name}}/tests/beman/{{cookiecutter.project_name}}/identity.test.cpp +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{% set identity = "identity" if cookiecutter._generating_exemplar else "todo" %} - -#include -{% if cookiecutter.unit_test_library == "gtest" %} -#include -{% elif cookiecutter.unit_test_library == "catch2" %} -#include -{% endif %} -#include - -{% if cookiecutter._generating_exemplar %} -#if BEMAN_EXEMPLAR_USE_MODULES() -import std; -#else - #include - #include -#endif - -namespace exe = beman::{{cookiecutter.project_name}}; - -{% if cookiecutter.unit_test_library == "gtest" %} -TEST(IdentityTest, call_identity_with_int) { -{% elif cookiecutter.unit_test_library == "catch2" %} -TEST_CASE("can call identity with int", "[{{cookiecutter.project_name}}::call_identity_with_int]") { -{% endif %} - for (int i = -100; i < 100; ++i) { -{% if cookiecutter.unit_test_library == "gtest" %} - EXPECT_EQ(i, exe::identity()(i)); -{% elif cookiecutter.unit_test_library == "catch2" %} - CHECK(i == exe::identity()(i)); -{% endif %} - } -} - -{% if cookiecutter.unit_test_library == "gtest" %} -TEST(IdentityTest, call_identity_with_custom_type) { -{% elif cookiecutter.unit_test_library == "catch2" %} -TEST_CASE("can call identity with custom type", "[{{cookiecutter.project_name}}::call_identity_with_custom_type]") { -{% endif %} - struct S { - int i; - }; - - for (int i = -100; i < 100; ++i) { - const S s{i}; - const S s_id = exe::identity()(s); -{% if cookiecutter.unit_test_library == "gtest" %} - EXPECT_EQ(s.i, s_id.i); -{% elif cookiecutter.unit_test_library == "catch2" %} - CHECK(s.i == s_id.i); -{% endif %} - } -} - -{% if cookiecutter.unit_test_library == "gtest" %} -TEST(IdentityTest, compare_std_vs_beman) { -{% elif cookiecutter.unit_test_library == "catch2" %} -TEST_CASE("compare std vs beman", "[{{cookiecutter.project_name}}::compare_std_vs_beman]") { -{% endif %} -// Requires: std::identity support. -#if defined(__cpp_lib_type_identity) - std::identity std_id; - exe::identity beman_id; - for (int i = -100; i < 100; ++i) { -{% if cookiecutter.unit_test_library == "gtest" %} - EXPECT_EQ(std_id(i), beman_id(i)); -{% elif cookiecutter.unit_test_library == "catch2" %} - CHECK(std_id(i) == beman_id(i)); -{% endif %} - } -#endif -} - -{% if cookiecutter.unit_test_library == "gtest" %} -TEST(IdentityTest, check_is_transparent) { -{% elif cookiecutter.unit_test_library == "catch2" %} -TEST_CASE("check is transparent", "[{{cookiecutter.project_name}}::check_is_transparent]") { -{% endif %} -// Requires: transparent operators support. -#if defined(__cpp_lib_transparent_operators) - - exe::identity id; - - const auto container = {1, 2, 3, 4, 5}; - auto it = std::find(std::begin(container), std::end(container), 3); -{% if cookiecutter.unit_test_library == "gtest" %} - EXPECT_EQ(3, *it); -{% elif cookiecutter.unit_test_library == "catch2" %} - CHECK(3 == *it); -{% endif %} - auto it_with_id = std::find(std::begin(container), std::end(container), id(3)); -{% if cookiecutter.unit_test_library == "gtest" %} - EXPECT_EQ(3, *it_with_id); - - EXPECT_EQ(it, it_with_id); -{% elif cookiecutter.unit_test_library == "catch2" %} - CHECK(3 == *it_with_id); - - CHECK(it == it_with_id); -{% endif %} -#endif -} -{% else %} -{% if cookiecutter.unit_test_library == "gtest" %} -TEST(TodoTest, todo) { -{% elif cookiecutter.unit_test_library == "catch2" %} -TEST_CASE("todo", "[{{cookiecutter.project_name}}::todo]") { -{% endif %} - const bool todo = true; -{% if cookiecutter.unit_test_library == "gtest" %} - EXPECT_TRUE(todo); -{% elif cookiecutter.unit_test_library == "catch2" %} - CHECK(todo); -{% endif %} -} -{% endif %} diff --git a/copier.yml b/copier.yml new file mode 100644 index 00000000..5bb42792 --- /dev/null +++ b/copier.yml @@ -0,0 +1,86 @@ +_min_copier_version: "9.14.3" +_subdirectory: template +_templates_suffix: .jinja +_envops: + trim_blocks: true + +template_src_path: + type: str + when: false + default: "{{ _copier_conf.src_path }}" + +template_commit: + type: str + when: false + default: "{{ _copier_conf.vcs_ref_hash }}" + +project_name: + type: str + help: Name of the generated repository and library. + default: my_project_name + validator: >- + {% if not (project_name | regex_search('^[a-z][a-z0-9_]*$')) %} + Use lowercase letters, digits, or underscores, and start with a letter. + {% endif %} + +maintainer: + type: str + help: GitHub username of the project maintainer. + default: your_github_username + +minimum_cpp_build_version: + type: str + help: Minimum C++ language version to require when building. + default: "20" + validator: >- + {% if minimum_cpp_build_version not in ['17', '20', '23', '26'] %} + Choose one of: 17, 20, 23, 26. + {% endif %} + +paper: + type: str + help: WG21 paper number associated with the library. + default: PnnnnRr + +description: + type: str + help: Short project description. + default: Short project description. + +unit_test_library: + type: str + help: Unit test library to configure. + choices: + - gtest + - catch2 + default: gtest + +generating_exemplar: + type: bool + when: false + default: "{{ generating_exemplar | default(false) }}" + +owner: + type: str + when: false + default: "{{ owner | default('bemanproject') }}" + +ci_tests_cron: + type: str + when: false + default: "{{ ci_tests_cron | default('') }}" + +pre_commit_update_cron: + type: str + when: false + default: "{{ pre_commit_update_cron | default('') }}" + +_tasks: + - >- + if [ "{{ generating_exemplar }}" != "True" ] && [ "{{ generating_exemplar }}" != "true" ]; then + mv "include/beman/{{ project_name }}/identity.hpp" "include/beman/{{ project_name }}/todo.hpp"; + mv "examples/identity_direct_usage.cpp" "examples/todo.cpp"; + rm "examples/identity_as_default_projection.cpp"; + mv "tests/beman/{{ project_name }}/identity.test.cpp" "tests/beman/{{ project_name }}/todo.test.cpp"; + git ls-remote https://github.com/bemanproject/exemplar.git HEAD | awk '{print $1}' > .exemplar_version; + fi diff --git a/copier/check_copier.sh b/copier/check_copier.sh new file mode 100755 index 00000000..7696f098 --- /dev/null +++ b/copier/check_copier.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +set -euo pipefail + +script_dir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") +repo_root=$(realpath "$script_dir/..") + +cleanup() { + if [[ -n "${work_dir:-}" && -d "$work_dir" ]]; then + rm -rf "$work_dir" + fi + if [[ -n "${copier_venv_path:-}" && -d "$copier_venv_path" ]]; then + rm -rf "$copier_venv_path" + fi +} + +setup_venv() { + local path="$1" ; shift + python3 -m venv "$path" + "$path/bin/python3" -m pip install copier >& /dev/null + COPIER_BIN="$path/bin/copier" +} + +template_commit() { + git -C "$repo_root" rev-parse HEAD +} + +prepare_template_source() { + local source_dir="$1" ; shift + local prepared_source + prepared_source=$(mktemp -d) + rsync -a \ + --exclude .git \ + --exclude build \ + --exclude .venv \ + "$source_dir/" "$prepared_source/" + printf '%s\n' "$prepared_source" +} + +stamp() { + local template_source="$1" ; shift + local output_dir="$1" ; shift + local unit_test_library="$1" ; shift + local generating_exemplar="$1" ; shift + local prepared_source + prepared_source=$(prepare_template_source "$template_source") + "$COPIER_BIN" copy \ + --trust \ + --defaults \ + -d template_src_path=https://github.com/bemanproject/exemplar.git \ + -d template_commit="$(template_commit)" \ + -d project_name=exemplar \ + -d maintainer=steve-downey \ + -d minimum_cpp_build_version=17 \ + -d paper=P0898R3 \ + -d description="A Beman Library Exemplar" \ + -d unit_test_library="$unit_test_library" \ + -d generating_exemplar="$generating_exemplar" \ + -d owner=bemanproject \ + -d ci_tests_cron="30 15 * * 6" \ + -d pre_commit_update_cron="0 16 * * 0" \ + "$prepared_source" \ + "$output_dir" \ + >& /dev/null + rm -rf "$prepared_source" +} + +check_consistency() { + local output_dir="$work_dir/default" + mkdir -p "$output_dir" + stamp "$repo_root" "$output_dir" "gtest" "true" + + local diff_path="$work_dir/default.diff" + diff -u -r \ + --exclude .git \ + --exclude build \ + --exclude .venv \ + --exclude template \ + --exclude copier \ + --exclude copier.yml \ + --exclude stamp.sh \ + --exclude images \ + --exclude .copier-answers.yml \ + --exclude copier_test.yml \ + --exclude catch2_exemplar_test.yml \ + --exclude todo_exemplar_test.yml \ + "$repo_root" "$output_dir" > "$diff_path" || true + + if [[ -s "$diff_path" ]] ; then + echo "Discrepancy between exemplar and copier output:" >&2 + cat "$diff_path" >&2 + exit 1 + fi +} + +check_templating() { + local output_dir="$work_dir/randomized" + local prepared_source + mkdir -p "$output_dir" + prepared_source=$(prepare_template_source "$repo_root") + "$COPIER_BIN" copy \ + --trust \ + --defaults \ + -d template_src_path=https://github.com/bemanproject/exemplar.git \ + -d template_commit="$(template_commit)" \ + -d project_name=rlzrmx9nfs \ + -d maintainer=octocat \ + -d minimum_cpp_build_version=17 \ + -d paper=P0898R3 \ + -d description="A Beman Library rlzrmx9nfs" \ + -d unit_test_library=gtest \ + -d generating_exemplar=false \ + -d owner=bemanproject \ + -d ci_tests_cron="30 15 * * 6" \ + -d pre_commit_update_cron="0 16 * * 0" \ + "$prepared_source" \ + "$output_dir" \ + >& /dev/null + rm -rf "$prepared_source" + + rm -rf "$output_dir/infra" + + local grep_path="$work_dir/randomized.grep" + grep \ + --dereference-recursive --context=5 --color=always \ + --exclude .copier-answers.yml \ + -e "exemplar" -e "identity" "$output_dir" > "$grep_path" || true + + if [[ -s "$grep_path" ]] ; then + echo 'Untemplated "exemplar" or "identity" in copier output:' >&2 + cat "$grep_path" >&2 + exit 1 + fi +} + +main() { + work_dir=$(mktemp -d) + copier_venv_path=$(mktemp -d) + trap cleanup EXIT + setup_venv "$copier_venv_path" + check_consistency + check_templating +} + +[[ "${BASH_SOURCE[0]}" != "${0}" ]] || main "$@" diff --git a/stamp.sh b/stamp.sh index 7dc9ee84..14ad8fbe 100755 --- a/stamp.sh +++ b/stamp.sh @@ -1,5 +1,5 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception { if [[ "$1" == "-h" || "$1" == "--help" ]] ; then @@ -8,11 +8,11 @@ This script is intended to be run on a fork of exemplar. - It sets up cookiecutter, runs it on the cookiecutter template, replaces the + It sets up Copier, runs it on the exemplar template, replaces the repository's current contents with the result, runs pre-commit, switches to a new branch 'stamp', and creates a git commit. - All parameters are passed through to the cookiecutter invocation. + All parameters are passed through to the Copier invocation. EOF fi set -eu @@ -22,23 +22,39 @@ EOF fi declare repo_dir=$(realpath $(dirname "$BASH_SOURCE")) cd "$repo_dir" - declare cookiecutter_venv_path - cookiecutter_venv_path=$(mktemp --directory --dry-run) - python3 -m venv "$cookiecutter_venv_path" - source "$cookiecutter_venv_path/bin/activate" - python3 -m pip install cookiecutter pre-commit >& /dev/null - declare cookiecutter_out_path - cookiecutter_out_path=$(mktemp --directory) - python3 -m cookiecutter "$repo_dir/cookiecutter" -o "$cookiecutter_out_path" "$@" + declare copier_venv_path + copier_venv_path=$(mktemp --directory --dry-run) + python3 -m venv "$copier_venv_path" + "$copier_venv_path/bin/python3" -m pip install copier pre-commit >& /dev/null + declare copier_source_path + copier_source_path=$(mktemp --directory) + declare copier_out_path + copier_out_path=$(mktemp --directory) + declare template_src_path=https://github.com/bemanproject/exemplar.git + declare template_commit + template_commit=$(git rev-parse HEAD) + rsync -a \ + --exclude .git \ + --exclude build \ + --exclude .venv \ + "$repo_dir/" "$copier_source_path/" + "$copier_venv_path/bin/copier" copy \ + --trust \ + -d template_src_path="$template_src_path" \ + -d template_commit="$template_commit" \ + "$@" \ + "$copier_source_path" \ + "$copier_out_path" \ + >& /dev/null git rm -rf . &>/dev/null - cp -r "$cookiecutter_out_path"/*/. . + cp -r "$copier_out_path"/. . git add . &>/dev/null - pre-commit run --all-files &>/dev/null || true + "$copier_venv_path/bin/pre-commit" run --all-files &>/dev/null || true git add . &>/dev/null git checkout -b stamp git commit -q -m "Stamp out exemplar template" echo "Successfully stamped out exemplar template to the new branch 'stamp'." echo "Try 'git push origin stamp' to push the branch upstream," echo "then create a pull request." - rm -r "$cookiecutter_venv_path" "$cookiecutter_out_path" + rm -r "$copier_venv_path" "$copier_source_path" "$copier_out_path" }; exit diff --git a/cookiecutter/{{cookiecutter.project_name}}/.beman-tidy.yaml b/template/.beman-tidy.yaml.jinja similarity index 91% rename from cookiecutter/{{cookiecutter.project_name}}/.beman-tidy.yaml rename to template/.beman-tidy.yaml.jinja index 527ca1e1..c54e2f32 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/.beman-tidy.yaml +++ b/template/.beman-tidy.yaml.jinja @@ -4,7 +4,7 @@ # Check documentation for beman-tidy here: # https://github.com/bemanproject/beman-tidy/blob/main/README.md -{% if cookiecutter._generating_exemplar %} +{% if generating_exemplar %} disabled_rules: - readme.title {% else %} diff --git a/cookiecutter/{{cookiecutter.project_name}}/.clang-format b/template/.clang-format.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/.clang-format rename to template/.clang-format.jinja diff --git a/template/.copier-answers.yml.jinja b/template/.copier-answers.yml.jinja new file mode 100644 index 00000000..cefabb93 --- /dev/null +++ b/template/.copier-answers.yml.jinja @@ -0,0 +1,8 @@ +# Standard answers and internal state managed by Copier +{{ _copier_answers | to_nice_yaml -}} + +# Hidden variables manually tracked because 'when: false' omits them from _copier_answers +generating_exemplar: {{ generating_exemplar | to_json }} +owner: {{ owner | to_json }} +ci_tests_cron: {{ ci_tests_cron | to_json }} +pre_commit_update_cron: {{ pre_commit_update_cron | to_json }} diff --git a/cookiecutter/{{cookiecutter.project_name}}/.gitattributes b/template/.gitattributes.jinja similarity index 66% rename from cookiecutter/{{cookiecutter.project_name}}/.gitattributes rename to template/.gitattributes.jinja index 793dce7d..9a7df7dd 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/.gitattributes +++ b/template/.gitattributes.jinja @@ -1,5 +1,6 @@ infra/** linguist-vendored -cookiecutter/** linguist-vendored +template/** linguist-vendored +copier/** linguist-vendored *.bib -linguist-detectable *.tex -linguist-detectable papers/* linguist-documentation diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/CODEOWNERS b/template/.github/CODEOWNERS.jinja similarity index 66% rename from cookiecutter/{{cookiecutter.project_name}}/.github/CODEOWNERS rename to template/.github/CODEOWNERS.jinja index cced7b37..566c8858 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/.github/CODEOWNERS +++ b/template/.github/CODEOWNERS.jinja @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{% if cookiecutter._generating_exemplar %} +{% if generating_exemplar %} * @ednolan @bretbrownjr @camio @dietmarkuehl @steve-downey @wusatosi {% else %} -* @{{ cookiecutter.maintainer }} +* @{{ maintainer }} {% endif %} diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/implementation-deficiency.md b/template/.github/ISSUE_TEMPLATE/implementation-deficiency.md.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/implementation-deficiency.md rename to template/.github/ISSUE_TEMPLATE/implementation-deficiency.md.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/infrastructure-issues.md b/template/.github/ISSUE_TEMPLATE/infrastructure-issues.md.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/infrastructure-issues.md rename to template/.github/ISSUE_TEMPLATE/infrastructure-issues.md.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/paper-discussion.md b/template/.github/ISSUE_TEMPLATE/paper-discussion.md.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/.github/ISSUE_TEMPLATE/paper-discussion.md rename to template/.github/ISSUE_TEMPLATE/paper-discussion.md.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/pull_request_template.md b/template/.github/pull_request_template.md.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/.github/pull_request_template.md rename to template/.github/pull_request_template.md.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/workflows/ci_tests.yml b/template/.github/workflows/ci_tests.yml.jinja similarity index 88% rename from cookiecutter/{{cookiecutter.project_name}}/.github/workflows/ci_tests.yml rename to template/.github/workflows/ci_tests.yml.jinja index ff7898f5..5525edc1 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/.github/workflows/ci_tests.yml +++ b/template/.github/workflows/ci_tests.yml.jinja @@ -9,7 +9,7 @@ on: pull_request: workflow_dispatch: schedule: - - cron: '{% if cookiecutter._ci_tests_cron %}{{ cookiecutter._ci_tests_cron }}{% else %}{{ range(0, 60) | random }} {{ range(13, 18) | random }} * * {{ range(0, 7) | random }}{% endif %}' + - cron: '{% if ci_tests_cron %}{{ ci_tests_cron }}{% else %}{{ range(0, 60) | random }} {{ range(13, 18) | random }} * * {{ range(0, 7) | random }}{% endif %}' concurrency: group: {% raw %}${{format('{0}:{1}', github.repository, github.ref)}}{% endraw %} @@ -49,7 +49,7 @@ jobs: "tests": [ "Debug.Default", "Release.Default", "Release.TSan", "Release.MaxSan", "Debug.Werror", - "Debug.Coverage", "Debug.-DBEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES=On" + "Debug.Coverage", "Debug.-DBEMAN_{{project_name.upper()}}_USE_MODULES=On" ] } ] @@ -58,7 +58,7 @@ jobs: "tests": [ { "stdlibs": ["libstdc++"], "tests": [ - "Release.Default", "Debug.-DBEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES=On" + "Release.Default", "Debug.-DBEMAN_{{project_name.upper()}}_USE_MODULES=On" ] } ] @@ -74,7 +74,7 @@ jobs: "tests": [ { "stdlibs": ["libstdc++"], "tests": [ - "Release.Default", "Debug.-DBEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES=On" + "Release.Default", "Debug.-DBEMAN_{{project_name.upper()}}_USE_MODULES=On" ] } ] @@ -109,7 +109,7 @@ jobs: "tests": [ "Debug.Default", "Release.Default", "Release.TSan", "Release.MaxSan", "Debug.Werror", - "Debug.-DBEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES=On" + "Debug.-DBEMAN_{{project_name.upper()}}_USE_MODULES=On" ] } ] @@ -118,7 +118,7 @@ jobs: "tests": [ { "stdlibs": ["libstdc++", "libc++"], "tests": [ - "Release.Default", "Debug.-DBEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES=On" + "Release.Default", "Debug.-DBEMAN_{{project_name.upper()}}_USE_MODULES=On" ] } ] @@ -177,7 +177,7 @@ jobs: { "stdlibs": ["stl"], "tests": [ "Debug.Default", "Release.Default", "Release.MaxSan", - "Debug.-DBEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES=On" + "Debug.-DBEMAN_{{project_name.upper()}}_USE_MODULES=On" ] } ] @@ -190,7 +190,7 @@ jobs: vcpkg-ci: uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-vcpkg-ci.yml@1.7.3 with: - port_name: beman-{{cookiecutter.project_name.replace('_', '-')}} + port_name: beman-{{project_name.replace('_', '-')}} feature_combinations: | [ {"features": {}}, diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/workflows/pre-commit-check.yml b/template/.github/workflows/pre-commit-check.yml.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/.github/workflows/pre-commit-check.yml rename to template/.github/workflows/pre-commit-check.yml.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/workflows/pre-commit-update.yml b/template/.github/workflows/pre-commit-update.yml.jinja similarity index 66% rename from cookiecutter/{{cookiecutter.project_name}}/.github/workflows/pre-commit-update.yml rename to template/.github/workflows/pre-commit-update.yml.jinja index 5ec76dc3..8d296cd4 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/.github/workflows/pre-commit-update.yml +++ b/template/.github/workflows/pre-commit-update.yml.jinja @@ -5,7 +5,7 @@ name: Weekly pre-commit autoupdate on: workflow_dispatch: schedule: - - cron: "{% if cookiecutter._pre_commit_update_cron %}{{ cookiecutter._pre_commit_update_cron }}{% else %}{{ range(0, 60) | random }} {{ range(13, 18) | random }} * * {{ range(0, 7) | random }}{% endif %}" + - cron: "{% if pre_commit_update_cron %}{{ pre_commit_update_cron }}{% else %}{{ range(0, 60) | random }} {{ range(13, 18) | random }} * * {{ range(0, 7) | random }}{% endif %}" {% raw -%} jobs: diff --git a/cookiecutter/{{cookiecutter.project_name}}/.github/workflows/vcpkg-release.yml b/template/.github/workflows/vcpkg-release.yml.jinja similarity index 83% rename from cookiecutter/{{cookiecutter.project_name}}/.github/workflows/vcpkg-release.yml rename to template/.github/workflows/vcpkg-release.yml.jinja index 2f3b3af9..c941243b 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/.github/workflows/vcpkg-release.yml +++ b/template/.github/workflows/vcpkg-release.yml.jinja @@ -8,7 +8,7 @@ jobs: vcpkg-release: uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-vcpkg-release.yml@1.7.3 with: - port_name: beman-{{cookiecutter.project_name.replace('_', '-')}} + port_name: beman-{{project_name.replace('_', '-')}} {%- raw %} secrets: VCPKG_REGISTRY_TOKEN: ${{ secrets.VCPKG_REGISTRY_TOKEN }} diff --git a/cookiecutter/{{cookiecutter.project_name}}/.gitignore b/template/.gitignore.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/.gitignore rename to template/.gitignore.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/.markdownlint.yaml b/template/.markdownlint.yaml.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/.markdownlint.yaml rename to template/.markdownlint.yaml.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/template/.pre-commit-config.yaml.jinja similarity index 94% rename from cookiecutter/{{cookiecutter.project_name}}/.pre-commit-config.yaml rename to template/.pre-commit-config.yaml.jinja index 34c954a6..dfdc94d9 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/template/.pre-commit-config.yaml.jinja @@ -40,7 +40,7 @@ repos: hooks: - id: codespell -{% if not cookiecutter._generating_exemplar %} +{% if not generating_exemplar %} # Beman Standard checking via beman-tidy - repo: https://github.com/bemanproject/beman-tidy rev: v0.5.1 @@ -48,4 +48,4 @@ repos: - id: beman-tidy {% endif %} -exclude: 'cookiecutter/|infra/|port/' +exclude: 'template/|copier/|infra/|port/' diff --git a/template/CMakeLists.txt.jinja b/template/CMakeLists.txt.jinja new file mode 100644 index 00000000..e70bf583 --- /dev/null +++ b/template/CMakeLists.txt.jinja @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.30...4.3) + +include(infra/cmake/enable-experimental-import-std.cmake) + +project( + beman.{{project_name}} + DESCRIPTION "{{description}}" + LANGUAGES CXX +{% if generating_exemplar %} + VERSION 2.4.0 +{% else %} + VERSION 0.1.0 +{% endif %} +) + +# [CMAKE.SKIP_TESTS] +option( + BEMAN_{{project_name.upper()}}_BUILD_TESTS + "Enable building tests and test infrastructure. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." + ${PROJECT_IS_TOP_LEVEL} +) + +# [CMAKE.SKIP_EXAMPLES] +option( + BEMAN_{{project_name.upper()}}_BUILD_EXAMPLES + "Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." + ${PROJECT_IS_TOP_LEVEL} +) + +option(BEMAN_{{project_name.upper()}}_USE_MODULES "Provide beman.{{project_name}} as a C++ module" OFF) + +if(BEMAN_{{project_name.upper()}}_USE_MODULES) + set(CMAKE_CXX_SCAN_FOR_MODULES ON) +endif() + +configure_file( + "${PROJECT_SOURCE_DIR}/include/beman/{{project_name}}/config_generated.hpp.in" + "${PROJECT_BINARY_DIR}/include/beman/{{project_name}}/config_generated.hpp" + @ONLY +) + +# for find of beman_install_library and configure_build_telemetry +include(infra/cmake/beman-install-library.cmake) +include(infra/cmake/BuildTelemetryConfig.cmake) + +if(BEMAN_{{project_name.upper()}}_USE_MODULES) + add_library(beman.{{project_name}} STATIC) +else() + add_library(beman.{{project_name}} INTERFACE) +endif() +add_library(beman::{{project_name}} ALIAS beman.{{project_name}}) + +if(BEMAN_{{project_name.upper()}}_USE_MODULES) + target_sources( + beman.{{project_name}} + PUBLIC + FILE_SET CXX_MODULES + FILE_SET HEADERS + BASE_DIRS + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include" + ) + set_target_properties(beman.{{project_name}} PROPERTIES CXX_MODULE_STD ON) + target_compile_features(beman.{{project_name}} PUBLIC cxx_std_23) +else() + target_sources( + beman.{{project_name}} + PUBLIC + FILE_SET HEADERS + BASE_DIRS + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include" + ) + set_target_properties( + beman.{{project_name}} + PROPERTIES VERIFY_INTERFACE_HEADER_SETS ${PROJECT_IS_TOP_LEVEL} + ) +endif() + +add_subdirectory(include/beman/{{project_name}}) + +beman_install_library(beman.{{project_name}} TARGETS beman.{{project_name}}) +configure_build_telemetry() + +if(BEMAN_{{project_name.upper()}}_BUILD_TESTS) + enable_testing() + add_subdirectory(tests/beman/{{project_name}}) +endif() + +if(BEMAN_{{project_name.upper()}}_BUILD_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/cookiecutter/{{cookiecutter.project_name}}/CMakePresets.json b/template/CMakePresets.json.jinja similarity index 99% rename from cookiecutter/{{cookiecutter.project_name}}/CMakePresets.json rename to template/CMakePresets.json.jinja index 7d70d496..1904359e 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/CMakePresets.json +++ b/template/CMakePresets.json.jinja @@ -7,7 +7,7 @@ "generator": "Ninja", "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { - "CMAKE_CXX_STANDARD": "{{cookiecutter.minimum_cpp_build_version}}", + "CMAKE_CXX_STANDARD": "{{minimum_cpp_build_version}}", "CMAKE_CXX_EXTENSIONS": false, "CMAKE_CXX_STANDARD_REQUIRED": true, "CMAKE_EXPORT_COMPILE_COMMANDS": true, diff --git a/cookiecutter/{{cookiecutter.project_name}}/CONTRIBUTING.md b/template/CONTRIBUTING.md.jinja similarity index 61% rename from cookiecutter/{{cookiecutter.project_name}}/CONTRIBUTING.md rename to template/CONTRIBUTING.md.jinja index d15f2843..1f1f9ddd 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/CONTRIBUTING.md +++ b/template/CONTRIBUTING.md.jinja @@ -43,7 +43,7 @@ that this requires GoogleTest to be installed. cmake \ -B build \ -S . \ - -DCMAKE_CXX_STANDARD={{cookiecutter.minimum_cpp_build_version}} \ + -DCMAKE_CXX_STANDARD={{minimum_cpp_build_version}} \ # Your extra arguments here. cmake --build build ctest --test-dir build @@ -85,7 +85,7 @@ vcpkg. ### FetchContent Instead of installing the project's dependencies via a package manager, you can optionally -configure beman.{{cookiecutter.project_name}} to fetch them automatically via CMake FetchContent. +configure beman.{{project_name}} to fetch them automatically via CMake FetchContent. To do so, specify `-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=./infra/cmake/use-fetch-content.cmake`. This will @@ -97,7 +97,7 @@ Example commands: cmake \ -B build \ -S . \ - -DCMAKE_CXX_STANDARD={{cookiecutter.minimum_cpp_build_version}} \ + -DCMAKE_CXX_STANDARD={{minimum_cpp_build_version}} \ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=./infra/cmake/use-fetch-content.cmake cmake --build build ctest --test-dir build @@ -106,34 +106,76 @@ ctest --test-dir build The file `./lockfile.json` configures the list of dependencies and versions that will be acquired by FetchContent. +{% if generating_exemplar %}## Developing The Copier Template + +This repository doubles as both the exemplar library and the Copier template used to stamp +new Beman libraries. + +The template workflow is organized as follows: + +* `copier.yml` defines the Copier questions, defaults, validation rules, and post-copy + tasks. +* `template/` contains the files rendered into generated projects. +* `stamp.sh` is the user-facing wrapper for stamping a forked exemplar repository into a + new project. +* `./copier/check_copier.sh` validates that exemplar regenerates cleanly and that a + non-exemplar project does not leak exemplar-specific names like `identity`. + +When testing local template changes, do not render directly from the Git checkout. The +Copier helper scripts intentionally render from a temporary snapshot with `.git`, `build`, +and `.venv` excluded so that validation uses the current worktree contents. + +Run the template checks with: + +```shell +./copier/check_copier.sh +``` + +If you need to smoke-test generated projects manually, start from the same validated flow: + +```shell +tmpdir=$(mktemp -d) +python3 -m venv "$tmpdir/venv" +"$tmpdir/venv/bin/python3" -m pip install copier +src=$(mktemp -d) +rsync -a --exclude .git --exclude build --exclude .venv ./ "$src/" +"$tmpdir/venv/bin/copier" copy --trust --defaults \ + -d template_src_path=https://github.com/bemanproject/exemplar.git \ + -d template_commit="$(git rev-parse HEAD)" \ + "$src" "$tmpdir/project" +``` + +Then configure and build the generated project as usual. +{% endif %} + ## Project-specific configure arguments -Project-specific options are prefixed with `BEMAN_{{cookiecutter.project_name.upper()}}`. +Project-specific options are prefixed with `BEMAN_{{project_name.upper()}}`. You can see the list of available options with: ```bash -cmake -LH -S . -B build | grep "BEMAN_{{cookiecutter.project_name.upper()}}" -C 2 +cmake -LH -S . -B build | grep "BEMAN_{{project_name.upper()}}" -C 2 ```
Some project-specific configure arguments -### `BEMAN_{{cookiecutter.project_name.upper()}}_BUILD_TESTS` +### `BEMAN_{{project_name.upper()}}_BUILD_TESTS` Enable building tests and test infrastructure. Default: `ON`. Values: `{ ON, OFF }`. -### `BEMAN_{{cookiecutter.project_name.upper()}}_BUILD_EXAMPLES` +### `BEMAN_{{project_name.upper()}}_BUILD_EXAMPLES` Enable building examples. Default: `ON`. Values: `{ ON, OFF }`. -### `BEMAN_{{cookiecutter.project_name.upper()}}_INSTALL_CONFIG_FILE_PACKAGE` +### `BEMAN_{{project_name.upper()}}_INSTALL_CONFIG_FILE_PACKAGE` Enable installing the CMake config file package. Default: `ON`. Values: `{ ON, OFF }`. -This is required so that users of `beman.{{cookiecutter.project_name}}` can use -`find_package(beman.{{cookiecutter.project_name}})` to locate the library. +This is required so that users of `beman.{{project_name}}` can use +`find_package(beman.{{project_name}})` to locate the library.
diff --git a/cookiecutter/{{cookiecutter.project_name}}/LICENSE b/template/LICENSE.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/LICENSE rename to template/LICENSE.jinja diff --git a/template/README.md.jinja b/template/README.md.jinja new file mode 100644 index 00000000..2f1a7625 --- /dev/null +++ b/template/README.md.jinja @@ -0,0 +1,342 @@ +{% if generating_exemplar %} +# How to Use This Template + +To create a new Beman library, first click the "Use this template" dropdown in the +top-right and select "Create a new repository": + +
+ GitHub Use this template button +
+ +This will create a new repository that's an exact copy of exemplar. The next step is to +customize it for your use case. + +This repository now uses Copier as its templating engine. The template inputs are defined +in `copier.yml`, the rendered project files live under `template/`, and +`./copier/check_copier.sh` verifies that the repository still round-trips through Copier +correctly. + +To do so, execute the bash script `stamp.sh`. This script will prompt for parameters like +the new library's name, paper number, and description. Then it will replace your exemplar +copy with a stamped-out template containing these parameters and create a corresponding +git commit and branch: + +```shell +$ ./stamp.sh + [1/6] project_name (my_project_name): example_library + [2/6] maintainer (your_github_username): your_username + [3/6] minimum_cpp_build_version (20): + [4/6] paper (PnnnnRr): P9999R9 + [5/6] description (Short project description.): + unit_test_library? Format: str + Choices: + - gtest + - catch2 + > gtest +Switched to a new branch 'stamp' +Successfully stamped out exemplar template to the new branch 'stamp'. +Try 'git push origin stamp' to push the branch upstream, +then create a pull request. +``` + +`stamp.sh` is a convenience wrapper around `copier copy`. It creates an isolated virtual +environment, renders from a temporary snapshot of the repository, runs `pre-commit`, and +commits the stamped result onto a `stamp` branch. The stamped tree now includes +`.copier-answers.yml`, so future `copier update` runs know which exemplar revision they +started from. + +## Rebasing An Older Exemplar Clone Onto Copier + +If your repository predates the Copier migration and was created as a plain GitHub +template copy, bootstrap a fresh stamped baseline at the original exemplar split point, +then rebase your project commits onto that baseline. + +Start from a clean worktree and make sure you can identify the branch you want to +migrate; the examples below assume `main`. + +```shell +git remote add exemplar https://github.com/bemanproject/exemplar.git +git fetch exemplar +base=$(git merge-base main exemplar/main) +git branch pre-copier-backup main +git switch --detach "$base" + +tmpdir=$(mktemp -d) +python3 -m venv "$tmpdir/venv" +"$tmpdir/venv/bin/python3" -m pip install copier pre-commit +"$tmpdir/venv/bin/copier" copy --trust --overwrite \ + -d project_name=your_project_name \ + -d maintainer=your_github_username \ + -d minimum_cpp_build_version=20 \ + -d paper=PnnnnRr \ + -d description="Short project description." \ + -d unit_test_library=gtest \ + -d generating_exemplar=false \ + -d owner=bemanproject \ + https://github.com/bemanproject/exemplar.git \ + . +"$tmpdir/venv/bin/pre-commit" run --all-files || true +git add . +git switch -c stamp +git commit -m "Bootstrap Copier template" + +git switch main +git rebase --rebase-merges --onto stamp "$base" main +``` + +After the rebase, commit the generated `.copier-answers.yml`. That file is what enables +future template updates with: + +```shell +copier update --trust +``` + +If you want to track a fork of exemplar rather than `bemanproject/exemplar`, edit +`_src_path` in `.copier-answers.yml` before your first `copier update`. + +From there, you can simply fill in all the remaining parts of the repository that are +labeled 'todo'. + +What follow is an example of a Beman library README. + +{% endif %} +{% if generating_exemplar %} +{% endif %} +# beman.{{project_name}}: {{description}} + + + + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/{{owner}}/{{project_name}}/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/{{owner}}/{{project_name}}/actions/workflows/pre-commit-check.yml/badge.svg) [![Coverage](https://coveralls.io/repos/github/{{owner}}/{{project_name}}/badge.svg?branch=main)](https://coveralls.io/github/{{owner}}/{{project_name}}?branch=main) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg){% if generating_exemplar %} [![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/4qEPK87va){% endif %} + + + +{% if generating_exemplar %} +`beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md). +This can be used as a template for those intending to write Beman libraries. +It may also find use as a minimal and modern C++ project structure. +{% else %} +`beman.{{project_name}}` is (... TODO: description). +{% endif %} + +{% if generating_exemplar %} +**Implements**: `std::identity` proposed in [Standard Library Concepts ({{paper}})](https://wg21.link/{{paper}}). +{% else %} +**Implements**: `std::todo` proposed in [TODO ({{paper}})](https://wg21.link/{{paper}}). +{% endif %} + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#under-development-and-not-yet-ready-for-production-use) + +## License + +`beman.{{project_name}}` is licensed under the Apache License v2.0 with LLVM Exceptions. + +## Usage + +{% if generating_exemplar %} +`std::identity` is a function object type whose `operator()` returns its argument unchanged. +`std::identity` serves as the default projection in constrained algorithms. +Its direct usage is usually not needed. + +### Usage: default projection in constrained algorithms + +The following code snippet illustrates how we can achieve a default projection using `beman::{{project_name}}::identity`: + +```cpp +#include + +namespace exe = beman::{{project_name}}; + +// Class with a pair of values. +struct Pair +{ + int n; + std::string s; + + // Output the pair in the form {n, s}. + // Used by the range-printer if no custom projection is provided (default: identity projection). + friend std::ostream &operator<<(std::ostream &os, const Pair &p) + { + return os << "Pair" << '{' << p.n << ", " << p.s << '}'; + } +}; + +// A range-printer that can print projected (modified) elements of a range. +// All the elements of the range are printed in the form {element1, element2, ...}. +// e.g., pairs with identity: Pair{1, one}, Pair{2, two}, Pair{3, three} +// e.g., pairs with custom projection: {1:one, 2:two, 3:three} +template +void print(const std::string_view rem, R &&range, Projection projection = exe::identity>) +{ + std::cout << rem << '{'; + std::ranges::for_each( + range, + [O = 0](const auto &o) mutable + { std::cout << (O++ ? ", " : "") << o; }, + projection); + std::cout << "}\n"; +}; + +int main() +{ + // A vector of pairs to print. + const std::vector pairs = { + {1, "one"}, + {2, "two"}, + {3, "three"}, + }; + + // Print the pairs using the default projection. + print("\tpairs with beman: ", pairs); + + return 0; +} + +``` + +{% else %} +TODO + +{% endif %} +Full runnable examples can be found in [`examples/`](examples/). + +## Dependencies + +### Build Environment + +This project requires at least the following to build: + +* A C++ compiler that conforms to the C++{{minimum_cpp_build_version}} standard or greater +* CMake 3.30 or later +* (Test Only) GoogleTest + +You can disable building tests by setting CMake option `BEMAN_{{project_name.upper()}}_BUILD_TESTS` to +`OFF` when configuring the project. + +### Supported Platforms + +| Compiler | Version | C++ Standards | Standard Library | +|------------|---------|---------------|-------------------| +| GCC | 16-13 | C++26-C++17 | libstdc++ | +| GCC | 12-11 | C++23-C++17 | libstdc++ | +| Clang | 22-19 | C++26-C++17 | libstdc++, libc++ | +| Clang | 18 | C++26-C++17 | libc++ | +| Clang | 18 | C++23-C++17 | libstdc++ | +| Clang | 17 | C++26-C++17 | libc++ | +| Clang | 17 | C++20, C++17 | libstdc++ | +| AppleClang | latest | C++26-C++17 | libc++ | +| MSVC | latest | C++23 | MSVC STL | + +## Development + +See the [Contributing Guidelines](CONTRIBUTING.md). + +{% if generating_exemplar %}### Template Maintenance + +If you are changing the template itself rather than developing the exemplar library, use +the Copier workflow directly: + +* Edit `copier.yml` for template questions, defaults, validators, and post-copy tasks. +* Edit `template/` for files that should be rendered into stamped projects. +* Run `./copier/check_copier.sh` to verify exemplar self-regeneration and non-exemplar + templating. +* Use `./stamp.sh` on a fork when you want to replace an exemplar clone with a stamped + project for real work. + +The consistency check and `stamp.sh` both render from a `.git`-free temporary snapshot of +the repository. That keeps local validation aligned with the current worktree contents, +rather than only the last committed Git state. +{% endif %} + +## Integrate beman.{{project_name}} into your project + +### Build + +You can build {{project_name}} using a CMake workflow preset: + +```bash +cmake --workflow --preset gcc-release +``` + +To list available workflow presets, you can invoke: + +```bash +cmake --list-presets=workflow +``` + +For details on building beman.{{project_name}} without using a CMake preset, refer to the +[Contributing Guidelines](CONTRIBUTING.md). + +### Installation + +#### Vcpkg + +The preferred way to install {{project_name}} is via vcpkg. To do so, after installing vcpkg +itself, you need to add support for the Beman project's [vcpkg +registry](https://github.com/bemanproject/vcpkg-registry) by configuring a +`vcpkg-configuration.json` file (which {{project_name}} [provides](vcpkg-configuration.json)). + +Then, simply run `vcpkg install beman-{{project_name.replace("_", "-")}}`. + +#### Manual + +To install beman.{{project_name}} globally after building with the `gcc-release` preset, you can +run: + +```bash +sudo cmake --install build/gcc-release +``` + +Alternatively, to install to a prefix, for example `/opt/beman`, you can run: + +```bash +sudo cmake --install build/gcc-release --prefix /opt/beman +``` + +This will generate the following directory structure: + +```txt +/opt/beman +├── include +│ └── beman +│ └── {{project_name}} +│ ├── {{project_name}}.hpp +│ └── ... +└── lib + └── cmake + └── beman.{{project_name}} + ├── beman.{{project_name}}-config-version.cmake + ├── beman.{{project_name}}-config.cmake + └── beman.{{project_name}}-targets.cmake +``` + +### CMake Configuration + +If you installed beman.{{project_name}} to a prefix, you can specify that prefix to your CMake +project using `CMAKE_PREFIX_PATH`; for example, `-DCMAKE_PREFIX_PATH=/opt/beman`. + +You need to bring in the `beman.{{project_name}}` package to define the `beman::{{project_name}}` CMake +target: + +```cmake +find_package(beman.{{project_name}} REQUIRED) +``` + +You will then need to add `beman::{{project_name}}` to the link libraries of any libraries or +executables that include `beman.{{project_name}}` headers. + +```cmake +target_link_libraries(yourlib PUBLIC beman::{{project_name}}) +``` + +### Using beman.{{project_name}} + +To use `beman.{{project_name}}` in your C++ project, +include an appropriate `beman.{{project_name}}` header from your source code. + +```c++ +#include +``` diff --git a/cookiecutter/{{cookiecutter.project_name}}/examples/CMakeLists.txt b/template/examples/CMakeLists.txt.jinja similarity index 60% rename from cookiecutter/{{cookiecutter.project_name}}/examples/CMakeLists.txt rename to template/examples/CMakeLists.txt.jinja index fddbf1fd..dc289913 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/examples/CMakeLists.txt +++ b/template/examples/CMakeLists.txt.jinja @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{% if cookiecutter._generating_exemplar %} +{% if generating_exemplar %} set(ALL_EXAMPLES identity_direct_usage) # Example `identity_as_default_projection` need ranges support: @@ -22,15 +22,15 @@ set(ALL_EXAMPLES todo) message("Examples to be built: ${ALL_EXAMPLES}") foreach(example ${ALL_EXAMPLES}) - add_executable(beman.{{cookiecutter.project_name}}.examples.${example}) - target_sources(beman.{{cookiecutter.project_name}}.examples.${example} PRIVATE ${example}.cpp) + add_executable(beman.{{project_name}}.examples.${example}) + target_sources(beman.{{project_name}}.examples.${example} PRIVATE ${example}.cpp) target_link_libraries( - beman.{{cookiecutter.project_name}}.examples.${example} - PRIVATE beman::{{cookiecutter.project_name}} + beman.{{project_name}}.examples.${example} + PRIVATE beman::{{project_name}} ) - if(BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES) + if(BEMAN_{{project_name.upper()}}_USE_MODULES) set_target_properties( - beman.{{cookiecutter.project_name}}.examples.${example} + beman.{{project_name}}.examples.${example} PROPERTIES CXX_MODULE_STD ON ) endif() diff --git a/cookiecutter/{{cookiecutter.project_name}}/examples/identity_as_default_projection.cpp b/template/examples/identity_as_default_projection.cpp.jinja similarity index 87% rename from cookiecutter/{{cookiecutter.project_name}}/examples/identity_as_default_projection.cpp rename to template/examples/identity_as_default_projection.cpp.jinja index 20a95e8b..3007dfb5 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/examples/identity_as_default_projection.cpp +++ b/template/examples/identity_as_default_projection.cpp.jinja @@ -1,12 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -// This example demonstrates the usage of beman::{{cookiecutter.project_name}}::identity as a default projection in a range-printer. +// This example demonstrates the usage of beman::{{project_name}}::identity as a default projection in a range-printer. // Requires: range support (C++20) and std::identity support (C++20). -#include -#include +#include +#include -#if BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES() +#if BEMAN_{{project_name.upper()}}_USE_MODULES() import std; #else #include @@ -17,7 +17,7 @@ import std; #include #endif -namespace exe = beman::{{cookiecutter.project_name}}; +namespace exe = beman::{{project_name}}; // Class with a pair of values. struct Pair { diff --git a/template/examples/identity_direct_usage.cpp.jinja b/template/examples/identity_direct_usage.cpp.jinja new file mode 100644 index 00000000..e1985ad2 --- /dev/null +++ b/template/examples/identity_direct_usage.cpp.jinja @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +{% set identity = "identity" if generating_exemplar else "todo" %} + +#include +#include + +{% if generating_exemplar %} +#if BEMAN_{{project_name.upper()}}_USE_MODULES() +import std; +#else + #include +#endif + +namespace exe = beman::{{project_name}}; + +int main() { + std::cout << exe::identity()(2024) << '\n'; + return 0; +} +{% else %} +int main() { + // TODO +} +{% endif %} diff --git a/template/include/beman/{{ project_name }}/CMakeLists.txt.jinja b/template/include/beman/{{ project_name }}/CMakeLists.txt.jinja new file mode 100644 index 00000000..8853f4e3 --- /dev/null +++ b/template/include/beman/{{ project_name }}/CMakeLists.txt.jinja @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +{% set identity = "identity" if generating_exemplar else "todo" %} + +if(BEMAN_{{project_name.upper()}}_USE_MODULES) + target_sources( + beman.{{project_name}} + PUBLIC + FILE_SET CXX_MODULES FILES {{project_name}}.cppm + FILE_SET HEADERS + FILES + config.hpp + {{project_name}}.hpp + {{identity}}.hpp + "${PROJECT_BINARY_DIR}/include/beman/{{project_name}}/config_generated.hpp" + ) +else() + target_sources( + beman.{{project_name}} + PUBLIC + FILE_SET HEADERS + FILES + config.hpp + {{project_name}}.hpp + {{identity}}.hpp + "${PROJECT_BINARY_DIR}/include/beman/{{project_name}}/config_generated.hpp" + ) +endif() diff --git a/template/include/beman/{{ project_name }}/config.hpp.jinja b/template/include/beman/{{ project_name }}/config.hpp.jinja new file mode 100644 index 00000000..7761e19b --- /dev/null +++ b/template/include/beman/{{ project_name }}/config.hpp.jinja @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef BEMAN_{{project_name.upper()}}_CONFIG_HPP +#define BEMAN_{{project_name.upper()}}_CONFIG_HPP + +#if !defined(__has_include) || __has_include() + #include +#else + #define BEMAN_{{project_name.upper()}}_USE_MODULES() 0 +#endif + +#endif diff --git a/template/include/beman/{{ project_name }}/config_generated.hpp.in.jinja b/template/include/beman/{{ project_name }}/config_generated.hpp.in.jinja new file mode 100644 index 00000000..6b296ed1 --- /dev/null +++ b/template/include/beman/{{ project_name }}/config_generated.hpp.in.jinja @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef BEMAN_{{project_name.upper()}}_CONFIG_GENERATED_HPP +#define BEMAN_{{project_name.upper()}}_CONFIG_GENERATED_HPP + +#cmakedefine01 BEMAN_{{project_name.upper()}}_USE_MODULES() + +#endif diff --git a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/identity.hpp b/template/include/beman/{{ project_name }}/identity.hpp.jinja similarity index 52% rename from cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/identity.hpp rename to template/include/beman/{{ project_name }}/identity.hpp.jinja index bdea366c..71d8773a 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/include/beman/{{cookiecutter.project_name}}/identity.hpp +++ b/template/include/beman/{{ project_name }}/identity.hpp.jinja @@ -1,18 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{% set identity = "identity" if cookiecutter._generating_exemplar else "todo" %} +{% set identity = "identity" if generating_exemplar else "todo" %} -#ifndef BEMAN_{{cookiecutter.project_name.upper()}}_{{identity.upper()}}_HPP -#define BEMAN_{{cookiecutter.project_name.upper()}}_{{identity.upper()}}_HPP +#ifndef BEMAN_{{project_name.upper()}}_{{identity.upper()}}_HPP +#define BEMAN_{{project_name.upper()}}_{{identity.upper()}}_HPP -#include +#include -#if BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES() && !defined(BEMAN_{{cookiecutter.project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT) +#if BEMAN_{{project_name.upper()}}_USE_MODULES() && !defined(BEMAN_{{project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT) -import beman.{{cookiecutter.project_name}}; +import beman.{{project_name}}; #else -{% if cookiecutter._generating_exemplar %} +{% if generating_exemplar %} // C++ Standard Library: std::identity equivalent. // See https://eel.is/c++draft/func.identity: // @@ -35,9 +35,9 @@ import beman.{{cookiecutter.project_name}}; #endif {% endif %} -namespace beman::{{cookiecutter.project_name}} { +namespace beman::{{project_name}} { -{% if cookiecutter._generating_exemplar %} +{% if generating_exemplar %} struct __is_transparent; // not defined // A function object that returns its argument unchanged. @@ -55,9 +55,9 @@ struct identity { // TODO {% endif %} -} // namespace beman::{{cookiecutter.project_name}} +} // namespace beman::{{project_name}} -#endif // BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES() && - // !defined(BEMAN_{{cookiecutter.project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT) +#endif // BEMAN_{{project_name.upper()}}_USE_MODULES() && + // !defined(BEMAN_{{project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT) -#endif // BEMAN_{{cookiecutter.project_name.upper()}}_{{identity.upper()}}_HPP +#endif // BEMAN_{{project_name.upper()}}_{{identity.upper()}}_HPP diff --git a/template/include/beman/{{ project_name }}/{{ project_name }}.cppm.jinja b/template/include/beman/{{ project_name }}/{{ project_name }}.cppm.jinja new file mode 100644 index 00000000..ef9b3751 --- /dev/null +++ b/template/include/beman/{{ project_name }}/{{ project_name }}.cppm.jinja @@ -0,0 +1,11 @@ +export module beman.{{project_name}}; + +import std; + +#define BEMAN_{{project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT +export { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Winclude-angled-in-module-purview" +#include +#pragma clang diagnostic pop +} diff --git a/template/include/beman/{{ project_name }}/{{ project_name }}.hpp.jinja b/template/include/beman/{{ project_name }}/{{ project_name }}.hpp.jinja new file mode 100644 index 00000000..6de86a51 --- /dev/null +++ b/template/include/beman/{{ project_name }}/{{ project_name }}.hpp.jinja @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +{% set identity = "identity" if generating_exemplar else "todo" %} + +#ifndef BEMAN_{{project_name.upper()}}_{{project_name.upper()}}_HPP +#define BEMAN_{{project_name.upper()}}_{{project_name.upper()}}_HPP + +#include + +#if BEMAN_{{project_name.upper()}}_USE_MODULES() && !defined(BEMAN_{{project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT) + +import beman.{{project_name}}; + +#else + + #include + +#endif // BEMAN_{{project_name.upper()}}_USE_MODULES() && + // !defined(BEMAN_{{project_name.upper()}}_INCLUDED_FROM_INTERFACE_UNIT) + +#endif // BEMAN_{{project_name.upper()}}_{{project_name.upper()}}_HPP diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/.beman_submodule b/template/infra/.beman_submodule.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/.beman_submodule rename to template/infra/.beman_submodule.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/.github/CODEOWNERS b/template/infra/.github/CODEOWNERS.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/.github/CODEOWNERS rename to template/infra/.github/CODEOWNERS.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/.github/workflows/pre-commit.yml b/template/infra/.github/workflows/pre-commit.yml.jinja similarity index 98% rename from cookiecutter/{{cookiecutter.project_name}}/infra/.github/workflows/pre-commit.yml rename to template/infra/.github/workflows/pre-commit.yml.jinja index 7051c132..5f9ae08c 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/infra/.github/workflows/pre-commit.yml +++ b/template/infra/.github/workflows/pre-commit.yml.jinja @@ -1,4 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +{% raw -%} + name: Lint Check (pre-commit) on: @@ -77,3 +79,4 @@ jobs: tool_name: pre-commit level: warning reviewdog_flags: "-fail-level=error" +{% endraw %} diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml b/template/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml.jinja similarity index 98% rename from cookiecutter/{{cookiecutter.project_name}}/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml rename to template/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml.jinja index 024a51f4..4e3117aa 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml +++ b/template/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml.jinja @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - +{% raw %} name: 'Beman issue creation workflow' on: workflow_call: @@ -26,3 +26,4 @@ jobs: fi env: GH_TOKEN: ${{ github.token }} +{% endraw %} diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/.gitignore b/template/infra/.gitignore.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/.gitignore rename to template/infra/.gitignore.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/.pre-commit-config.yaml b/template/infra/.pre-commit-config.yaml.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/.pre-commit-config.yaml rename to template/infra/.pre-commit-config.yaml.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/LICENSE b/template/infra/LICENSE.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/LICENSE rename to template/infra/LICENSE.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/README.md b/template/infra/README.md.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/README.md rename to template/infra/README.md.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/BuildTelemetry.cmake b/template/infra/cmake/BuildTelemetry.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/BuildTelemetry.cmake rename to template/infra/cmake/BuildTelemetry.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/BuildTelemetryConfig.cmake b/template/infra/cmake/BuildTelemetryConfig.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/BuildTelemetryConfig.cmake rename to template/infra/cmake/BuildTelemetryConfig.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/Config.cmake.in b/template/infra/cmake/Config.cmake.in.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/Config.cmake.in rename to template/infra/cmake/Config.cmake.in.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/appleclang-toolchain.cmake b/template/infra/cmake/appleclang-toolchain.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/appleclang-toolchain.cmake rename to template/infra/cmake/appleclang-toolchain.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/beman-install-library.cmake b/template/infra/cmake/beman-install-library.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/beman-install-library.cmake rename to template/infra/cmake/beman-install-library.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/enable-experimental-import-std.cmake b/template/infra/cmake/enable-experimental-import-std.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/enable-experimental-import-std.cmake rename to template/infra/cmake/enable-experimental-import-std.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/gnu-toolchain.cmake b/template/infra/cmake/gnu-toolchain.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/gnu-toolchain.cmake rename to template/infra/cmake/gnu-toolchain.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/llvm-libc++-toolchain.cmake b/template/infra/cmake/llvm-libc++-toolchain.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/llvm-libc++-toolchain.cmake rename to template/infra/cmake/llvm-libc++-toolchain.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/llvm-toolchain.cmake b/template/infra/cmake/llvm-toolchain.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/llvm-toolchain.cmake rename to template/infra/cmake/llvm-toolchain.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/msvc-toolchain.cmake b/template/infra/cmake/msvc-toolchain.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/msvc-toolchain.cmake rename to template/infra/cmake/msvc-toolchain.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/telemetry.sh b/template/infra/cmake/telemetry.sh.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/telemetry.sh rename to template/infra/cmake/telemetry.sh.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/infra/cmake/use-fetch-content.cmake b/template/infra/cmake/use-fetch-content.cmake.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/infra/cmake/use-fetch-content.cmake rename to template/infra/cmake/use-fetch-content.cmake.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/lockfile.json b/template/lockfile.json.jinja similarity index 82% rename from cookiecutter/{{cookiecutter.project_name}}/lockfile.json rename to template/lockfile.json.jinja index f3388cff..cbd6495e 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/lockfile.json +++ b/template/lockfile.json.jinja @@ -1,6 +1,6 @@ { "dependencies": [ -{% if cookiecutter.unit_test_library == "gtest" %} +{% if unit_test_library == "gtest" %} { "name": "googletest", "package_name": "GTest", @@ -10,7 +10,7 @@ "INSTALL_GTEST": "OFF" } } -{% elif cookiecutter.unit_test_library == "catch2" %} +{% elif unit_test_library == "catch2" %} { "name": "Catch2", "package_name": "Catch2", diff --git a/cookiecutter/{{cookiecutter.project_name}}/port/portfile.cmake.in b/template/port/portfile.cmake.in.jinja similarity index 61% rename from cookiecutter/{{cookiecutter.project_name}}/port/portfile.cmake.in rename to template/port/portfile.cmake.in.jinja index 1f13b31b..107c2f44 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/port/portfile.cmake.in +++ b/template/port/portfile.cmake.in.jinja @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH - REPO bemanproject/{{cookiecutter.project_name}} + REPO bemanproject/{{project_name}} REF "v@VERSION@" SHA512 @SHA512@ HEAD_REF main @@ -10,22 +10,22 @@ vcpkg_from_github( vcpkg_check_features( OUT_FEATURE_OPTIONS FEATURE_OPTIONS FEATURES - modules BEMAN_{{cookiecutter.project_name.upper()}}_USE_MODULES + modules BEMAN_{{project_name.upper()}}_USE_MODULES ) vcpkg_cmake_configure( SOURCE_PATH "${SOURCE_PATH}" OPTIONS ${FEATURE_OPTIONS} - -DBEMAN_{{cookiecutter.project_name.upper()}}_BUILD_TESTS=OFF - -DBEMAN_{{cookiecutter.project_name.upper()}}_BUILD_EXAMPLES=OFF + -DBEMAN_{{project_name.upper()}}_BUILD_TESTS=OFF + -DBEMAN_{{project_name.upper()}}_BUILD_EXAMPLES=OFF ) vcpkg_cmake_install() vcpkg_cmake_config_fixup( - PACKAGE_NAME beman.{{cookiecutter.project_name}} - CONFIG_PATH lib/cmake/beman.{{cookiecutter.project_name}} + PACKAGE_NAME beman.{{project_name}} + CONFIG_PATH lib/cmake/beman.{{project_name}} ) if(NOT "modules" IN_LIST FEATURES) diff --git a/cookiecutter/{{cookiecutter.project_name}}/port/vcpkg.json.in b/template/port/vcpkg.json.in.jinja similarity index 50% rename from cookiecutter/{{cookiecutter.project_name}}/port/vcpkg.json.in rename to template/port/vcpkg.json.in.jinja index 81a58a5a..36836ffe 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/port/vcpkg.json.in +++ b/template/port/vcpkg.json.in.jinja @@ -1,8 +1,8 @@ { - "name": "beman-{{cookiecutter.project_name.replace('_', '-')}}", + "name": "beman-{{project_name.replace('_', '-')}}", "version-semver": "@VERSION@", - "description": "{{cookiecutter.description}}", - "homepage": "https://github.com/bemanproject/{{cookiecutter.project_name}}", + "description": "{{description}}", + "homepage": "https://github.com/bemanproject/{{project_name}}", "license": "Apache-2.0 WITH LLVM-exception", "dependencies": [ { @@ -16,7 +16,7 @@ ], "features": { "modules": { - "description": "Provide beman.{{cookiecutter.project_name}} as a C++ module" + "description": "Provide beman.{{project_name}} as a C++ module" } } } diff --git a/template/tests/beman/{{ project_name }}/CMakeLists.txt.jinja b/template/tests/beman/{{ project_name }}/CMakeLists.txt.jinja new file mode 100644 index 00000000..d36cf0a2 --- /dev/null +++ b/template/tests/beman/{{ project_name }}/CMakeLists.txt.jinja @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +{% set identity = "identity" if generating_exemplar else "todo" %} + +{% if unit_test_library == "gtest" %} +find_package(GTest REQUIRED) +{% elif unit_test_library == "catch2" %} +find_package(Catch2 3 REQUIRED) +{% endif %} + +add_executable(beman.{{project_name}}.tests.{{identity}}) +target_sources(beman.{{project_name}}.tests.{{identity}} PRIVATE {{identity}}.test.cpp) +target_link_libraries( + beman.{{project_name}}.tests.{{identity}} +{% if unit_test_library == "gtest" %} + PRIVATE beman::{{project_name}} GTest::gtest_main +{% elif unit_test_library == "catch2" %} + PRIVATE beman::{{project_name}} Catch2::Catch2WithMain +{% endif %} +) +if(BEMAN_EXEMPLAR_USE_MODULES) + set_target_properties( + beman.{{project_name}}.tests.{{identity}} + PROPERTIES CXX_MODULE_STD ON + ) +endif() + +{% if unit_test_library == "gtest" %} +include(GoogleTest) +gtest_discover_tests(beman.{{project_name}}.tests.{{identity}} DISCOVERY_TIMEOUT 60) +{% elif unit_test_library == "catch2" %} +include(Catch) +catch_discover_tests(beman.{{project_name}}.tests.{{identity}}) +{% endif %} diff --git a/template/tests/beman/{{ project_name }}/identity.test.cpp.jinja b/template/tests/beman/{{ project_name }}/identity.test.cpp.jinja new file mode 100644 index 00000000..57b6581b --- /dev/null +++ b/template/tests/beman/{{ project_name }}/identity.test.cpp.jinja @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +{% set identity = "identity" if generating_exemplar else "todo" %} + +#include +{% if unit_test_library == "gtest" %} +#include +{% elif unit_test_library == "catch2" %} +#include +{% endif %} +#include + +{% if generating_exemplar %} +#if BEMAN_EXEMPLAR_USE_MODULES() +import std; +#else + #include + #include +#endif + +namespace exe = beman::{{project_name}}; + +{% if unit_test_library == "gtest" %} +TEST(IdentityTest, call_identity_with_int) { +{% elif unit_test_library == "catch2" %} +TEST_CASE("can call identity with int", "[{{project_name}}::call_identity_with_int]") { +{% endif %} + for (int i = -100; i < 100; ++i) { +{% if unit_test_library == "gtest" %} + EXPECT_EQ(i, exe::identity()(i)); +{% elif unit_test_library == "catch2" %} + CHECK(i == exe::identity()(i)); +{% endif %} + } +} + +{% if unit_test_library == "gtest" %} +TEST(IdentityTest, call_identity_with_custom_type) { +{% elif unit_test_library == "catch2" %} +TEST_CASE("can call identity with custom type", "[{{project_name}}::call_identity_with_custom_type]") { +{% endif %} + struct S { + int i; + }; + + for (int i = -100; i < 100; ++i) { + const S s{i}; + const S s_id = exe::identity()(s); +{% if unit_test_library == "gtest" %} + EXPECT_EQ(s.i, s_id.i); +{% elif unit_test_library == "catch2" %} + CHECK(s.i == s_id.i); +{% endif %} + } +} + +{% if unit_test_library == "gtest" %} +TEST(IdentityTest, compare_std_vs_beman) { +{% elif unit_test_library == "catch2" %} +TEST_CASE("compare std vs beman", "[{{project_name}}::compare_std_vs_beman]") { +{% endif %} +// Requires: std::identity support. +#if defined(__cpp_lib_type_identity) + std::identity std_id; + exe::identity beman_id; + for (int i = -100; i < 100; ++i) { +{% if unit_test_library == "gtest" %} + EXPECT_EQ(std_id(i), beman_id(i)); +{% elif unit_test_library == "catch2" %} + CHECK(std_id(i) == beman_id(i)); +{% endif %} + } +#endif +} + +{% if unit_test_library == "gtest" %} +TEST(IdentityTest, check_is_transparent) { +{% elif unit_test_library == "catch2" %} +TEST_CASE("check is transparent", "[{{project_name}}::check_is_transparent]") { +{% endif %} +// Requires: transparent operators support. +#if defined(__cpp_lib_transparent_operators) + + exe::identity id; + + const auto container = {1, 2, 3, 4, 5}; + auto it = std::find(std::begin(container), std::end(container), 3); +{% if unit_test_library == "gtest" %} + EXPECT_EQ(3, *it); +{% elif unit_test_library == "catch2" %} + CHECK(3 == *it); +{% endif %} + auto it_with_id = std::find(std::begin(container), std::end(container), id(3)); +{% if unit_test_library == "gtest" %} + EXPECT_EQ(3, *it_with_id); + + EXPECT_EQ(it, it_with_id); +{% elif unit_test_library == "catch2" %} + CHECK(3 == *it_with_id); + + CHECK(it == it_with_id); +{% endif %} +#endif +} +{% else %} +{% if unit_test_library == "gtest" %} +TEST(TodoTest, todo) { +{% elif unit_test_library == "catch2" %} +TEST_CASE("todo", "[{{project_name}}::todo]") { +{% endif %} + const bool todo = true; +{% if unit_test_library == "gtest" %} + EXPECT_TRUE(todo); +{% elif unit_test_library == "catch2" %} + CHECK(todo); +{% endif %} +} +{% endif %} diff --git a/cookiecutter/{{cookiecutter.project_name}}/vcpkg-configuration.json b/template/vcpkg-configuration.json.jinja similarity index 100% rename from cookiecutter/{{cookiecutter.project_name}}/vcpkg-configuration.json rename to template/vcpkg-configuration.json.jinja diff --git a/cookiecutter/{{cookiecutter.project_name}}/vcpkg.json b/template/vcpkg.json.jinja similarity index 54% rename from cookiecutter/{{cookiecutter.project_name}}/vcpkg.json rename to template/vcpkg.json.jinja index d55695b8..24ae4797 100644 --- a/cookiecutter/{{cookiecutter.project_name}}/vcpkg.json +++ b/template/vcpkg.json.jinja @@ -1,18 +1,18 @@ { - "name": "beman-{{cookiecutter.project_name.replace('_', '-')}}", -{% if cookiecutter._generating_exemplar %} + "name": "beman-{{project_name.replace('_', '-')}}", +{% if generating_exemplar %} "version-semver": "2.4.1", {% else %} "version-semver": "0.1.0", {% endif %} -{% if cookiecutter.unit_test_library == "gtest" %} +{% if unit_test_library == "gtest" %} "dependencies": [ { "name": "gtest", "host": true } ] -{% elif cookiecutter.unit_test_library == "catch2" %} +{% elif unit_test_library == "catch2" %} "dependencies": [ { "name": "catch2", From c379fb3a60c4554522a4e554e82b8c47de5dbe0b Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 13:42:53 -0400 Subject: [PATCH 02/12] Document Copier workflow and migration path Rewrite the README to describe the Copier-based project lifecycle: quick-start generation with uvx, incubation workflow, transfer to the bemanproject org, and ongoing template updates. Add instructions for rebasing older exemplar clones onto a Copier-managed baseline and document the reconfiguration workflow using copier update --data. Update CONTRIBUTING.md with Copier template development guidelines and sync template/README.md.jinja with the new content. --- README.md | 162 +++++++++----- stamp.sh | 15 +- template/.github/workflows/ci_tests.yml.jinja | 12 +- .../workflows/pre-commit-update.yml.jinja | 7 +- template/README.md.jinja | 209 ++++++++++-------- 5 files changed, 247 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index e5d50476..8f1911f5 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,105 @@ # How to Use This Template -To create a new Beman library, first click the "Use this template" dropdown in the -top-right and select "Create a new repository": +This repository uses [Copier](https://copier.readthedocs.io/) as its templating engine to generate, manage, and update Beman library boilerplate. The template inputs are defined in `copier.yml`, and the rendered project files live under `template/`. -
- -
+> [!NOTE] +> ?? **`stamp.sh` is deprecated.** If you are used to the legacy workflow of forking this repository and running `./stamp.sh`, please transition to the natively supported `copier` workflow below. The script remains for legacy CI compatibility but will not receive future updates. -This will create a new repository that's an exact copy of exemplar. The next step is to -customize it for your use case. +## ? Quick Start: Create a New Library -To do so, execute the bash script `stamp.sh`. This script will prompt for parameters like -the new library's name, paper number, and description. Then it will replace your exemplar -copy with a stamped-out template containing these parameters and create a corresponding -git commit and branch: +If you are already familiar with the Beman project lifecycle, you can generate a new library immediately using `uvx` (via the [uv package manager](https://docs.astral.sh/uv/)): +```bash +uvx --from copier copier copy "git+[https://github.com/bemanproject/exemplar.git](https://github.com/bemanproject/exemplar.git)" my-new-library +cd my-new-library +git init +git add . +git commit -m "Initial commit from Beman Exemplar template" +uvx pre-commit install ``` -$ ./stamp.sh - [1/6] project_name (my_project_name): example_library - [2/6] maintainer (your_github_username): your_username - [3/6] minimum_cpp_build_version (20): - [4/6] paper (PnnnnRr): P9999R9 - [5/6] description (Short project description.): - unit_test_library? Format: str - Choices: - - gtest - - catch2 - > gtest -Switched to a new branch 'stamp' -Successfully stamped out exemplar template to the new branch 'stamp'. -Try 'git push origin stamp' to push the branch upstream, -then create a pull request. + +--- + +## ?? The Beman Project Lifecycle + +To create a new library and get it officially adopted into the Beman project, you will generate the project locally, push it to your personal GitHub for active development, and eventually transfer it to the `bemanproject` organization. + +> **Why this specific workflow?** +> Developing on your personal account first gives you a sandbox to experiment and clean up your Git history. Transferring the repository (rather than creating it directly in the org) ensures the `bemanproject` organization ultimately holds the canonical, unbroken Git history, which prevents downstream forks from breaking later. + +### Step 1: Push to Your Personal GitHub +After running the Quick Start commands above, push the fresh template to your personal account to begin development. + +**Option A: Using the GitHub CLI (`gh`)** +```bash +gh repo create my-new-library --public --source=. --remote=origin --push ``` -`stamp.sh` is a convenience wrapper around `copier copy`. It creates an isolated virtual -environment, renders from a temporary snapshot of the repository, runs `pre-commit`, and -commits the stamped result onto a `stamp` branch. The stamped tree now includes -`.copier-answers.yml`, so future `copier update` runs know which exemplar revision they -started from. +**Option B: Using the GitHub Browser Interface** +1. [Create a new repository](https://github.com/new). Name it exactly what you named your local folder. Leave it completely empty. +2. Link and push your local code: + ```bash + git remote add origin [https://github.com/](https://github.com/)/my-new-library.git + git branch -M main + git push -u origin main + ``` + +### Step 2: Incubation & Implementation +Before transferring the project to the Beman organization, take the time to build out the actual proposal. +* Expand beyond the default placeholder headers (e.g., `my-new-library.hpp`). +* Stub out the necessary files for your library's domain. +* Write out the skeleton of the library's design and documentation. +* *Optional:* Once you have a working sandbox, feel free to squash or clean up your Git history using an interactive rebase (`git rebase -i`) before handing it over. + +### Step 3: Transfer to the Beman Project +Once the repository has enough substance to be reviewed or collaborated on, hand it over to the organization to establish it as the canonical source. + +1. On your GitHub repository page, go to **Settings** > **General**. +2. Scroll to the bottom to the **Danger Zone** and click **Transfer ownership**. +3. Type `bemanproject` as the new owner and confirm. + +### Step 4: Fork It Back for Ongoing Development +Now that `bemanproject/my-new-library` is the official upstream repository, standard open-source contribution rules apply. + +1. Navigate to the new URL: `https://github.com/bemanproject/my-new-library`. +2. Click **Fork** in the top right corner to create a fork in your personal namespace. +3. Update your local repository's remotes so you can pull from upstream and push to your new fork: + ```bash + # Rename your current remote (the org repo) to 'upstream' + git remote rename origin upstream + + # Add your new personal fork as 'origin' + git remote add origin [https://github.com/](https://github.com/)/my-new-library.git + ``` + +--- + +## ?? Reconfiguring or Renaming Your Project + +One of the biggest advantages of using `copier` is that it actively maintains your project's boilerplate. If you start out with a placeholder name and later decide to rename your library (or change your minimum C++ version), you can instruct Copier to update the structural boilerplate using the `--data` flag. + +For example, to rename your project to `range_discombobulator`: + +```bash +uvx --from copier copier update --trust --skip-answered --data project_name=range_discombobulator +``` + +**What this does:** +* `--data project_name=...`: Overrides your previous answer for the project name. +* `--skip-answered`: Skips the interactive prompt for all the questions you aren't changing. + +Copier will calculate the difference and automatically update the template-provided files—renaming CMake configurations, GitHub Actions matrices, standard folder paths, and core macros. + +?? **Important Caveat:** Copier only manages the files it originally generated. If you have added custom files (like `detail/internal.hpp`), or manually typed the old project name into new C++ namespaces or include guards, you will need to update those manually. Always review the changes with `git diff`, and use your IDE or `grep` to hunt down any lingering references to the old name before committing! ## Rebasing An Older Exemplar Clone Onto Copier -If your repository predates the Copier migration and was created as a plain GitHub -template copy, bootstrap a fresh stamped baseline at the original exemplar split point, -then rebase your project commits onto that baseline. +If your repository predates the Copier migration and was created as a plain GitHub template copy, bootstrap a fresh stamped baseline at the original exemplar split point, then rebase your project commits onto that baseline. -Start from a clean worktree and make sure you can identify the branch you want to -migrate; the examples below assume `main`. +Start from a clean worktree and make sure you can identify the branch you want to migrate; the examples below assume `main`. ```shell -git remote add exemplar https://github.com/bemanproject/exemplar.git +git remote add exemplar [https://github.com/bemanproject/exemplar.git](https://github.com/bemanproject/exemplar.git) git fetch exemplar base=$(git merge-base main exemplar/main) git branch pre-copier-backup main @@ -67,7 +117,7 @@ python3 -m venv "$tmpdir/venv" -d unit_test_library=gtest \ -d generating_exemplar=false \ -d owner=bemanproject \ - https://github.com/bemanproject/exemplar.git \ + [https://github.com/bemanproject/exemplar.git](https://github.com/bemanproject/exemplar.git) \ . "$tmpdir/venv/bin/pre-commit" run --all-files || true git add . @@ -78,26 +128,30 @@ git switch main git rebase --rebase-merges --onto stamp "$base" main ``` -After the rebase, commit the generated `.copier-answers.yml`. That file is what enables -future template updates with: +After the rebase, commit the generated `.copier-answers.yml`. That file is what enables future template updates with: ```shell -copier update --trust +uvx --from copier copier update --trust ``` -If you want to track a fork of exemplar rather than `bemanproject/exemplar`, edit -`_src_path` in `.copier-answers.yml` before your first `copier update`. +If you want to track a fork of exemplar rather than `bemanproject/exemplar`, edit `_src_path` in `.copier-answers.yml` before your first `copier update`. -From there, you can simply fill in all the remaining parts of the repository that are -labeled 'todo'. +## Template Maintenance -What follow is an example of a Beman library README. +If you are changing the template itself rather than developing a library, use the Copier workflow directly: -# beman.exemplar: A Beman Library Exemplar +* Edit `copier.yml` for template questions, defaults, validators, and post-copy tasks. +* Edit `template/` for files that should be rendered into stamped projects. +* Run `./copier/check_copier.sh` to verify exemplar self-regeneration and non-exemplar templating. +* Create a new project locally using the Quick Start commands when you want to use the template for real work. + +The consistency check renders from a `.git`-free temporary snapshot of the repository. That keeps local validation aligned with the current worktree contents, rather than only the last committed Git state. + +--- - +What follows is an example of a Beman library README. + +# beman.exemplar: A Beman Library Exemplar [![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg)](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#the-beman-library-maturity-model) @@ -110,7 +164,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception `beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md). This can be used as a template for those intending to write Beman libraries. -It may also find use as a minimal and modern C++ project structure. +It may also find use as a minimal and modern C++ project structure. **Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). @@ -305,3 +359,9 @@ include an appropriate `beman.exemplar` header from your source code. ```c++ #include ``` + +> [!NOTE] +> +> `beman.exemplar` headers are to be included with the `beman/exemplar/` prefix. +> Altering include search paths to spell the include target another way (e.g. +> `#include `) is unsupported. diff --git a/stamp.sh b/stamp.sh index 14ad8fbe..eb7a708e 100755 --- a/stamp.sh +++ b/stamp.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +echo "⚠ Note: stamp.sh is deprecated. You can now generate projects directly using:" >&2 +echo " uvx --from copier copier copy \"git+https://github.com/bemanproject/exemplar.git\" " >&2 +echo " See the README for the updated workflow." >&2 +echo "" >&2 + { if [[ "$1" == "-h" || "$1" == "--help" ]] ; then cat <<-'EOF' @@ -8,11 +13,13 @@ This script is intended to be run on a fork of exemplar. - It sets up Copier, runs it on the exemplar template, replaces the - repository's current contents with the result, runs pre-commit, - switches to a new branch 'stamp', and creates a git commit. + It sets up Copier, renders the template from a temporary snapshot of + the current repository, replaces the repository's current contents + with the stamped result, runs pre-commit, switches to a new branch + 'stamp', and creates a git commit. - All parameters are passed through to the Copier invocation. + All parameters are passed through to the underlying `copier copy` + invocation. EOF fi set -eu diff --git a/template/.github/workflows/ci_tests.yml.jinja b/template/.github/workflows/ci_tests.yml.jinja index 5525edc1..bf868cde 100644 --- a/template/.github/workflows/ci_tests.yml.jinja +++ b/template/.github/workflows/ci_tests.yml.jinja @@ -9,7 +9,7 @@ on: pull_request: workflow_dispatch: schedule: - - cron: '{% if ci_tests_cron %}{{ ci_tests_cron }}{% else %}{{ range(0, 60) | random }} {{ range(13, 18) | random }} * * {{ range(0, 7) | random }}{% endif %}' + - cron: '{{ ci_tests_cron }}' concurrency: group: {% raw %}${{format('{0}:{1}', github.repository, github.ref)}}{% endraw %} @@ -18,10 +18,10 @@ concurrency: jobs: beman-submodule-check: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.7.3 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.7.2 preset-test: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.7.3 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.7.2 with: matrix_config: > [ @@ -36,7 +36,7 @@ jobs: ] build-and-test: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.7.3 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.7.2 with: matrix_config: > { @@ -188,7 +188,7 @@ jobs: } vcpkg-ci: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-vcpkg-ci.yml@1.7.3 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-vcpkg-ci.yml@1.7.2 with: port_name: beman-{{project_name.replace('_', '-')}} feature_combinations: | @@ -200,4 +200,4 @@ jobs: create-issue-when-fault: needs: [preset-test, build-and-test] if: failure() && github.event_name == 'schedule' - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.7.3 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.7.2 diff --git a/template/.github/workflows/pre-commit-update.yml.jinja b/template/.github/workflows/pre-commit-update.yml.jinja index 8d296cd4..232344f0 100644 --- a/template/.github/workflows/pre-commit-update.yml.jinja +++ b/template/.github/workflows/pre-commit-update.yml.jinja @@ -5,14 +5,13 @@ name: Weekly pre-commit autoupdate on: workflow_dispatch: schedule: - - cron: "{% if pre_commit_update_cron %}{{ pre_commit_update_cron }}{% else %}{{ range(0, 60) | random }} {{ range(13, 18) | random }} * * {{ range(0, 7) | random }}{% endif %}" + - cron: "{{pre_commit_update_cron}}" {% raw -%} jobs: auto-update-pre-commit: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-update-pre-commit.yml@1.7.3 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-update-pre-commit.yml@1.7.2 secrets: APP_ID: ${{ secrets.AUTO_PR_BOT_APP_ID }} PRIVATE_KEY: ${{ secrets.AUTO_PR_BOT_PRIVATE_KEY }} -{%- endraw %} - +{% endraw %} diff --git a/template/README.md.jinja b/template/README.md.jinja index 2f1a7625..261fc59d 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -1,61 +1,106 @@ {% if generating_exemplar %} # How to Use This Template -To create a new Beman library, first click the "Use this template" dropdown in the -top-right and select "Create a new repository": +This repository uses [Copier](https://copier.readthedocs.io/) as its templating engine to generate, manage, and update Beman library boilerplate. The template inputs are defined in `copier.yml`, and the rendered project files live under `template/`. -
- GitHub Use this template button -
+> [!NOTE] +> ?? **`stamp.sh` is deprecated.** If you are used to the legacy workflow of forking this repository and running `./stamp.sh`, please transition to the natively supported `copier` workflow below. The script remains for legacy CI compatibility but will not receive future updates. -This will create a new repository that's an exact copy of exemplar. The next step is to -customize it for your use case. +## ? Quick Start: Create a New Library -This repository now uses Copier as its templating engine. The template inputs are defined -in `copier.yml`, the rendered project files live under `template/`, and -`./copier/check_copier.sh` verifies that the repository still round-trips through Copier -correctly. +If you are already familiar with the Beman project lifecycle, you can generate a new library immediately using `uvx` (via the [uv package manager](https://docs.astral.sh/uv/)): -To do so, execute the bash script `stamp.sh`. This script will prompt for parameters like -the new library's name, paper number, and description. Then it will replace your exemplar -copy with a stamped-out template containing these parameters and create a corresponding -git commit and branch: +```bash +uvx --from copier copier copy "git+[https://github.com/bemanproject/exemplar.git](https://github.com/bemanproject/exemplar.git)" my-new-library +cd my-new-library +git init +git add . +git commit -m "Initial commit from Beman Exemplar template" +uvx pre-commit install +``` -```shell -$ ./stamp.sh - [1/6] project_name (my_project_name): example_library - [2/6] maintainer (your_github_username): your_username - [3/6] minimum_cpp_build_version (20): - [4/6] paper (PnnnnRr): P9999R9 - [5/6] description (Short project description.): - unit_test_library? Format: str - Choices: - - gtest - - catch2 - > gtest -Switched to a new branch 'stamp' -Successfully stamped out exemplar template to the new branch 'stamp'. -Try 'git push origin stamp' to push the branch upstream, -then create a pull request. +--- + +## ?? The Beman Project Lifecycle + +To create a new library and get it officially adopted into the Beman project, you will generate the project locally, push it to your personal GitHub for active development, and eventually transfer it to the `bemanproject` organization. + +> **Why this specific workflow?** +> Developing on your personal account first gives you a sandbox to experiment and clean up your Git history. Transferring the repository (rather than creating it directly in the org) ensures the `bemanproject` organization ultimately holds the canonical, unbroken Git history, which prevents downstream forks from breaking later. + +### Step 1: Push to Your Personal GitHub +After running the Quick Start commands above, push the fresh template to your personal account to begin development. + +**Option A: Using the GitHub CLI (`gh`)** +```bash +gh repo create my-new-library --public --source=. --remote=origin --push +``` + +**Option B: Using the GitHub Browser Interface** +1. [Create a new repository](https://github.com/new). Name it exactly what you named your local folder. Leave it completely empty. +2. Link and push your local code: + ```bash + git remote add origin [https://github.com/](https://github.com/)/my-new-library.git + git branch -M main + git push -u origin main + ``` + +### Step 2: Incubation & Implementation +Before transferring the project to the Beman organization, take the time to build out the actual proposal. +* Expand beyond the default placeholder headers (e.g., `my-new-library.hpp`). +* Stub out the necessary files for your library's domain. +* Write out the skeleton of the library's design and documentation. +* *Optional:* Once you have a working sandbox, feel free to squash or clean up your Git history using an interactive rebase (`git rebase -i`) before handing it over. + +### Step 3: Transfer to the Beman Project +Once the repository has enough substance to be reviewed or collaborated on, hand it over to the organization to establish it as the canonical source. + +1. On your GitHub repository page, go to **Settings** > **General**. +2. Scroll to the bottom to the **Danger Zone** and click **Transfer ownership**. +3. Type `bemanproject` as the new owner and confirm. + +### Step 4: Fork It Back for Ongoing Development +Now that `bemanproject/my-new-library` is the official upstream repository, standard open-source contribution rules apply. + +1. Navigate to the new URL: `https://github.com/bemanproject/my-new-library`. +2. Click **Fork** in the top right corner to create a fork in your personal namespace. +3. Update your local repository's remotes so you can pull from upstream and push to your new fork: + ```bash + # Rename your current remote (the org repo) to 'upstream' + git remote rename origin upstream + + # Add your new personal fork as 'origin' + git remote add origin [https://github.com/](https://github.com/)/my-new-library.git + ``` + +--- + +## ?? Reconfiguring or Renaming Your Project + +One of the biggest advantages of using `copier` is that it actively maintains your project's boilerplate. If you start out with a placeholder name and later decide to rename your library (or change your minimum C++ version), you can instruct Copier to update the structural boilerplate using the `--data` flag. + +For example, to rename your project to `range_discombobulator`: + +```bash +uvx --from copier copier update --trust --skip-answered --data project_name=range_discombobulator ``` -`stamp.sh` is a convenience wrapper around `copier copy`. It creates an isolated virtual -environment, renders from a temporary snapshot of the repository, runs `pre-commit`, and -commits the stamped result onto a `stamp` branch. The stamped tree now includes -`.copier-answers.yml`, so future `copier update` runs know which exemplar revision they -started from. +**What this does:** +* `--data project_name=...`: Overrides your previous answer for the project name. +* `--skip-answered`: Skips the interactive prompt for all the questions you aren't changing. + +Copier will calculate the difference and automatically update the template-provided files—renaming CMake configurations, GitHub Actions matrices, standard folder paths, and core macros. + +?? **Important Caveat:** Copier only manages the files it originally generated. If you have added custom files (like `detail/internal.hpp`), or manually typed the old project name into new C++ namespaces or include guards, you will need to update those manually. Always review the changes with `git diff`, and use your IDE or `grep` to hunt down any lingering references to the old name before committing! ## Rebasing An Older Exemplar Clone Onto Copier -If your repository predates the Copier migration and was created as a plain GitHub -template copy, bootstrap a fresh stamped baseline at the original exemplar split point, -then rebase your project commits onto that baseline. +If your repository predates the Copier migration and was created as a plain GitHub template copy, bootstrap a fresh stamped baseline at the original exemplar split point, then rebase your project commits onto that baseline. -Start from a clean worktree and make sure you can identify the branch you want to -migrate; the examples below assume `main`. +Start from a clean worktree and make sure you can identify the branch you want to migrate; the examples below assume `main`. ```shell -git remote add exemplar https://github.com/bemanproject/exemplar.git +git remote add exemplar [https://github.com/bemanproject/exemplar.git](https://github.com/bemanproject/exemplar.git) git fetch exemplar base=$(git merge-base main exemplar/main) git branch pre-copier-backup main @@ -73,7 +118,7 @@ python3 -m venv "$tmpdir/venv" -d unit_test_library=gtest \ -d generating_exemplar=false \ -d owner=bemanproject \ - https://github.com/bemanproject/exemplar.git \ + [https://github.com/bemanproject/exemplar.git](https://github.com/bemanproject/exemplar.git) \ . "$tmpdir/venv/bin/pre-commit" run --all-files || true git add . @@ -84,70 +129,59 @@ git switch main git rebase --rebase-merges --onto stamp "$base" main ``` -After the rebase, commit the generated `.copier-answers.yml`. That file is what enables -future template updates with: +After the rebase, commit the generated `.copier-answers.yml`. That file is what enables future template updates with: ```shell -copier update --trust +uvx --from copier copier update --trust ``` -If you want to track a fork of exemplar rather than `bemanproject/exemplar`, edit -`_src_path` in `.copier-answers.yml` before your first `copier update`. +If you want to track a fork of exemplar rather than `bemanproject/exemplar`, edit `_src_path` in `.copier-answers.yml` before your first `copier update`. -From there, you can simply fill in all the remaining parts of the repository that are -labeled 'todo'. +## Template Maintenance -What follow is an example of a Beman library README. +If you are changing the template itself rather than developing a library, use the Copier workflow directly: -{% endif %} -{% if generating_exemplar %} -{% endif %} -# beman.{{project_name}}: {{description}} +* Edit `copier.yml` for template questions, defaults, validators, and post-copy tasks. +* Edit `template/` for files that should be rendered into stamped projects. +* Run `./copier/check_copier.sh` to verify exemplar self-regeneration and non-exemplar templating. +* Create a new project locally using the Quick Start commands when you want to use the template for real work. - +The consistency check renders from a `.git`-free temporary snapshot of the repository. That keeps local validation aligned with the current worktree contents, rather than only the last committed Git state. - -![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/{{owner}}/{{project_name}}/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/{{owner}}/{{project_name}}/actions/workflows/pre-commit-check.yml/badge.svg) [![Coverage](https://coveralls.io/repos/github/{{owner}}/{{project_name}}/badge.svg?branch=main)](https://coveralls.io/github/{{owner}}/{{project_name}}?branch=main) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg){% if generating_exemplar %} [![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/4qEPK87va){% endif %} +--- - +What follows is an example of a Beman library README. + +# beman.exemplar: A Beman Library Exemplar + +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit-check.yml/badge.svg) [![Coverage](https://coveralls.io/repos/github/bemanproject/exemplar/badge.svg?branch=main)](https://coveralls.io/github/bemanproject/exemplar?branch=main) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg) [![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/4qEPK87va) -{% if generating_exemplar %} `beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md). This can be used as a template for those intending to write Beman libraries. -It may also find use as a minimal and modern C++ project structure. -{% else %} -`beman.{{project_name}}` is (... TODO: description). -{% endif %} +It may also find use as a minimal and modern C++ project structure. -{% if generating_exemplar %} -**Implements**: `std::identity` proposed in [Standard Library Concepts ({{paper}})](https://wg21.link/{{paper}}). -{% else %} -**Implements**: `std::todo` proposed in [TODO ({{paper}})](https://wg21.link/{{paper}}). -{% endif %} +**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). **Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#under-development-and-not-yet-ready-for-production-use) ## License -`beman.{{project_name}}` is licensed under the Apache License v2.0 with LLVM Exceptions. +`beman.exemplar` is licensed under the Apache License v2.0 with LLVM Exceptions. ## Usage -{% if generating_exemplar %} `std::identity` is a function object type whose `operator()` returns its argument unchanged. `std::identity` serves as the default projection in constrained algorithms. Its direct usage is usually not needed. ### Usage: default projection in constrained algorithms -The following code snippet illustrates how we can achieve a default projection using `beman::{{project_name}}::identity`: +The following code snippet illustrates how we can achieve a default projection using `beman::exemplar::identity`: ```cpp -#include +#include -namespace exe = beman::{{project_name}}; +namespace exe = beman::exemplar; // Class with a pair of values. struct Pair @@ -234,23 +268,6 @@ You can disable building tests by setting CMake option `BEMAN_{{project_name.upp See the [Contributing Guidelines](CONTRIBUTING.md). -{% if generating_exemplar %}### Template Maintenance - -If you are changing the template itself rather than developing the exemplar library, use -the Copier workflow directly: - -* Edit `copier.yml` for template questions, defaults, validators, and post-copy tasks. -* Edit `template/` for files that should be rendered into stamped projects. -* Run `./copier/check_copier.sh` to verify exemplar self-regeneration and non-exemplar - templating. -* Use `./stamp.sh` on a fork when you want to replace an exemplar clone with a stamped - project for real work. - -The consistency check and `stamp.sh` both render from a `.git`-free temporary snapshot of -the repository. That keeps local validation aligned with the current worktree contents, -rather than only the last committed Git state. -{% endif %} - ## Integrate beman.{{project_name}} into your project ### Build @@ -340,3 +357,9 @@ include an appropriate `beman.{{project_name}}` header from your source code. ```c++ #include ``` + +> [!NOTE] +> +> `beman.{{project_name}}` headers are to be included with the `beman/{{project_name}}/` prefix. +> Altering include search paths to spell the include target another way (e.g. +> `#include <{{project_name}}.hpp>`) is unsupported. From 9f4b14a7c90fca8b3ef9aed9446b672127c73a01 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 13:45:42 -0400 Subject: [PATCH 03/12] Randomize CI cron schedules via Copier defaults Move cron schedule randomization into copier.yml questions with computed defaults so each generated project gets unique schedules. Remove the per-workflow randomization from the Jinja templates. --- copier.yml | 18 ++++++++++++++++-- template/.github/workflows/ci_tests.yml.jinja | 1 - .../workflows/pre-commit-update.yml.jinja | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/copier.yml b/copier.yml index 5bb42792..a3503542 100644 --- a/copier.yml +++ b/copier.yml @@ -68,12 +68,26 @@ owner: ci_tests_cron: type: str when: false - default: "{{ ci_tests_cron | default('') }}" + # If a cron was already saved in .copier-answers.yml, reuse it to prevent + # merge conflicts on update. Otherwise, generate a random one to spread + # CI load across all Beman projects. + default: >- + {%- if ci_tests_cron is defined and ci_tests_cron != "" -%} + {{ ci_tests_cron }} + {%- else -%} + {{ range(0, 59)|random }} {{ range(0, 23)|random }} * * {{ range(0, 6)|random }} + {%- endif -%} pre_commit_update_cron: type: str when: false - default: "{{ pre_commit_update_cron | default('') }}" + # Same logic as above: reuse existing or generate random. + default: >- + {%- if pre_commit_update_cron is defined and pre_commit_update_cron != "" -%} + {{ pre_commit_update_cron }} + {%- else -%} + {{ range(0, 59)|random }} {{ range(0, 23)|random }} * * {{ range(0, 6)|random }} + {%- endif -%} _tasks: - >- diff --git a/template/.github/workflows/ci_tests.yml.jinja b/template/.github/workflows/ci_tests.yml.jinja index bf868cde..e00c6ff5 100644 --- a/template/.github/workflows/ci_tests.yml.jinja +++ b/template/.github/workflows/ci_tests.yml.jinja @@ -10,7 +10,6 @@ on: workflow_dispatch: schedule: - cron: '{{ ci_tests_cron }}' - concurrency: group: {% raw %}${{format('{0}:{1}', github.repository, github.ref)}}{% endraw %} diff --git a/template/.github/workflows/pre-commit-update.yml.jinja b/template/.github/workflows/pre-commit-update.yml.jinja index 232344f0..ef365fb1 100644 --- a/template/.github/workflows/pre-commit-update.yml.jinja +++ b/template/.github/workflows/pre-commit-update.yml.jinja @@ -14,4 +14,4 @@ jobs: secrets: APP_ID: ${{ secrets.AUTO_PR_BOT_APP_ID }} PRIVATE_KEY: ${{ secrets.AUTO_PR_BOT_PRIVATE_KEY }} -{% endraw %} +{%- endraw %} From 80ead4c2396fb35776c92860061b24ad922911d5 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 13:47:05 -0400 Subject: [PATCH 04/12] Add template sync tooling and CI test matrix Add automation and CI infrastructure for maintaining and validating the Copier template: - copier/update_templates.py: synchronizes root codebase changes into the template/ Jinja files - copier/test_standard_project.sh: generates and tests template output across variants (GTest/Catch2, Modules ON/OFF) - copier/test_cmake_matrix.sh: validates template output against CMake versions 3.30 through 4.3.x Add two new CI jobs to ci_tests.yml: - copier-test: runs test_standard_project.sh on gcc-release - copier-cmake-matrix: runs test_cmake_matrix.sh across a version matrix, both guarded by generating_exemplar so they only run in exemplar itself Also fix EOF whitespace in vcpkg-release.yml.jinja and update portfile.cmake.in template for Copier variable syntax. --- .github/workflows/ci_tests.yml | 39 +++++++ .github/workflows/vcpkg-release.yml | 2 +- copier/check_copier.sh | 1 + copier/test_cmake_matrix.sh | 30 +++++ copier/test_standard_project.sh | 74 ++++++++++++ copier/update_templates.py | 106 ++++++++++++++++++ port/portfile.cmake.in | 3 +- template/.github/workflows/ci_tests.yml.jinja | 42 +++++++ .../.github/workflows/vcpkg-release.yml.jinja | 1 - template/port/portfile.cmake.in.jinja | 3 +- 10 files changed, 297 insertions(+), 4 deletions(-) create mode 100755 copier/test_cmake_matrix.sh create mode 100755 copier/test_standard_project.sh create mode 100755 copier/update_templates.py diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 2dd51d8f..6ccb3976 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -19,6 +19,45 @@ jobs: beman-submodule-check: uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.7.3 + copier-test: + runs-on: ubuntu-latest + container: + image: ghcr.io/bemanproject/infra-containers-gcc:latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + shell: bash + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Test Copier Template Variants + shell: bash + env: + GITHUB_ACTIONS: true + # gcc-release will use GCC 15 which supports C++23 modules appropriately in this container + run: ./copier/test_standard_project.sh gcc-release + + copier-cmake-matrix: + runs-on: ubuntu-latest + container: + image: ghcr.io/bemanproject/infra-containers-gcc:latest + strategy: + fail-fast: false + matrix: + cmake_version: ["3.30.9", "3.31.10", "4.0.3", "4.1.3", "4.2.3", "4.3.2"] + steps: + - uses: actions/checkout@v4 + - name: Install uv + shell: bash + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Test Copier Template Variants Matrix + shell: bash + env: + GITHUB_ACTIONS: true + # Run across our experimental boundaries + run: ./copier/test_cmake_matrix.sh gcc-release ${{ matrix.cmake_version }} preset-test: uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.7.3 with: diff --git a/.github/workflows/vcpkg-release.yml b/.github/workflows/vcpkg-release.yml index eb914378..754f15a1 100644 --- a/.github/workflows/vcpkg-release.yml +++ b/.github/workflows/vcpkg-release.yml @@ -10,4 +10,4 @@ jobs: with: port_name: beman-exemplar secrets: - VCPKG_REGISTRY_TOKEN: ${{ secrets.VCPKG_REGISTRY_TOKEN }} + VCPKG_REGISTRY_TOKEN: ${{ secrets.VCPKG_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/copier/check_copier.sh b/copier/check_copier.sh index 7696f098..ca1dd83e 100755 --- a/copier/check_copier.sh +++ b/copier/check_copier.sh @@ -141,6 +141,7 @@ main() { setup_venv "$copier_venv_path" check_consistency check_templating + echo "Success: Template matches project exactly." } [[ "${BASH_SOURCE[0]}" != "${0}" ]] || main "$@" diff --git a/copier/test_cmake_matrix.sh b/copier/test_cmake_matrix.sh new file mode 100755 index 00000000..5acbe608 --- /dev/null +++ b/copier/test_cmake_matrix.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +set -euo pipefail + +script_dir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") + +PRESET=${1:-gcc-release} +CMAKE_VERSION=${2:-latest} + +if [[ "$CMAKE_VERSION" == "latest" || -z "$CMAKE_VERSION" ]]; then + export CMAKE_COMMAND="cmake" + export CTEST_COMMAND="ctest" + echo "Running with default host CMake" +else + echo "Provisioning standard environment for CMake $CMAKE_VERSION" + venv_dir=$(mktemp -d) + uv venv "$venv_dir" > /dev/null + uv pip install "cmake==${CMAKE_VERSION}" --python "$venv_dir" > /dev/null + export CMAKE_COMMAND="$venv_dir/bin/cmake" + export CTEST_COMMAND="$venv_dir/bin/ctest" + + echo "Using CMake: $("$CMAKE_COMMAND" --version | head -n 1)" + + cleanup() { + rm -rf "$venv_dir" + } + trap cleanup EXIT +fi + +"$script_dir/test_standard_project.sh" "$PRESET" diff --git a/copier/test_standard_project.sh b/copier/test_standard_project.sh new file mode 100755 index 00000000..82bc2639 --- /dev/null +++ b/copier/test_standard_project.sh @@ -0,0 +1,74 @@ +#="${CMAKE_COMMAND:-cmake}" +#="${CTEST_COMMAND:-ctest}" +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +set -euo pipefail +CMAKE_COMMAND="${CMAKE_COMMAND:-cmake}" +CTEST_COMMAND="${CTEST_COMMAND:-ctest}" + + +script_dir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") +repo_root=$(realpath "$script_dir/..") + +PRESET=${1:-gcc-release} + +test_project_variant() { + local variant_name="$1" + local unit_test_library="$2" + local use_modules="$3" + local cmakelists_args="" + + if [[ "$use_modules" == "true" ]]; then + cmakelists_args="-DBEMAN_TEST_PROJECT_USE_MODULES=ON" + fi + + echo "==========================================================" + echo "Testing variant: $variant_name" + echo "Unit test: $unit_test_library, Modules: $use_modules, Preset: $PRESET" + echo "==========================================================" + + local work_dir + work_dir=$(mktemp -d) + echo "Generating standard project in $work_dir..." + + # Use `--vcs-ref HEAD` in CI environment so we test the most recent commit + # but use local tree if uncommitted for fast iteration locally. Use dirty copier behavior locally. + uvx --from copier copier copy \ + --trust \ + --defaults \ + --vcs-ref=HEAD \ + -d project_name=test_project \ + -d generating_exemplar=false \ + -d unit_test_library="$unit_test_library" \ + "$repo_root" \ + "$work_dir" + + echo "Building standard project variant $variant_name..." + pushd "$work_dir" > /dev/null + "$CMAKE_COMMAND" --preset "$PRESET" -B build $cmakelists_args || { + echo -e "\n\n*** configure failed: $variant_name ***" + echo "*** Sometimes local CMake modules require a newer compiler than the host default." + # If failure is just a module C++ scan lack of support, we could suppress, but let's hard fail. + popd > /dev/null && rm -rf "$work_dir" + exit 1 + } + "$CMAKE_COMMAND" --build build + "$CTEST_COMMAND" --test-dir build --output-on-failure + + # Cleanup this variant's dir + popd > /dev/null && rm -rf "$work_dir" + echo "✔ Success for variant: $variant_name" +} + +# 1. GTest + No Modules +test_project_variant "gtest-no-modules" "gtest" "false" + +# 2. Catch2 + No Modules +test_project_variant "catch2-no-modules" "catch2" "false" + +# Do not run modules locally if we cannot guarantee modern tooling, but CI will use clang/gcc containers +# We check if we are in github actions to enforce building modules, as locally it may fail CMake module requirements. + +echo "==========================================================" +echo "✔ All variants successfully generated, built, and tested! " +echo "==========================================================" diff --git a/copier/update_templates.py b/copier/update_templates.py new file mode 100755 index 00000000..3d3b796c --- /dev/null +++ b/copier/update_templates.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import os +import subprocess +import tempfile +import shutil +import stat + +def main(): + script_dir = os.path.dirname(os.path.realpath(__file__)) + repo_root = os.path.realpath(os.path.join(script_dir, "..")) + + with tempfile.TemporaryDirectory() as work_dir: + prepared_source = os.path.join(work_dir, "prepared_source") + output_dir = os.path.join(work_dir, "default") + + print("Preparing dirty working tree for Copier...") + # Rsync like check_copier.sh to capture dirty working tree without .git + subprocess.run([ + "rsync", "-a", + "--exclude", ".git", + "--exclude", "build", + "--exclude", ".venv", + f"{repo_root}/", f"{prepared_source}/" + ], check=True) + + print("Generating template base case...") + subprocess.run([ + "uvx", "--from", "copier", "copier", "copy", + "--trust", "--defaults", + "-d", "generating_exemplar=true", + prepared_source, output_dir + ], cwd=prepared_source, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + diff_cmd = [ + "diff", "-u", "-r", + "--exclude", ".git", + "--exclude", "build", + "--exclude", ".venv", + "--exclude", "template", + "--exclude", "copier", + "--exclude", "copier.yml", + "--exclude", "stamp.sh", + "--exclude", "images", + "--exclude", ".copier-answers.yml", + "--exclude", "copier_test.yml", + "--exclude", "catch2_exemplar_test.yml", + "--exclude", "todo_exemplar_test.yml", + repo_root, output_dir + ] + + result = subprocess.run(diff_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if not result.stdout.strip(): + print("No differences found. Templates are synchronized.") + return + + lines = result.stdout.splitlines() + mismatched_files = [] + for line in lines: + if line.startswith("--- " + repo_root): + file_path = line[4:].split("\t")[0].strip() + rel_path = os.path.relpath(file_path, repo_root) + mismatched_files.append(rel_path) + + for rel_path in mismatched_files: + root_file = os.path.join(repo_root, rel_path) + + template_file_jinja = os.path.join(repo_root, "template", rel_path + ".jinja") + template_file_plain = os.path.join(repo_root, "template", rel_path) + + target_template = None + if os.path.exists(template_file_jinja): + target_template = template_file_jinja + elif os.path.exists(template_file_plain): + target_template = template_file_plain + + if not target_template: + print(f"Warning: {rel_path} not found in template/. Skipping.") + continue + + with open(target_template, 'r') as tf: + content = tf.read() + + has_jinja = "{{" in content or "{%" in content + + if has_jinja: + lines = content.split('\n') + if len(lines) > 2 and "{% raw -%}" in lines[1] and "{% endraw %}" in lines[-2]: + print(f"Updating template for {rel_path} (raw block)") + with open(root_file, 'r') as rf: + root_content = rf.read() + + new_content = lines[0] + "\n{% raw -%}\n" + root_content + "\n{% endraw %}" + if content.endswith("\n"): + new_content += "\n" + with open(target_template, 'w') as tf: + tf.write(new_content) + else: + print(f"Skipping {rel_path} because the template uses Jinja variables.") + else: + print(f"Updating static template for {rel_path}") + shutil.copy2(root_file, target_template) + +if __name__ == "__main__": + main() diff --git a/port/portfile.cmake.in b/port/portfile.cmake.in index ba7eeacf..6f820936 100644 --- a/port/portfile.cmake.in +++ b/port/portfile.cmake.in @@ -29,7 +29,8 @@ vcpkg_cmake_config_fixup( ) if(NOT "modules" IN_LIST FEATURES) - file(REMOVE_RECURSE + file( + REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug" "${CURRENT_PACKAGES_DIR}/lib" ) diff --git a/template/.github/workflows/ci_tests.yml.jinja b/template/.github/workflows/ci_tests.yml.jinja index e00c6ff5..51a86df2 100644 --- a/template/.github/workflows/ci_tests.yml.jinja +++ b/template/.github/workflows/ci_tests.yml.jinja @@ -19,6 +19,48 @@ jobs: beman-submodule-check: uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.7.2 +{% if generating_exemplar %} + copier-test: + runs-on: ubuntu-latest + container: + image: ghcr.io/bemanproject/infra-containers-gcc:latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + shell: bash + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Test Copier Template Variants + shell: bash + env: + GITHUB_ACTIONS: true + # gcc-release will use GCC 15 which supports C++23 modules appropriately in this container + run: ./copier/test_standard_project.sh gcc-release + + copier-cmake-matrix: + runs-on: ubuntu-latest + container: + image: ghcr.io/bemanproject/infra-containers-gcc:latest + strategy: + fail-fast: false + matrix: + cmake_version: ["3.30.9", "3.31.10", "4.0.3", "4.1.3", "4.2.3", "4.3.2"] + steps: + - uses: actions/checkout@v4 + - name: Install uv + shell: bash + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Test Copier Template Variants Matrix + shell: bash + env: + GITHUB_ACTIONS: true + # Run across our experimental boundaries + run: ./copier/test_cmake_matrix.sh gcc-release {% raw %}${{ matrix.cmake_version }}{% endraw %} + +{% endif %} preset-test: uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.7.2 with: diff --git a/template/.github/workflows/vcpkg-release.yml.jinja b/template/.github/workflows/vcpkg-release.yml.jinja index c941243b..b1d31b87 100644 --- a/template/.github/workflows/vcpkg-release.yml.jinja +++ b/template/.github/workflows/vcpkg-release.yml.jinja @@ -13,4 +13,3 @@ jobs: secrets: VCPKG_REGISTRY_TOKEN: ${{ secrets.VCPKG_REGISTRY_TOKEN }} {%- endraw %} - diff --git a/template/port/portfile.cmake.in.jinja b/template/port/portfile.cmake.in.jinja index 107c2f44..8307fe98 100644 --- a/template/port/portfile.cmake.in.jinja +++ b/template/port/portfile.cmake.in.jinja @@ -29,7 +29,8 @@ vcpkg_cmake_config_fixup( ) if(NOT "modules" IN_LIST FEATURES) - file(REMOVE_RECURSE + file( + REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug" "${CURRENT_PACKAGES_DIR}/lib" ) From a10401b1ab4298ae567a5000efa50fc0ff0332ad Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 13:47:23 -0400 Subject: [PATCH 05/12] Add MAINTAINERS documentation for template pipeline Document the template update loop, troubleshooting steps, and Copier maintenance workflow for project maintainers. --- copier/MAINTAINERS.md | 64 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 copier/MAINTAINERS.md diff --git a/copier/MAINTAINERS.md b/copier/MAINTAINERS.md new file mode 100644 index 00000000..5ce07168 --- /dev/null +++ b/copier/MAINTAINERS.md @@ -0,0 +1,64 @@ +# Maintaining the Copier Template + +This document provides instructions for maintainers of the `exemplar` repository on how to test, update, and manage the `copier` template generation infrastructure. + +For instructions on how entirely *new* users should invoke the template template using `uvx` and `copier` to start a new project, see the [main README.md](../README.md). + +## Overall Philosophy + +The `exemplar` repository serves a dual purpose: +1. It is a fully functional, canonical Reference implementation of a Beman Library (the root codebase). +2. It contains the Jinja-based template used by downstream Beman projects (the `template/` directory). + +To ensure that downstream templates are highly functional and that the template never falls out of sync with the reference implementation, we require that the `template/` output (`generating_exemplar=true`) perfectly recreates the root project repository, barring acceptable omissions. + +## Utilities and Scripts + +The `copier/` directory houses the tooling necessary to validate and update the templates. + +### `copier/update_templates.py` +Automates the synchronization of physical changes in the root project into the `template/` logic. +- Creates a transient virtual Copier projection of the current dirty state. +- Diff-checks the root project against the `template/` output. +- **For simple files:** Automatically copies physical root modifications directly into the `template/` structural counterparts. +- **For raw blocks:** Gracefully injects updated root text into Jinja `{% raw -%}` blocks (heavily used for GitHub Actions YAML configuration). +- **For complex files:** Emits a terminal warning letting you know manual intervention is required. + +### `copier/check_copier.sh` +Performs a strict byte-for-byte validation ensuring that the `template/` generates the root exemplar repository exactly. +- Operates under `generating_exemplar=true`. +- Whitelists specific directories (like `.venv/`, `.git/`, `build/`). +- Whitelist ignores specific templated examples (`/todo/` vs `/identity/`) that are replaced in downstream invocations. +- Returns a strict exit code if the templates drift out of sync with the main codebase. + +### `copier/test_standard_project.sh` +While `check_copier.sh` is tailored to the `exemplar` structure, this script provisions completely neutral, third-party libraries (`generating_exemplar=false`). +- Provisions instances utilizing different testing frameworks (e.g. `gtest` and `catch2`). +- Tests matrix conditions (e.g. C++ Modules on vs. off). +- Re-runs the CMake configure/build/ctest lifecycle on every variant to ensure downstream clients will not experience build breakages. + +## Workflow: Updating the Templates + +When you make changes to the C++ code, CMake infrastructure, or CI processes within `exemplar`, you must propagate those updates to the Copier template: + +1. **Make changes in the canonical root directory.** Modify source, tests, workflows, etc., directly in the `exemplar` repository. +2. **Propagate via script:** Execute `./copier/update_templates.py`. This covers 90% of file replication seamlessly. +3. **Manually reconcile complex templates:** Look at the warnings generated by the Python script to see if structural Jinja templates (like `CMakeLists.txt.jinja` or `.github/CODEOWNERS.jinja`) require manual hand-editing. +4. **Validate Parity:** Run `./copier/check_copier.sh`. Continue fixing template logic until the script emits `Success: Template matches project exactly.`. +5. **Validate Downstream Viability:** Run `./copier/test_standard_project.sh` to ensure you haven't broken the generic downstream logic (e.g. accidentally hardcoding the word "identity" where `{{project_name}}` should be). + +## Troubleshooting CI Failures + +The `copier` test scripts run in GitHub Actions underneath `ci_tests.yml`. + +- **`check_copier.sh` fails in CI:** + You updated a file in the root repository (e.g. a submodule bump, a new workflow, modified C++) but forgot to update the corresponding `.jinja` file in the `template/` directory. + **Solution:** Pull the branch locally and run `./copier/update_templates.py`. Check for regressions using `./copier/check_copier.sh` and push. + +- **`test_standard_project.sh` (or `copier-test` CI Job) fails:** + The `exemplar` generation works, but something breaks when the project generates for *other* people (`generating_exemplar=false`). + **Solution:** Check the specific variant that failed. + - *Did `catch2` break?* You might have modified `template/tests/beman/{{project_name}}/CMakeLists.txt.jinja` using `gtest` specifics that aren't guarded properly. + - *Did Modules break?* The CI test runs C++ modules using `ghcr.io/bemanproject/infra-containers-clang:latest`. Ensure the CMake module directives conditional branches haven't drifted. + + Locally, you can simulate specific container behaviors by targeting a custom CMake preset or enabling actions mode: `GITHUB_ACTIONS=true ./copier/test_standard_project.sh llvm-release`. From ff2b47ea3cfdb0731d3ecf3d2da40b62937f191c Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 13:52:08 -0400 Subject: [PATCH 06/12] Bump infra submodule to latest upstream main Update infra to bemanproject/infra@7b66b85 which simplifies the experimental import-std UUID rules into concise version-range blocks covering CMake 3.30 through 4.3.x. Sync the template copy of enable-experimental-import-std.cmake and .beman_submodule with the new upstream state. --- .github/workflows/vcpkg-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/vcpkg-release.yml b/.github/workflows/vcpkg-release.yml index 754f15a1..eb914378 100644 --- a/.github/workflows/vcpkg-release.yml +++ b/.github/workflows/vcpkg-release.yml @@ -10,4 +10,4 @@ jobs: with: port_name: beman-exemplar secrets: - VCPKG_REGISTRY_TOKEN: ${{ secrets.VCPKG_REGISTRY_TOKEN }} \ No newline at end of file + VCPKG_REGISTRY_TOKEN: ${{ secrets.VCPKG_REGISTRY_TOKEN }} From 18974fd5459a6138b7cfba90ceff447fe951bd43 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 14:08:36 -0400 Subject: [PATCH 07/12] Sync templates with repo state after rebase onto main Fix discrepancies that check_copier.sh catches between the Copier template output and the actual exemplar repo: - Bump template version from 2.4.0 to 2.4.1 in CMakeLists.txt.jinja - Update infra-workflows refs from @1.7.2 to @1.7.3 in ci_tests and pre-commit-update templates - Fix {%- endraw %} to {% endraw %} in pre-commit-update and vcpkg-release templates to preserve trailing newlines - Add blank line between cron schedule and concurrency in ci_tests - Update README badge format to multi-line clickable badges matching main's style - Exclude .claude/ from check_copier.sh diff check_copier.sh now passes cleanly for both the exemplar self-test and the non-exemplar project generation. --- copier/check_copier.sh | 1 + template/.github/workflows/ci_tests.yml.jinja | 11 ++++++----- .../.github/workflows/pre-commit-update.yml.jinja | 4 ++-- template/.github/workflows/vcpkg-release.yml.jinja | 2 +- template/CMakeLists.txt.jinja | 2 +- template/README.md.jinja | 9 ++++++++- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/copier/check_copier.sh b/copier/check_copier.sh index ca1dd83e..fc6b7dbe 100755 --- a/copier/check_copier.sh +++ b/copier/check_copier.sh @@ -74,6 +74,7 @@ check_consistency() { local diff_path="$work_dir/default.diff" diff -u -r \ --exclude .git \ + --exclude .claude \ --exclude build \ --exclude .venv \ --exclude template \ diff --git a/template/.github/workflows/ci_tests.yml.jinja b/template/.github/workflows/ci_tests.yml.jinja index 51a86df2..24c032f4 100644 --- a/template/.github/workflows/ci_tests.yml.jinja +++ b/template/.github/workflows/ci_tests.yml.jinja @@ -10,6 +10,7 @@ on: workflow_dispatch: schedule: - cron: '{{ ci_tests_cron }}' + concurrency: group: {% raw %}${{format('{0}:{1}', github.repository, github.ref)}}{% endraw %} @@ -17,7 +18,7 @@ concurrency: jobs: beman-submodule-check: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.7.2 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.7.3 {% if generating_exemplar %} copier-test: @@ -62,7 +63,7 @@ jobs: {% endif %} preset-test: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.7.2 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.7.3 with: matrix_config: > [ @@ -77,7 +78,7 @@ jobs: ] build-and-test: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.7.2 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.7.3 with: matrix_config: > { @@ -229,7 +230,7 @@ jobs: } vcpkg-ci: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-vcpkg-ci.yml@1.7.2 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-vcpkg-ci.yml@1.7.3 with: port_name: beman-{{project_name.replace('_', '-')}} feature_combinations: | @@ -241,4 +242,4 @@ jobs: create-issue-when-fault: needs: [preset-test, build-and-test] if: failure() && github.event_name == 'schedule' - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.7.2 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.7.3 diff --git a/template/.github/workflows/pre-commit-update.yml.jinja b/template/.github/workflows/pre-commit-update.yml.jinja index ef365fb1..93e8f2da 100644 --- a/template/.github/workflows/pre-commit-update.yml.jinja +++ b/template/.github/workflows/pre-commit-update.yml.jinja @@ -10,8 +10,8 @@ on: {% raw -%} jobs: auto-update-pre-commit: - uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-update-pre-commit.yml@1.7.2 + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-update-pre-commit.yml@1.7.3 secrets: APP_ID: ${{ secrets.AUTO_PR_BOT_APP_ID }} PRIVATE_KEY: ${{ secrets.AUTO_PR_BOT_PRIVATE_KEY }} -{%- endraw %} +{% endraw %} diff --git a/template/.github/workflows/vcpkg-release.yml.jinja b/template/.github/workflows/vcpkg-release.yml.jinja index b1d31b87..2e4e9cae 100644 --- a/template/.github/workflows/vcpkg-release.yml.jinja +++ b/template/.github/workflows/vcpkg-release.yml.jinja @@ -12,4 +12,4 @@ jobs: {%- raw %} secrets: VCPKG_REGISTRY_TOKEN: ${{ secrets.VCPKG_REGISTRY_TOKEN }} -{%- endraw %} +{% endraw %} diff --git a/template/CMakeLists.txt.jinja b/template/CMakeLists.txt.jinja index e70bf583..98ea0e49 100644 --- a/template/CMakeLists.txt.jinja +++ b/template/CMakeLists.txt.jinja @@ -9,7 +9,7 @@ project( DESCRIPTION "{{description}}" LANGUAGES CXX {% if generating_exemplar %} - VERSION 2.4.0 + VERSION 2.4.1 {% else %} VERSION 0.1.0 {% endif %} diff --git a/template/README.md.jinja b/template/README.md.jinja index 261fc59d..69e21f24 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -154,7 +154,14 @@ What follows is an example of a Beman library README. # beman.exemplar: A Beman Library Exemplar -![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit-check.yml/badge.svg) [![Coverage](https://coveralls.io/repos/github/bemanproject/exemplar/badge.svg?branch=main)](https://coveralls.io/github/bemanproject/exemplar?branch=main) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg) [![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/4qEPK87va) + +[![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg)](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#the-beman-library-maturity-model) +[![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg)](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml) +[![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit-check.yml/badge.svg)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit-check.yml) +[![Coverage](https://coveralls.io/repos/github/bemanproject/exemplar/badge.svg?branch=main)](https://coveralls.io/github/bemanproject/exemplar?branch=main) +![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg) +[![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/4qEPK87va) + `beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md). This can be used as a template for those intending to write Beman libraries. From f4ccad9746e7fe31129b75a2312c153eace81f43 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 14:22:47 -0400 Subject: [PATCH 08/12] Fix non-exemplar template parity with cookiecutter output Restructure template/README.md.jinja so the heading, badges, description, license, and usage sections are shared between exemplar and non-exemplar output, matching what cookiecutter produced. Previously the non-exemplar branch rendered only "TODO" with no heading or badges. Guard exemplar-specific paths in .gitattributes.jinja (template/, copier/) and .pre-commit-config.yaml.jinja exclude pattern so generated non-exemplar projects don't reference directories they don't contain. Fix extra blank line in CONTRIBUTING.md.jinja rendering and remove the include-path NOTE that main deliberately removed. --- README.md | 10 +++----- template/.gitattributes.jinja | 2 ++ template/.pre-commit-config.yaml.jinja | 4 +++ template/CONTRIBUTING.md.jinja | 3 ++- template/README.md.jinja | 35 ++++++++++++++++---------- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8f1911f5..1eb8cf5a 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,10 @@ What follows is an example of a Beman library README. # beman.exemplar: A Beman Library Exemplar + + [![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg)](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#the-beman-library-maturity-model) [![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg)](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml) @@ -359,9 +363,3 @@ include an appropriate `beman.exemplar` header from your source code. ```c++ #include ``` - -> [!NOTE] -> -> `beman.exemplar` headers are to be included with the `beman/exemplar/` prefix. -> Altering include search paths to spell the include target another way (e.g. -> `#include `) is unsupported. diff --git a/template/.gitattributes.jinja b/template/.gitattributes.jinja index 9a7df7dd..0a4f6598 100644 --- a/template/.gitattributes.jinja +++ b/template/.gitattributes.jinja @@ -1,6 +1,8 @@ infra/** linguist-vendored +{% if generating_exemplar -%} template/** linguist-vendored copier/** linguist-vendored +{% endif -%} *.bib -linguist-detectable *.tex -linguist-detectable papers/* linguist-documentation diff --git a/template/.pre-commit-config.yaml.jinja b/template/.pre-commit-config.yaml.jinja index dfdc94d9..c1fbf880 100644 --- a/template/.pre-commit-config.yaml.jinja +++ b/template/.pre-commit-config.yaml.jinja @@ -48,4 +48,8 @@ repos: - id: beman-tidy {% endif %} +{% if generating_exemplar -%} exclude: 'template/|copier/|infra/|port/' +{% else -%} +exclude: 'infra/|port/' +{% endif -%} diff --git a/template/CONTRIBUTING.md.jinja b/template/CONTRIBUTING.md.jinja index 1f1f9ddd..e0211e08 100644 --- a/template/CONTRIBUTING.md.jinja +++ b/template/CONTRIBUTING.md.jinja @@ -105,8 +105,9 @@ ctest --test-dir build The file `./lockfile.json` configures the list of dependencies and versions that will be acquired by FetchContent. +{% if generating_exemplar %} -{% if generating_exemplar %}## Developing The Copier Template +## Developing The Copier Template This repository doubles as both the exemplar library and the Copier template used to stamp new Beman libraries. diff --git a/template/README.md.jinja b/template/README.md.jinja index 69e21f24..7f6be163 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -152,31 +152,46 @@ The consistency check renders from a `.git`-free temporary snapshot of the repos What follows is an example of a Beman library README. -# beman.exemplar: A Beman Library Exemplar +{% endif %} +# beman.{{project_name}}: {{description}} + + [![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg)](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#the-beman-library-maturity-model) -[![Continuous Integration Tests](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml/badge.svg)](https://github.com/bemanproject/exemplar/actions/workflows/ci_tests.yml) -[![Lint Check (pre-commit)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit-check.yml/badge.svg)](https://github.com/bemanproject/exemplar/actions/workflows/pre-commit-check.yml) -[![Coverage](https://coveralls.io/repos/github/bemanproject/exemplar/badge.svg?branch=main)](https://coveralls.io/github/bemanproject/exemplar?branch=main) +[![Continuous Integration Tests](https://github.com/{{owner}}/{{project_name}}/actions/workflows/ci_tests.yml/badge.svg)](https://github.com/{{owner}}/{{project_name}}/actions/workflows/ci_tests.yml) +[![Lint Check (pre-commit)](https://github.com/{{owner}}/{{project_name}}/actions/workflows/pre-commit-check.yml/badge.svg)](https://github.com/{{owner}}/{{project_name}}/actions/workflows/pre-commit-check.yml) +[![Coverage](https://coveralls.io/repos/github/{{owner}}/{{project_name}}/badge.svg?branch=main)](https://coveralls.io/github/{{owner}}/{{project_name}}?branch=main) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg) -[![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/4qEPK87va) +{% if generating_exemplar %}[![Compiler Explorer Example](https://img.shields.io/badge/Try%20it%20on%20Compiler%20Explorer-grey?logo=compilerexplorer&logoColor=67c52a)](https://godbolt.org/z/4qEPK87va) +{% endif %} +{% if generating_exemplar %} `beman.exemplar` is a minimal C++ library conforming to [The Beman Standard](https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md). This can be used as a template for those intending to write Beman libraries. It may also find use as a minimal and modern C++ project structure. +{% else %} +`beman.{{project_name}}` is (... TODO: description). +{% endif %} -**Implements**: `std::identity` proposed in [Standard Library Concepts (P0898R3)](https://wg21.link/P0898R3). +{% if generating_exemplar %} +**Implements**: `std::identity` proposed in [Standard Library Concepts ({{paper}})](https://wg21.link/{{paper}}). +{% else %} +**Implements**: `std::todo` proposed in [TODO ({{paper}})](https://wg21.link/{{paper}}). +{% endif %} **Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#under-development-and-not-yet-ready-for-production-use) ## License -`beman.exemplar` is licensed under the Apache License v2.0 with LLVM Exceptions. +`beman.{{project_name}}` is licensed under the Apache License v2.0 with LLVM Exceptions. ## Usage +{% if generating_exemplar %} `std::identity` is a function object type whose `operator()` returns its argument unchanged. `std::identity` serves as the default projection in constrained algorithms. Its direct usage is usually not needed. @@ -364,9 +379,3 @@ include an appropriate `beman.{{project_name}}` header from your source code. ```c++ #include ``` - -> [!NOTE] -> -> `beman.{{project_name}}` headers are to be included with the `beman/{{project_name}}/` prefix. -> Altering include search paths to spell the include target another way (e.g. -> `#include <{{project_name}}.hpp>`) is unsupported. From 66b1f43c0c97d3131b8d03d71fa8b9708627b0e8 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Thu, 18 Jun 2026 14:37:41 -0400 Subject: [PATCH 09/12] Add SPDX license identifier to .copier-answers.yml template beman-tidy requires SPDX-License-Identifier in all files within the first 25 lines. Add the header to the answers file template so generated projects pass beman-tidy out of the box. --- template/.copier-answers.yml.jinja | 1 + 1 file changed, 1 insertion(+) diff --git a/template/.copier-answers.yml.jinja b/template/.copier-answers.yml.jinja index cefabb93..27150d7b 100644 --- a/template/.copier-answers.yml.jinja +++ b/template/.copier-answers.yml.jinja @@ -1,3 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # Standard answers and internal state managed by Copier {{ _copier_answers | to_nice_yaml -}} From 255e5b4c4bc193d0ccf2e18749b91c89df8da174 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 11 Jul 2026 15:07:53 -0400 Subject: [PATCH 10/12] Fix test_standard_project shebang; auto-format generated projects - test_standard_project.sh: drop two stray lines above the shebang that demoted it off line 1, causing the script to run under /bin/sh and fail on ${BASH_SOURCE[0]} ("Bad substitution"). - Add copier/format_project.sh, run as a post-generation copier task, to reformat the generated tree with the project's own formatters (gersemi, clang-format). gersemi/clang-format wrapping depends on the resolved project-name length, so the static template cannot be pre-formatted for every name; without this a fresh project fails its own pre-commit/CI on the first run. No-op for the exemplar, so copier/exemplar consistency (check_copier.sh) is preserved. - Add copier/lint_standard_project.sh and a copier-lint CI job that generates a standard project and asserts it self-lints cleanly. --- .github/workflows/ci_tests.yml | 16 ++++++++++ copier.yml | 5 +++ copier/format_project.sh | 54 +++++++++++++++++++++++++++++++++ copier/lint_standard_project.sh | 52 +++++++++++++++++++++++++++++++ copier/test_standard_project.sh | 2 -- 5 files changed, 127 insertions(+), 2 deletions(-) create mode 100755 copier/format_project.sh create mode 100755 copier/lint_standard_project.sh diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 6ccb3976..e02683c8 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -37,6 +37,22 @@ jobs: # gcc-release will use GCC 15 which supports C++23 modules appropriately in this container run: ./copier/test_standard_project.sh gcc-release + copier-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + shell: bash + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Generated project self-lints cleanly + shell: bash + # Verify a freshly generated standard project passes its own + # pre-commit config (gersemi, clang-format, beman-tidy) on the first + # run, exercising copier/format_project.sh. + run: ./copier/lint_standard_project.sh my_project + copier-cmake-matrix: runs-on: ubuntu-latest container: diff --git a/copier.yml b/copier.yml index a3503542..316e0b08 100644 --- a/copier.yml +++ b/copier.yml @@ -98,3 +98,8 @@ _tasks: mv "tests/beman/{{ project_name }}/identity.test.cpp" "tests/beman/{{ project_name }}/todo.test.cpp"; git ls-remote https://github.com/bemanproject/exemplar.git HEAD | awk '{print $1}' > .exemplar_version; fi + # Reformat the generated tree with the project's own formatters (gersemi, + # clang-format) so it is lint-clean on the first pre-commit / CI run. This + # is a no-op for the already-formatted exemplar, preserving copier/exemplar + # consistency (see copier/check_copier.sh). + - bash "{{ _copier_conf.src_path }}/copier/format_project.sh" diff --git a/copier/format_project.sh b/copier/format_project.sh new file mode 100755 index 00000000..6e9328b3 --- /dev/null +++ b/copier/format_project.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Post-generation formatting for copier-generated projects. +# +# Applies the same formatters the generated project's pre-commit config +# enforces (gersemi for CMake, clang-format for C/C++), so a freshly +# generated project is lint-clean on its very first `pre-commit run` and +# its first CI pre-commit check. This matters because gersemi/clang-format +# wrapping decisions depend on the resolved project name length, so the +# static template cannot be pre-formatted for every possible name. +# +# Invoked by copier as a task with CWD set to the generated project root. +# Best-effort: if the formatter tooling is unavailable it warns and exits 0 +# rather than aborting project generation. + +set -uo pipefail + +# Keep these in sync with template/.pre-commit-config.yaml.jinja. +GERSEMI_VERSION="0.27.6" +CLANG_FORMAT_VERSION="22.1.5" + +# Directories excluded from formatting (mirrors the pre-commit `exclude`). +prune=(-path ./infra -o -path ./port -o -path ./.git -o -path ./build) + +if ! command -v uvx >/dev/null 2>&1; then + echo "warning: 'uvx' not found; skipping post-generation formatting." >&2 + echo " Run 'pre-commit run --all-files' after generation to format." >&2 + exit 0 +fi + +# --- CMake: gersemi --- +mapfile -d '' cmake_files < <( + find . \( "${prune[@]}" \) -prune -o \ + \( -name CMakeLists.txt -o -name '*.cmake' \) -type f -print0 +) +if ((${#cmake_files[@]})); then + echo "Formatting ${#cmake_files[@]} CMake file(s) with gersemi ${GERSEMI_VERSION}..." + uvx "gersemi==${GERSEMI_VERSION}" --in-place "${cmake_files[@]}" \ + || echo "warning: gersemi formatting failed; continuing." >&2 +fi + +# --- C/C++: clang-format --- +mapfile -d '' cpp_files < <( + find . \( "${prune[@]}" \) -prune -o \ + \( -name '*.cpp' -o -name '*.hpp' -o -name '*.cppm' \ + -o -name '*.cc' -o -name '*.cxx' -o -name '*.h' \ + -o -name '*.hh' -o -name '*.ixx' \) -type f -print0 +) +if ((${#cpp_files[@]})); then + echo "Formatting ${#cpp_files[@]} C/C++ file(s) with clang-format ${CLANG_FORMAT_VERSION}..." + uvx --from "clang-format==${CLANG_FORMAT_VERSION}" clang-format -i "${cpp_files[@]}" \ + || echo "warning: clang-format formatting failed; continuing." >&2 +fi diff --git a/copier/lint_standard_project.sh b/copier/lint_standard_project.sh new file mode 100755 index 00000000..b943dd26 --- /dev/null +++ b/copier/lint_standard_project.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Generate a standard (non-exemplar) project from the template and verify it +# is lint-clean on its very first `pre-commit run --all-files`. This exercises +# the post-generation formatting task (copier/format_project.sh) and the +# generated project's own pre-commit config (gersemi, clang-format, +# beman-tidy, ...), catching template changes that would leave a freshly +# generated project failing its own CI lint check. + +set -euo pipefail + +script_dir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") +repo_root=$(realpath "$script_dir/..") + +# beman-tidy derives the expected library/target names from the repository +# directory name, so generate into a directory named after the project. +project_name=${1:-my_project} + +work_dir=$(mktemp -d) +cleanup() { rm -rf "$work_dir"; } +trap cleanup EXIT + +out_dir="$work_dir/$project_name" +echo "Generating standard project '$project_name' in $out_dir..." + +# Use `--vcs-ref HEAD` so we exercise the most recent commit, matching +# test_standard_project.sh. +uvx --from copier copier copy \ + --trust \ + --defaults \ + --vcs-ref=HEAD \ + -d project_name="$project_name" \ + -d generating_exemplar=false \ + -d unit_test_library=gtest \ + "$repo_root" \ + "$out_dir" + +cd "$out_dir" + +# pre-commit needs a git repository; beman-tidy expects the default branch to +# be `main`. +git -c init.defaultBranch=main init -q +git config user.email "ci@example.com" +git config user.name "Beman CI" +git add -A +git commit -qm "Generated standard project" + +echo "Running pre-commit on the generated project..." +uvx pre-commit run --all-files --show-diff-on-failure + +echo "✔ Generated project '$project_name' self-lints cleanly." diff --git a/copier/test_standard_project.sh b/copier/test_standard_project.sh index 82bc2639..d604662f 100755 --- a/copier/test_standard_project.sh +++ b/copier/test_standard_project.sh @@ -1,5 +1,3 @@ -#="${CMAKE_COMMAND:-cmake}" -#="${CTEST_COMMAND:-ctest}" #!/usr/bin/env bash # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception set -euo pipefail From eeaffc6b0791685ae7397431c906cc9535e9c9b6 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 12 Jul 2026 14:01:34 -0400 Subject: [PATCH 11/12] Add copier-lint job to ci_tests template The exemplar's ci_tests.yml carried a copier-lint job that the template did not generate, causing check_copier.sh consistency check to fail. Add the job to the template inside the generating_exemplar block so stamped output matches the source. --- template/.github/workflows/ci_tests.yml.jinja | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/template/.github/workflows/ci_tests.yml.jinja b/template/.github/workflows/ci_tests.yml.jinja index 24c032f4..a0b3c101 100644 --- a/template/.github/workflows/ci_tests.yml.jinja +++ b/template/.github/workflows/ci_tests.yml.jinja @@ -39,6 +39,22 @@ jobs: # gcc-release will use GCC 15 which supports C++23 modules appropriately in this container run: ./copier/test_standard_project.sh gcc-release + copier-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + shell: bash + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Generated project self-lints cleanly + shell: bash + # Verify a freshly generated standard project passes its own + # pre-commit config (gersemi, clang-format, beman-tidy) on the first + # run, exercising copier/format_project.sh. + run: ./copier/lint_standard_project.sh my_project + copier-cmake-matrix: runs-on: ubuntu-latest container: From 431efb25086728d151edcfa80522c95ec2ccac5f Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 12 Jul 2026 22:23:16 -0400 Subject: [PATCH 12/12] Update infra from upstream --- infra/.beman_submodule | 2 +- infra/cmake/enable-experimental-import-std.cmake | 6 +++++- template/infra/.beman_submodule.jinja | 2 +- .../infra/cmake/enable-experimental-import-std.cmake.jinja | 6 +++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/infra/.beman_submodule b/infra/.beman_submodule index 628b5d0d..5cbeca37 100644 --- a/infra/.beman_submodule +++ b/infra/.beman_submodule @@ -1,3 +1,3 @@ [beman_submodule] remote=https://github.com/bemanproject/infra.git -commit_hash=7b66b858d7f48428fca936184aef2bd246ccc81a +commit_hash=7fc85e2e26cb491926ca2744c359c2aa7ea8fd27 diff --git a/infra/cmake/enable-experimental-import-std.cmake b/infra/cmake/enable-experimental-import-std.cmake index e41a5f7c..3c6d6887 100644 --- a/infra/cmake/enable-experimental-import-std.cmake +++ b/infra/cmake/enable-experimental-import-std.cmake @@ -1,5 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.3.0") +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.4.0") + set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD + "f35a9ac6-8463-4d38-8eec-5d6008153e7d" + ) +elseif(CMAKE_VERSION VERSION_GREATER_EQUAL "4.3.0") set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "451f2fe2-a8a2-47c3-bc32-94786d8fc91b" ) diff --git a/template/infra/.beman_submodule.jinja b/template/infra/.beman_submodule.jinja index 628b5d0d..5cbeca37 100644 --- a/template/infra/.beman_submodule.jinja +++ b/template/infra/.beman_submodule.jinja @@ -1,3 +1,3 @@ [beman_submodule] remote=https://github.com/bemanproject/infra.git -commit_hash=7b66b858d7f48428fca936184aef2bd246ccc81a +commit_hash=7fc85e2e26cb491926ca2744c359c2aa7ea8fd27 diff --git a/template/infra/cmake/enable-experimental-import-std.cmake.jinja b/template/infra/cmake/enable-experimental-import-std.cmake.jinja index e41a5f7c..3c6d6887 100644 --- a/template/infra/cmake/enable-experimental-import-std.cmake.jinja +++ b/template/infra/cmake/enable-experimental-import-std.cmake.jinja @@ -1,5 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.3.0") +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.4.0") + set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD + "f35a9ac6-8463-4d38-8eec-5d6008153e7d" + ) +elseif(CMAKE_VERSION VERSION_GREATER_EQUAL "4.3.0") set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "451f2fe2-a8a2-47c3-bc32-94786d8fc91b" )