diff --git a/Makefile b/Makefile index ce5a47f..51f187a 100644 --- a/Makefile +++ b/Makefile @@ -132,17 +132,17 @@ docs-trees: ## Regenerate tree blocks in documentation uv run python scripts/gen_tree_docs.py docs: ensure-docs-coverage-report ## Build documentation - uv run mkdocs build --config-file=config/mkdocs.yml + uv run properdocs build --config-file=config/mkdocs.yml docs-serve: ensure-docs-coverage-report ## Serve documentation locally - uv run mkdocs serve --config-file=config/mkdocs.yml + uv run properdocs serve --config-file=config/mkdocs.yml docs-serve-open: ensure-docs-coverage-report ## Serve documentation and open in default browser @(sleep 2 && uv run python -m webbrowser "http://127.0.0.1:8000") & - uv run mkdocs serve --config-file=config/mkdocs.yml + uv run properdocs serve --config-file=config/mkdocs.yml docs-check: ensure-docs-coverage-report ## Check documentation for issues - uv run mkdocs build --config-file=config/mkdocs.yml --strict + uv run properdocs build --config-file=config/mkdocs.yml --strict check-docs: docs-check ## Alias for docs-check diff --git a/config/coverage.ini b/config/coverage.ini index 0321c85..c2a2713 100644 --- a/config/coverage.ini +++ b/config/coverage.ini @@ -8,6 +8,7 @@ omit = src/repoman/__init__.py src/repoman/__main__.py src/repoman/main_template/* + src/repoman/cpp_template/template/* tests/__init__.py tests/conftest.py diff --git a/docs/cli.md b/docs/cli.md index 8cc818b..6214783 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -63,12 +63,12 @@ repoman inspect --path ./my-project --answers ./my-project/custom-answers.yml ### create -Create a new Python project using the repoman template. For the default (interactive or answers-based) flow, use options; project name is passed with `--project_name` / `-pn`. Alternatively, use a subcommand (e.g. `create cli`) and pass the project name as a positional argument. +Create a new project using a bundled repoman template. For the default (interactive or answers-based) flow, use options; project name is passed with `--project_name` / `-pn`. Alternatively, use a subcommand (e.g. `create cli`) and pass the project name as a positional argument. ```bash repoman create [OPTIONS] # or with a preset subcommand (positional project name): -repoman create cli PROJECT_NAME [OPTIONS] +repoman create cli cli PROJECT_NAME [OPTIONS] ``` **Options (default create)** @@ -79,7 +79,7 @@ repoman create cli PROJECT_NAME [OPTIONS] | `--template` | `-t` | Path to custom template (defaults to bundled main template) | | `--output` | `-o` | Output directory (defaults to current directory) | | `--answers` | `-a` | Path to answers file for non-interactive use | -| `--preset` | `-p` | Use a preset: `cli`, `docs-only`, `library`, `fastapi` (non-interactive) | +| `--preset` | `-p` | Use a preset: `cli`, `cpp`, `docs-only`, `library`, `fastapi` (non-interactive) | | `--force` | `-f` | Force overwrite of existing files | | `--dry-run` | | Show what would be created without creating | @@ -90,6 +90,7 @@ repoman create --project_name my-new-project repoman create -pn my-app --output /path/to/parent --force repoman create --project_name my-app --answers .copier-answers.yml --dry-run repoman create -pn my-app --preset cli +repoman create -pn my-lib --preset cpp ``` **Create subcommands (preset-based, positional project name)** @@ -99,6 +100,7 @@ Each subcommand creates a project using a preset and takes the project name as a | Subcommand | Preset | Description | | ---------- | -------- | -------------------------------------------------------- | | `create cli` | cli | Plain CLI project (no FastAPI or dataset) | +| `create cpp` | cpp | Meson-based C++ library project | | `create docs` | docs_only| Documentation-only project | | `create library` | library | Library-style project (no CLI entry point) | | `create fastapi` | fastapi | Project with FastAPI enabled | @@ -106,10 +108,11 @@ Each subcommand creates a project using a preset and takes the project name as a **Examples (subcommands)** ```bash -repoman create cli my-cli-app -repoman create library my-lib --dry-run -repoman create docs docs-project --output /path/to/parent -repoman create fastapi my-api +repoman create cli cli my-cli-app +repoman create cpp cpp my-cpp-lib +repoman create library library my-lib --dry-run +repoman create docs docs-only docs-project --output /path/to/parent +repoman create fastapi fastapi my-api ``` ### update diff --git a/docs/concepts/generated-project.md b/docs/concepts/generated-project.md index a96a6ff..2235eac 100644 --- a/docs/concepts/generated-project.md +++ b/docs/concepts/generated-project.md @@ -8,7 +8,7 @@ Generated projects typically include: - `src//` for the main Python package - `tests/` for pytest-based tests -- `config/` for Ruff, MyPy, pytest, MkDocs, and related config +- `config/` for Ruff, MyPy, pytest, ProperDocs, and related config - `docs/` for project documentation - `make_cmds/` for Makefile fragments - `pyproject.toml` for package metadata and dependencies diff --git a/docs/development/README.md b/docs/development/README.md index fba9824..393509d 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -57,7 +57,7 @@ Welcome to the repoman development environment! This guide will help you set up - **[Code Quality](code-quality.md)** — Linting (Ruff), type checking (MyPy) - **[CLI messages](cli-messages.md)** — Centralized error/warning message text and panels - **[Config validation](config-validation.md)** — Pydantic-based answers validation flow -- **[Documentation](documentation.md)** — MkDocs, Material theme, building docs +- **[Documentation](documentation.md)** — ProperDocs, Material for MkDocs theme, building docs - **[CI](ci.md)** — Continuous integration and local simulation - **[Debugging](debugging.md)** — Common issues and debug tools - **[Performance Tips](performance-tips.md)** — Fast development workflow diff --git a/docs/development/additional-resources.md b/docs/development/additional-resources.md index 0b2e20a..0b5c4ff 100644 --- a/docs/development/additional-resources.md +++ b/docs/development/additional-resources.md @@ -10,7 +10,7 @@ - **[uv Documentation](https://docs.astral.sh/uv/)**: uv package manager guide - **[Pytest Documentation](https://docs.pytest.org/)**: Testing framework guide - **[Ruff Documentation](https://docs.astral.sh/ruff/)**: Linting and formatting guide -- **[MkDocs Documentation](https://www.mkdocs.org/)**: Static site generator guide +- **[ProperDocs Documentation](https://properdocs.org/)**: Static site generator guide - **[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)**: Theme documentation - **[mkdocstrings Documentation](https://mkdocstrings.github.io/)**: API documentation generator diff --git a/docs/development/documentation.md b/docs/development/documentation.md index 9574b87..992a1a8 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -1,6 +1,6 @@ # Documentation -The project uses **MkDocs** with the **Material theme** for documentation. This provides a modern, responsive documentation site with excellent features like search, navigation, and code highlighting. +The project uses **ProperDocs** with the **Material for MkDocs theme** for documentation. This provides a modern, responsive documentation site with excellent features like search, navigation, and code highlighting. ## Documentation Structure @@ -59,7 +59,7 @@ docs ``` -MkDocs is configured in `config/mkdocs.yml` (in the repo root). The `docs_dir` points to this `docs/` folder. +ProperDocs is configured in `config/mkdocs.yml` (in the repo root). The `docs_dir` points to this `docs/` folder. ## Documentation map @@ -104,7 +104,7 @@ flowchart LR update --> troubleshooting ``` -## MkDocs Configuration +## ProperDocs Configuration The documentation is configured in `config/mkdocs.yml` with the following features: diff --git a/docs/development/tools.md b/docs/development/tools.md index bfb7c18..f67b6ad 100644 --- a/docs/development/tools.md +++ b/docs/development/tools.md @@ -112,10 +112,10 @@ format: uv run ruff format src/ tests/ docs: - uv run mkdocs build --config-file=config/mkdocs.yml + uv run properdocs build --config-file=config/mkdocs.yml docs-serve: - uv run mkdocs serve --config-file=config/mkdocs.yml + uv run properdocs serve --config-file=config/mkdocs.yml clean: rm -rf build/ dist/ *.egg-info/ .pytest_cache/ htmlcov/ site/ @@ -130,11 +130,11 @@ To add documentation commands to your Makefile, add these lines: # Documentation # ####################### docs: ## Build documentation - uv run mkdocs build --config-file=config/mkdocs.yml + uv run properdocs build --config-file=config/mkdocs.yml docs-serve: ## Serve documentation locally - uv run mkdocs serve --config-file=config/mkdocs.yml + uv run properdocs serve --config-file=config/mkdocs.yml docs-check: ## Check documentation for issues - uv run mkdocs build --config-file=config/mkdocs.yml --strict + uv run properdocs build --config-file=config/mkdocs.yml --strict ``` diff --git a/docs/template-prompts.md b/docs/template-prompts.md index 9c60e36..06d73a3 100644 --- a/docs/template-prompts.md +++ b/docs/template-prompts.md @@ -48,3 +48,18 @@ When `dataset_enabled` is true, at least one dataset modality must be selected. ## Where answers are stored Generated projects store answers in `.copier-answers.yml`. Repoman uses that file for `repoman update` and for non-interactive workflows. + +## C++ template prompts + +The bundled C++ template lives in `src/repoman/cpp_template/` and is selected +with `--preset cpp` or `repoman create cpp cpp`. In addition to project, author, +repository, and license metadata, it prompts for: + +| Prompt | Description | Default | +| -------------------- | -------------------------------------- | ------------------------- | +| `cpp_namespace` | C++ namespace and public include dir | project name with `_` | +| `cpp_library_name` | Meson library target name | project name with `_` | +| `cpp_standard` | C++ language standard | `c++17` | +| `cpp_build_cli` | Build the example CLI executable | `true` | +| `cpp_build_examples` | Build example programs | `true` | +| `cpp_build_tests` | Build test executables | `true` | diff --git a/docs/template-structure.md b/docs/template-structure.md index 97f37f9..d480ff6 100644 --- a/docs/template-structure.md +++ b/docs/template-structure.md @@ -8,6 +8,7 @@ - **Datasets:** If `dataset_enabled` is true, the template generates `config/dataset_config.json`, `src/{{ python_package_import_name }}/config/dataset_config.py`, and the selected dataset loaders/adapters under `src/{{ python_package_import_name }}/datasets/`. - **Docs-only:** If `docs_only` is true, source package and test files are omitted. - **Notebooks:** If `python_notebooks` is true, the template generates a `notebooks/` folder. +- **C++:** The bundled `cpp_template` is a separate Meson-based template with `include/`, `src/`, `tests/`, `examples/`, Doxygen hooks, and the same repoman Make target names used by CI. ## High-level generated layout @@ -40,3 +41,20 @@ my_project/ - **`src//datasets/`** contains the selected dataset loaders and helpers when dataset support is enabled. For prompt-level details, see [Template prompts](template-prompts.md). For generated behavior, see [Template architecture](concepts/template-architecture.md). + +## C++ generated layout + +The `cpp` preset and `create cpp cpp` subcommand use `src/repoman/cpp_template/`. +The generated project looks like: + +```text +my_cpp_library/ +├── config/ +├── docs/ +├── examples/ +├── include/ +│ └── / +├── scripts/ +├── src/ +└── tests/ +``` diff --git a/pyproject.toml b/pyproject.toml index 08da71c..6e16ca5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,12 +64,11 @@ docs = [ "mkdocs-literate-nav>=0.6.1", "mkdocs-minify-plugin>=0.8.0", "mkdocs-jupyter>=0.24.8", - "mkdocs>=1.6.1", "mkdocstrings[python]>=1.0.0", "mkdocs-table-reader-plugin>=3.1.0", "mkdocs-charts-plugin>=0.0.13", "pymdown-extensions>=10.12", "mkdocs-material>=9.7.6", + "properdocs>=1.6.7", ] - diff --git a/src/repoman/cli/commands/create/__init__.py b/src/repoman/cli/commands/create/__init__.py index 0ca54eb..90999e8 100644 --- a/src/repoman/cli/commands/create/__init__.py +++ b/src/repoman/cli/commands/create/__init__.py @@ -8,6 +8,7 @@ from repoman.cli.commands.create._shared import run_create from repoman.cli.commands.create.cli_only import app as cli_only_app +from repoman.cli.commands.create.cpp import app as cpp_app from repoman.cli.commands.create.docs_only import app as docs_only_app from repoman.cli.commands.create.fastapi_mvc import app as fastapi_mvc_app from repoman.cli.commands.create.library import app as library_app @@ -18,6 +19,7 @@ app = Typer(add_completion=True) app.add_typer(cli_only_app, name="cli") +app.add_typer(cpp_app, name="cpp") app.add_typer(docs_only_app, name="docs") app.add_typer(library_app, name="library") app.add_typer(fastapi_mvc_app, name="fastapi") @@ -59,7 +61,7 @@ def create( Option( "--preset", "-p", - help="Use a preset: cli, docs-only, library, fastapi", + help="Use a preset: cli, cpp, docs-only, library, fastapi", ), ] = None, dry_run: Annotated[ @@ -69,12 +71,14 @@ def create( ) -> None: """Create a new Python project using the repoman template. - Use a preset, a typed subcommand (cli, docs, library, fastapi), or an answers file. + Use a preset, a typed subcommand (cli, cpp, docs, library, fastapi), or an answers file. Examples: repoman create -pn my-app --preset cli + repoman create -pn my-lib --preset cpp repoman create -pn my-app --preset fastapi -o ~/projects repoman create cli cli my-app + repoman create cpp cpp my-lib repoman create fastapi fastapi my-app --force repoman create -pn my-app -a ./my-answers.yml """ @@ -93,16 +97,11 @@ def create( ) raise Exit(1) from None - if template_path is None: - current_file = Path(__file__) - template_path_obj = current_file.parent.parent.parent.parent - logger.info(f"Using main template at {template_path_obj}") - else: - template_path_obj = Path(template_path) - output_dir_base = Path.cwd() if output_dir is None else Path(output_dir) if answers_file is not None: + template_path_obj = Path(__file__).parent.parent.parent.parent if template_path is None else Path(template_path) + logger.info(f"Using main template at {template_path_obj}") answers_path = Path(answers_file).resolve() if not answers_path.exists(): console.print(error_panel(answers_file_not_found(answers_path), console=console)) @@ -112,17 +111,27 @@ def create( data = yaml.safe_load(f) or {} elif preset is not None: preset_key = preset.replace("-", "_") # docs-only -> docs_only - valid = ("cli", "docs_only", "library", "fastapi") + valid = ("cli", "cpp", "docs_only", "library", "fastapi") if preset_key not in valid: console.print( error_panel( - f"Invalid preset '{preset}'. Choose from: cli, docs-only, library, fastapi", + f"Invalid preset '{preset}'. Choose from: cli, cpp, docs-only, library, fastapi", console=console, ) ) raise Exit(1) from None + if template_path is None and preset_key == "cpp": + template_path_obj = Path(__file__).parent.parent.parent.parent / "cpp_template" + logger.info(f"Using C++ template at {template_path_obj}") + else: + template_path_obj = ( + Path(__file__).parent.parent.parent.parent if template_path is None else Path(template_path) + ) + logger.info(f"Using main template at {template_path_obj}") data = build_preset_data(preset_key, project_name) else: + template_path_obj = Path(__file__).parent.parent.parent.parent if template_path is None else Path(template_path) + logger.info(f"Using main template at {template_path_obj}") data = None run_create( diff --git a/src/repoman/cli/commands/create/cpp.py b/src/repoman/cli/commands/create/cpp.py new file mode 100644 index 0000000..bd99080 --- /dev/null +++ b/src/repoman/cli/commands/create/cpp.py @@ -0,0 +1,58 @@ +"""C++ create subcommand: Meson-based C++ library.""" + +from pathlib import Path +from typing import Annotated + +from typer import Argument, Option, Typer + +from repoman.cli.commands.create._shared import run_create +from repoman.copier import build_preset_data + +app = Typer( + add_completion=True, + help="""Create a Meson-based C++ library project. + +Examples: + + repoman create cpp cpp my-lib + repoman create cpp cpp my-lib -o ./libs --dry-run +""", +) + + +def _default_template_path() -> Path: + """Return the bundled C++ template path.""" + current_file = Path(__file__) + return current_file.parent.parent.parent.parent / "cpp_template" + + +@app.command(no_args_is_help=True) +def cpp( + project_name: str = Argument(..., help="Name of the project to create"), + template_path: Annotated[ + str | None, + Option( + "--template", + "-t", + help="Path to custom template (defaults to bundled C++ template)", + ), + ] = None, + output_dir: Annotated[ + str | None, + Option("--output", "-o", help="Output directory (defaults to current directory)"), + ] = None, + force: bool = Option(False, "--force", "-f", help="Overwrite existing files"), + dry_run: bool = Option(False, "--dry-run", help="Show what would be created without creating"), +) -> None: + """Create a Meson-based C++ library project.""" + template_path_obj = _default_template_path() if template_path is None else Path(template_path) + output_dir_base = Path.cwd() if output_dir is None else Path(output_dir) + data = build_preset_data("cpp", project_name) + run_create( + project_name=project_name, + output_dir=output_dir_base, + template_path=template_path_obj, + data=data, + force=force, + dry_run=dry_run, + ) diff --git a/src/repoman/copier/presets.py b/src/repoman/copier/presets.py index e19d2c0..82974d9 100644 --- a/src/repoman/copier/presets.py +++ b/src/repoman/copier/presets.py @@ -17,6 +17,11 @@ "fastapi_enabled": False, "dataset_enabled": False, }, + "cpp": { + "fastapi_enabled": False, + "dataset_enabled": False, + "python_package_command_line_name": "", + }, "docs_only": { "docs_only": True, "fastapi_enabled": False, @@ -37,10 +42,21 @@ def build_preset_data(preset_name: str, project_name: str) -> dict: """Build copier data from preset overrides and base defaults.""" - base = yaml.safe_load(get_copier_answers_template()) or {} + base = {} if preset_name == "cpp" else yaml.safe_load(get_copier_answers_template()) or {} overrides = PRESETS.get(preset_name, {}).copy() if "python_package_command_line_name" not in overrides: overrides["python_package_command_line_name"] = slugify(project_name) + if preset_name == "cpp": + cpp_name = slugify(project_name, "_") + overrides.setdefault("project_description", "") + overrides.setdefault("repository_provider", "github") + overrides.setdefault("copyright_license", "MIT") + overrides.setdefault("cpp_namespace", cpp_name) + overrides.setdefault("cpp_library_name", cpp_name) + overrides.setdefault("cpp_standard", "c++17") + overrides.setdefault("cpp_build_cli", True) + overrides.setdefault("cpp_build_examples", True) + overrides.setdefault("cpp_build_tests", True) return {**base, **overrides} diff --git a/src/repoman/cpp_template/__init__.py b/src/repoman/cpp_template/__init__.py new file mode 100644 index 0000000..a26f5da --- /dev/null +++ b/src/repoman/cpp_template/__init__.py @@ -0,0 +1 @@ +"""C++ project Copier template.""" diff --git a/src/repoman/cpp_template/copier.yml b/src/repoman/cpp_template/copier.yml new file mode 100644 index 0000000..736e60e --- /dev/null +++ b/src/repoman/cpp_template/copier.yml @@ -0,0 +1,132 @@ +# CONFIGURATION ------------------------- +_min_copier_version: "9" +_envops: + autoescape: false + keep_trailing_newline: true + trim_blocks: true + lstrip_blocks: true +_subdirectory: template/ +_templates_suffix: .jinja +_preserve_symlinks: true +_jinja_extensions: +- copier_template_extensions.TemplateExtensionLoader +- extensions.py:CurrentYearExtension +- extensions.py:GitExtension +- extensions.py:SlugifyExtension +_tasks: + - git init + + +# PROMPT -------------------------------- + +ci: + type: str + help: Your repository URL (hostname; derived from repository provider) + default: "{{ repository_provider }}.com" + choices: + - github.com + - gitlab.com + +project_name: + type: str + help: Your project name + placeholder: my-cpp-library + validator: "{% if not (project_name | regex_search('^[a-z][a-z0-9-]*$')) %}Use lowercase letters, digits, and hyphens only.{% endif %}" + +project_description: + type: str + help: Your project description + +author_fullname: + type: str + help: Your full name + default: "{{ 'Leandro G. Almeida' | git_user_name }}" + +author_email: + type: str + help: Your email + default: "{{ 'leandro.g.almeida@gmail.com' | git_user_email }}" + +author_username: + type: str + help: Your username (e.g. on GitHub) + default: "lalmei" + +repository_provider: + type: str + help: Your repository provider + default: github + choices: + - github + - gitlab + +repository_namespace: + type: str + help: Your repository namespace + default: "{{ author_username }}" + +repository_name: + type: str + help: Your repository name + default: "{{ project_name | slugify }}" + +copyright_holder: + type: str + help: The name of the person/entity holding the copyright + default: "{{ author_fullname }}" + +copyright_holder_email: + type: str + help: The email of the person/entity holding the copyright + default: "{{ author_email }}" + +copyright_date: + type: str + help: The copyright date + default: "{{ current_year }}" + +copyright_license: + type: str + help: Your project's license + default: MIT + choices: + Apache License 2.0: Apache-2.0 + BSD 3-Clause "New" or "Revised" License: BSD-3-Clause + ISC License: ISC + MIT License: MIT + +cpp_namespace: + type: str + help: C++ namespace and public include directory + default: "{{ project_name | slugify('_') }}" + validator: "{% if not (cpp_namespace | regex_search('^[a-z][a-z0-9_]*$')) %}Use a valid lowercase C++ namespace identifier.{% endif %}" + +cpp_library_name: + type: str + help: Meson library target name + default: "{{ project_name | slugify('_') }}" + validator: "{% if not (cpp_library_name | regex_search('^[a-z][a-z0-9_]*$')) %}Use lowercase letters, digits, and underscores only.{% endif %}" + +cpp_standard: + type: str + help: C++ language standard + default: c++17 + choices: + - c++17 + - c++20 + - c++23 + +cpp_build_cli: + type: bool + help: Build the example command-line executable + default: true + +cpp_build_examples: + type: bool + help: Build example programs + default: true + +cpp_build_tests: + type: bool + help: Build test executables + default: true diff --git a/src/repoman/cpp_template/extensions.py b/src/repoman/cpp_template/extensions.py new file mode 100644 index 0000000..60a3451 --- /dev/null +++ b/src/repoman/cpp_template/extensions.py @@ -0,0 +1,5 @@ +"""Jinja extensions available to the C++ Copier template.""" + +from repoman.extensions import CurrentYearExtension, GitExtension, SlugifyExtension + +__all__ = ["CurrentYearExtension", "GitExtension", "SlugifyExtension"] diff --git a/src/repoman/cpp_template/template/.clang-format.jinja b/src/repoman/cpp_template/template/.clang-format.jinja new file mode 100644 index 0000000..cc94049 --- /dev/null +++ b/src/repoman/cpp_template/template/.clang-format.jinja @@ -0,0 +1,16 @@ +BasedOnStyle: LLVM +Language: Cpp +Standard: {{ cpp_standard }} +ColumnLimit: 80 +IndentWidth: 2 +TabWidth: 2 +UseTab: Never +AccessModifierOffset: -2 +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +BreakBeforeBraces: Attach +DerivePointerAlignment: false +PointerAlignment: Right +SortIncludes: CaseSensitive +NamespaceIndentation: None diff --git a/src/repoman/cpp_template/template/.dockerignore.jinja b/src/repoman/cpp_template/template/.dockerignore.jinja new file mode 100644 index 0000000..8323a33 --- /dev/null +++ b/src/repoman/cpp_template/template/.dockerignore.jinja @@ -0,0 +1,8 @@ +.git +.cache +.venv +.venvs +build +dist +site +*.egg-info diff --git a/src/repoman/cpp_template/template/.gitignore.jinja b/src/repoman/cpp_template/template/.gitignore.jinja new file mode 100644 index 0000000..a7f1249 --- /dev/null +++ b/src/repoman/cpp_template/template/.gitignore.jinja @@ -0,0 +1,29 @@ +# editors +.idea/ +.cursor/ +.vscode/ + +# build outputs +/build/ +/dist/ +/site/ +*.o +*.obj +*.a +*.lib +*.so +*.dylib +*.dll +*.exe + +# docs and python tooling +.venv/ +.venvs/ +uv.lock +*.egg-info/ + +# cache +.cache/ +.pytest_cache/ +.ruff_cache/ +__pycache__/ diff --git a/src/repoman/cpp_template/template/AI_POLICY.md.jinja b/src/repoman/cpp_template/template/AI_POLICY.md.jinja new file mode 100644 index 0000000..a3dc932 --- /dev/null +++ b/src/repoman/cpp_template/template/AI_POLICY.md.jinja @@ -0,0 +1,8 @@ +# AI Usage Policy + +External contributions that use AI assistance must disclose the tool and the +extent of use in the pull request or issue. AI-assisted pull requests must be +verified by a human before submission. + +AI-generated media is not accepted for this project unless maintainers make an +explicit exception. diff --git a/src/repoman/cpp_template/template/CHANGELOG.md.jinja b/src/repoman/cpp_template/template/CHANGELOG.md.jinja new file mode 100644 index 0000000..b4e2281 --- /dev/null +++ b/src/repoman/cpp_template/template/CHANGELOG.md.jinja @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + diff --git a/src/repoman/cpp_template/template/CODE_OF_CONDUCT.md.jinja b/src/repoman/cpp_template/template/CODE_OF_CONDUCT.md.jinja new file mode 100644 index 0000000..44d7dd6 --- /dev/null +++ b/src/repoman/cpp_template/template/CODE_OF_CONDUCT.md.jinja @@ -0,0 +1,5 @@ +# Code of Conduct + +This project follows the Contributor Covenant Code of Conduct. + +Report unacceptable behavior to {{ author_email }}. diff --git a/src/repoman/cpp_template/template/CONTRIBUTING.md.jinja b/src/repoman/cpp_template/template/CONTRIBUTING.md.jinja new file mode 100644 index 0000000..eb6c2d5 --- /dev/null +++ b/src/repoman/cpp_template/template/CONTRIBUTING.md.jinja @@ -0,0 +1,29 @@ +# Contributing + +Contributions are welcome. + +## Environment setup + +Fork and clone the repository, then run: + +```bash +cd {{ repository_name }} +make setup +make build +make test +``` + +You need a C++ compiler, `clang-format`, and `uv`. Meson and Ninja are installed +through `uv` for local development. + +## Development + +Before committing: + +1. Run `make format`. +1. Run `make check`. +1. Run `make test`. +1. Run `make docs-check` if you changed documentation. + +Please review [AI Usage Policy](AI_POLICY.md) before submitting external +contributions that used AI assistance. diff --git a/src/repoman/cpp_template/template/Dockerfile.dev.jinja b/src/repoman/cpp_template/template/Dockerfile.dev.jinja new file mode 100644 index 0000000..c5fa7cc --- /dev/null +++ b/src/repoman/cpp_template/template/Dockerfile.dev.jinja @@ -0,0 +1,24 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + clang-format \ + cmake \ + curl \ + doxygen \ + gdb \ + git \ + meson \ + nano \ + ninja-build \ + pkg-config \ + python3-pip \ + python3-venv \ + unzip \ + vim \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspaces/{{ project_name }} diff --git a/src/repoman/cpp_template/template/Doxyfile.jinja b/src/repoman/cpp_template/template/Doxyfile.jinja new file mode 100644 index 0000000..e50b7fe --- /dev/null +++ b/src/repoman/cpp_template/template/Doxyfile.jinja @@ -0,0 +1,37 @@ +# Doxygen configuration for {{ project_name }} public API documentation. + +PROJECT_NAME = "{{ project_name }}" +PROJECT_BRIEF = "{{ project_description }}" +OUTPUT_DIRECTORY = build/doxygen +CREATE_SUBDIRS = NO + +INPUT = include/{{ cpp_namespace }} +FILE_PATTERNS = *.hpp +RECURSIVE = NO +EXCLUDE = include/{{ cpp_namespace }}/version.hpp \ + include/{{ cpp_namespace }}/meson.build + +EXTRACT_ALL = NO +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = NO +EXTRACT_LOCAL_CLASSES = NO +HIDE_UNDOC_MEMBERS = YES +HIDE_UNDOC_CLASSES = YES +HIDE_FRIEND_COMPOUNDS = YES + +BUILTIN_STL_SUPPORT = YES +CPP_CLI_SUPPORT = NO + +GENERATE_HTML = YES +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_PROGRAMLISTING = NO + +QUIET = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = YES + +JAVADOC_AUTOBRIEF = YES +AUTOLINK_SUPPORT = YES +MARKDOWN_SUPPORT = YES diff --git a/src/repoman/cpp_template/template/LICENSE.jinja b/src/repoman/cpp_template/template/LICENSE.jinja new file mode 100644 index 0000000..fb5c771 --- /dev/null +++ b/src/repoman/cpp_template/template/LICENSE.jinja @@ -0,0 +1,27 @@ +{% if copyright_license == "MIT" -%} +MIT License + +Copyright (c) {{ copyright_date }} {{ copyright_holder }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +{% else -%} +Copyright (c) {{ copyright_date }} {{ copyright_holder }} + +SPDX-License-Identifier: {{ copyright_license }} +{% endif -%} diff --git a/src/repoman/cpp_template/template/Makefile.jinja b/src/repoman/cpp_template/template/Makefile.jinja new file mode 100644 index 0000000..37fbd36 --- /dev/null +++ b/src/repoman/cpp_template/template/Makefile.jinja @@ -0,0 +1,103 @@ +SHELL := bash + +version := 0.1.0 + +BUILD_DIR ?= build +IMAGE ?= {{ project_name }}-dev +DOCS_CONFIG ?= config/mkdocs.yml +MESON ?= uv run meson +NINJA ?= uv run ninja +CLANG_FORMAT ?= clang-format +DOXYGEN ?= doxygen + +cxx.format.files := $(shell find include src tests examples -type f \( -name "*.hpp" -o -name "*.cpp" \) 2>/dev/null || :) + +.PHONY: setup configure build test clean distclean format format-check lint check-types type-check check-quality check docs docs-serve docs-check check-docs docs-api docs-check-api docker-build docker-shell help + +setup: ## Install Python-hosted developer tooling. + @$(call i, Installing developer tooling) + uv sync + +configure: ## Configure the Meson build directory. + @$(call i, Configuring Meson build) + @if [ ! -d "$(BUILD_DIR)" ]; then $(MESON) setup "$(BUILD_DIR)"; fi + +build: configure ## Build the C++ library and executables. + @$(call i, Building project) + $(MESON) compile -C "$(BUILD_DIR)" + +test: build ## Run all tests. + @$(call i, Running tests) + $(MESON) test -C "$(BUILD_DIR)" + +clean: ## Clean compiled outputs while keeping the Meson build directory. + @if [ -d "$(BUILD_DIR)" ]; then $(NINJA) -C "$(BUILD_DIR)" clean; fi + +distclean: ## Remove all generated build and docs outputs. + rm -rf "$(BUILD_DIR)" site + +format: ## Format C++ sources with clang-format. + @$(call i, Formatting C++ sources) + @if [ -n "$(cxx.format.files)" ]; then $(CLANG_FORMAT) -i $(cxx.format.files); fi + +format-check: ## Check C++ formatting with clang-format. + @$(call i, Checking C++ formatting) + @if [ -n "$(cxx.format.files)" ]; then $(CLANG_FORMAT) --dry-run --Werror $(cxx.format.files); fi + +lint: build ## Compile with the template warning policy. + @$(call i, Compiler warning checks passed) + +check-types: build ## Alias for build-time C++ type checks. + +type-check: check-types ## Alias for check-types. + +check-quality: format-check lint ## Run formatting and compiler-warning checks. + +check: check-quality check-types ## Run all quality checks. + +docs-api: ## Generate Doxygen API documentation. + @$(call i, Generating Doxygen documentation) + $(DOXYGEN) Doxyfile + +docs-check-api: docs-api ## Check Doxygen API documentation generation. + +docs: ## Build documentation. + @$(call i, Building documentation) + uv run properdocs build --config-file="$(DOCS_CONFIG)" + +docs-serve: ## Serve documentation locally. + @$(call i, Serving documentation) + uv run properdocs serve --config-file="$(DOCS_CONFIG)" + +docs-check: ## Check documentation in strict mode. + @$(call i, Checking documentation) + uv run properdocs build --config-file="$(DOCS_CONFIG)" --strict + +check-docs: docs-check ## Alias for docs-check. + +docker-build: ## Build the development container image. + docker build -f Dockerfile.dev -t "$(IMAGE)" . + +docker-shell: ## Start an interactive shell in the development container. + docker run --rm -it -v "$$PWD":/workspaces/{{ project_name }} -w /workspaces/{{ project_name }} "$(IMAGE)" bash + +help: ## Print the help screen. + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":|:[[:space:]].*?##"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' + +define i +echo +python3 scripts/colors.py INFO "$1" +echo +endef + +define w +echo +python3 scripts/colors.py WARN "$1" +echo +endef + +define e +echo +python3 scripts/colors.py ERROR "$1" +echo +endef diff --git a/src/repoman/cpp_template/template/README.md.jinja b/src/repoman/cpp_template/template/README.md.jinja new file mode 100644 index 0000000..06a3f61 --- /dev/null +++ b/src/repoman/cpp_template/template/README.md.jinja @@ -0,0 +1,53 @@ +{% set repo_host_by_provider = {"github": "github.com", "gitlab": "gitlab.com"} -%} +{% set repo_host = repo_host_by_provider[repository_provider] -%} +{% set repo_url = "https://" ~ repo_host ~ "/" ~ repository_namespace ~ "/" ~ repository_name -%} +{% if repository_provider == "github" -%} +{% set docs_url = "https://" ~ repository_namespace ~ ".github.io/" ~ repository_name -%} +{% elif repository_provider == "gitlab" -%} +{% set docs_url = "https://" ~ repository_namespace ~ ".gitlab.io/" ~ repository_name -%} +{% endif -%} +# {{ project_name }} + +{% if repository_provider == "github" -%} +[![ci](https://github.com/{{ repository_namespace }}/{{ repository_name }}/actions/workflows/ci.yml/badge.svg)](https://github.com/{{ repository_namespace }}/{{ repository_name }}/actions/workflows/ci.yml) +{% endif -%} +[![documentation](https://img.shields.io/badge/docs-properdocs-708FCC.svg?style=flat)]({{ docs_url }}/) +[![license](https://img.shields.io/badge/license-{{ copyright_license }}-blue.svg?style=flat)](LICENSE) +[![c++](https://img.shields.io/badge/{{ cpp_standard | replace('+', '%2B') }}-library-blue.svg?style=flat)](#building) + +{{ project_description }} + +## Building + +Install the developer tooling and configure the Meson build directory: + +```bash +make setup +make configure +make build +``` + +Run the test suite: + +```bash +make test +``` + +Format and check the project: + +```bash +make format +make check +``` + +## Layout + +```text +include/{{ cpp_namespace }}/ Public headers +src/ Library and CLI sources +tests/ Meson test executables +examples/ Small example programs +docs/ ProperDocs documentation +``` + +The public umbrella header is `include/{{ cpp_namespace }}/{{ cpp_namespace }}.hpp`. diff --git a/src/repoman/cpp_template/template/config/mkdocs.yml.jinja b/src/repoman/cpp_template/template/config/mkdocs.yml.jinja new file mode 100644 index 0000000..6323ea4 --- /dev/null +++ b/src/repoman/cpp_template/template/config/mkdocs.yml.jinja @@ -0,0 +1,114 @@ +{% set repo_host_by_provider = {"github": "github.com", "gitlab": "gitlab.com"} -%} +{% set repo_host = repo_host_by_provider[repository_provider] -%} +{% set repo_url = "https://" ~ repo_host ~ "/" ~ repository_namespace ~ "/" ~ repository_name -%} +{% if repository_provider == "github" -%} +{% set site_url_value = "https://" ~ repository_namespace ~ ".github.io/" ~ repository_name -%} +{% elif repository_provider == "gitlab" -%} +{% set site_url_value = "https://" ~ repository_namespace ~ ".gitlab.io/" ~ repository_name -%} +{% endif -%} +site_name: "{{ project_name }}" +site_description: "{{ project_description }}" +site_url: "{{ site_url_value }}" +repo_url: "{{ repo_url }}" +repo_name: "{{ repository_namespace }}/{{ repository_name }}" +docs_dir: "../docs" +site_dir: "{% if repository_provider == 'github' %}../site{% endif %}" +watch: [../README.md, ../CONTRIBUTING.md, ../CHANGELOG.md, ../AI_POLICY.md, ../include, ../src, ../docs] +copyright: Copyright © {{ copyright_date }} {{ copyright_holder }} +edit_uri: edit/main/docs/ + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + +nav: +- Home: + - Overview: index.md + - Changelog: changelog.md + - License: license.md +- Architecture: reference/architecture.md +- API reference: reference/api.md +- Development: + - Build and Test: development/build-and-test.md + - API Documentation: development/api-documentation.md + - Contributing: contributing.md + - Code of Conduct: code_of_conduct.md + - AI Usage Policy: ai_policy.md + +theme: + name: material + + features: + - announce.dismiss + - content.action.edit + - content.action.view + - content.code.copy + - content.tooltips + - navigation.footer + - navigation.path + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - search.highlight + - search.suggest + - toc.follow + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: teal + accent: purple + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: lime + toggle: + icon: material/weather-night + name: Switch to system preference + +extra_css: +- css/material.css +- css/mkdocstrings.css + +extra_javascript: +- js/feedback.js + +markdown_extensions: +- attr_list +- admonition +- callouts +- footnotes +- pymdownx.magiclink +- pymdownx.superfences +- pymdownx.tabbed: + alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower +- pymdownx.tasklist: + custom_checkbox: true +- toc: + permalink: "¤" + +plugins: +- search +- section-index +- git-revision-date-localized: + enabled: !ENV [DEPLOY, false] + enable_creation_date: true + type: timeago +- minify: + minify_html: !ENV [DEPLOY, false] + +extra: + social: + - icon: fontawesome/brands/{{ repository_provider }} + link: https://{{ repo_host }}/{{ author_username }} diff --git a/src/repoman/cpp_template/template/docs/ai_policy.md.jinja b/src/repoman/cpp_template/template/docs/ai_policy.md.jinja new file mode 100644 index 0000000..9d43dda --- /dev/null +++ b/src/repoman/cpp_template/template/docs/ai_policy.md.jinja @@ -0,0 +1,5 @@ +# AI Usage Policy + +External contributions that use AI assistance must disclose the tool and the +extent of use in the pull request or issue. AI-assisted pull requests must be +verified by a human before submission. diff --git a/src/repoman/cpp_template/template/docs/changelog.md.jinja b/src/repoman/cpp_template/template/docs/changelog.md.jinja new file mode 100644 index 0000000..ab66fc4 --- /dev/null +++ b/src/repoman/cpp_template/template/docs/changelog.md.jinja @@ -0,0 +1,3 @@ +# Changelog + +All notable changes to this project are tracked in the repository changelog. diff --git a/src/repoman/cpp_template/template/docs/code_of_conduct.md.jinja b/src/repoman/cpp_template/template/docs/code_of_conduct.md.jinja new file mode 100644 index 0000000..44d7dd6 --- /dev/null +++ b/src/repoman/cpp_template/template/docs/code_of_conduct.md.jinja @@ -0,0 +1,5 @@ +# Code of Conduct + +This project follows the Contributor Covenant Code of Conduct. + +Report unacceptable behavior to {{ author_email }}. diff --git a/src/repoman/cpp_template/template/docs/contributing.md.jinja b/src/repoman/cpp_template/template/docs/contributing.md.jinja new file mode 100644 index 0000000..1524b33 --- /dev/null +++ b/src/repoman/cpp_template/template/docs/contributing.md.jinja @@ -0,0 +1,4 @@ +# Contributing + +Set up the project with `make setup`, then run `make build`, `make test`, and +`make check` before submitting changes. diff --git a/src/repoman/cpp_template/template/docs/css/material.css.jinja b/src/repoman/cpp_template/template/docs/css/material.css.jinja new file mode 100644 index 0000000..aad98fc --- /dev/null +++ b/src/repoman/cpp_template/template/docs/css/material.css.jinja @@ -0,0 +1,3 @@ +:root { + --md-primary-fg-color: #006a6a; +} diff --git a/src/repoman/cpp_template/template/docs/css/mkdocstrings.css.jinja b/src/repoman/cpp_template/template/docs/css/mkdocstrings.css.jinja new file mode 100644 index 0000000..b4ae980 --- /dev/null +++ b/src/repoman/cpp_template/template/docs/css/mkdocstrings.css.jinja @@ -0,0 +1 @@ +/* Reserved for documentation plugin overrides. */ diff --git a/src/repoman/cpp_template/template/docs/development/api-documentation.md.jinja b/src/repoman/cpp_template/template/docs/development/api-documentation.md.jinja new file mode 100644 index 0000000..3ba1d16 --- /dev/null +++ b/src/repoman/cpp_template/template/docs/development/api-documentation.md.jinja @@ -0,0 +1,16 @@ +# API Documentation + +Public declarations should be documented in headers under +`include/{{ cpp_namespace }}/`. + +Use Doxygen to check generated API output: + +```bash +make docs-api +``` + +Use ProperDocs to check the documentation site: + +```bash +make docs-check +``` diff --git a/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja b/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja new file mode 100644 index 0000000..ef11baa --- /dev/null +++ b/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja @@ -0,0 +1,25 @@ +# Build and Test + +Install developer tooling: + +```bash +make setup +``` + +Build the project: + +```bash +make build +``` + +Run tests: + +```bash +make test +``` + +Run all local checks: + +```bash +make check +``` diff --git a/src/repoman/cpp_template/template/docs/index.md.jinja b/src/repoman/cpp_template/template/docs/index.md.jinja new file mode 100644 index 0000000..525459e --- /dev/null +++ b/src/repoman/cpp_template/template/docs/index.md.jinja @@ -0,0 +1,19 @@ +--- +title: Overview +hide: +- feedback +--- + +# {{ project_name }} + +{{ project_description }} + +## Building + +```bash +make setup +make build +make test +``` + +The public C++ API lives in `include/{{ cpp_namespace }}/`. diff --git a/src/repoman/cpp_template/template/docs/js/feedback.js.jinja b/src/repoman/cpp_template/template/docs/js/feedback.js.jinja new file mode 100644 index 0000000..e8a6258 --- /dev/null +++ b/src/repoman/cpp_template/template/docs/js/feedback.js.jinja @@ -0,0 +1 @@ +// Reserved for documentation feedback hooks. diff --git a/src/repoman/cpp_template/template/docs/license.md.jinja b/src/repoman/cpp_template/template/docs/license.md.jinja new file mode 100644 index 0000000..d32df38 --- /dev/null +++ b/src/repoman/cpp_template/template/docs/license.md.jinja @@ -0,0 +1,3 @@ +# License + +This project is licensed under `{{ copyright_license }}`. diff --git a/src/repoman/cpp_template/template/docs/reference/api.md.jinja b/src/repoman/cpp_template/template/docs/reference/api.md.jinja new file mode 100644 index 0000000..ed00827 --- /dev/null +++ b/src/repoman/cpp_template/template/docs/reference/api.md.jinja @@ -0,0 +1,12 @@ +# API Reference + +The public C++ headers are in `include/{{ cpp_namespace }}/`. + +Run Doxygen locally when you need generated API artifacts: + +```bash +make docs-api +``` + +The hand-written API overview should stay in this page or in nearby reference +pages. Generated Doxygen output is kept under `build/doxygen/`. diff --git a/src/repoman/cpp_template/template/docs/reference/architecture.md.jinja b/src/repoman/cpp_template/template/docs/reference/architecture.md.jinja new file mode 100644 index 0000000..a9a213e --- /dev/null +++ b/src/repoman/cpp_template/template/docs/reference/architecture.md.jinja @@ -0,0 +1,24 @@ +# Architecture + +The generated project follows the same C++ shape as `spinach`: public headers +in `include/`, implementation files in `src/`, tests in `tests/`, examples in +`examples/`, and documentation under `docs/`. + +## Library + +The public API lives under `include/{{ cpp_namespace }}/`. The umbrella header is +`include/{{ cpp_namespace }}/{{ cpp_namespace }}.hpp`; implementation lives in +`src/{{ cpp_namespace }}.cpp`. + +## Build + +Meson owns the C++ build graph. The root `meson.build` declares the library +target, optional CLI executable, optional examples, and optional tests. The +Makefile preserves the standard repoman workflow names (`make setup`, +`make build`, `make test`, `make check`, and `make docs-check`). + +## Documentation + +ProperDocs is the primary documentation site generator. Doxygen is configured in +`Doxyfile` and can be run with `make docs-api` when you want generated C++ API +artifacts. diff --git a/src/repoman/cpp_template/template/examples/basic.cpp.jinja b/src/repoman/cpp_template/template/examples/basic.cpp.jinja new file mode 100644 index 0000000..19ece64 --- /dev/null +++ b/src/repoman/cpp_template/template/examples/basic.cpp.jinja @@ -0,0 +1,9 @@ +#include "{{ cpp_namespace }}/{{ cpp_namespace }}.hpp" + +#include + +int main() { + std::cout << "{{ project_name }} " << {{ cpp_namespace }}::version() << '\n'; + std::cout << "2 + 3 = " << {{ cpp_namespace }}::add(2, 3) << '\n'; + return 0; +} diff --git a/src/repoman/cpp_template/template/examples/meson.build.jinja b/src/repoman/cpp_template/template/examples/meson.build.jinja new file mode 100644 index 0000000..e0fdacb --- /dev/null +++ b/src/repoman/cpp_template/template/examples/meson.build.jinja @@ -0,0 +1,5 @@ +basic_example = executable( + 'basic', + 'basic.cpp', + dependencies: [{{ cpp_library_name }}_dep], +) diff --git a/src/repoman/cpp_template/template/include/{{cpp_namespace}}/meson.build.jinja b/src/repoman/cpp_template/template/include/{{cpp_namespace}}/meson.build.jinja new file mode 100644 index 0000000..9a78e0d --- /dev/null +++ b/src/repoman/cpp_template/template/include/{{cpp_namespace}}/meson.build.jinja @@ -0,0 +1,5 @@ +configure_file( + input: 'version.hpp.in', + output: 'version.hpp', + configuration: config, +) diff --git a/src/repoman/cpp_template/template/include/{{cpp_namespace}}/version.hpp.in.jinja b/src/repoman/cpp_template/template/include/{{cpp_namespace}}/version.hpp.in.jinja new file mode 100644 index 0000000..d1653af --- /dev/null +++ b/src/repoman/cpp_template/template/include/{{cpp_namespace}}/version.hpp.in.jinja @@ -0,0 +1,3 @@ +#pragma once + +#define {{ cpp_namespace | upper }}_VERSION @{{ cpp_namespace | upper }}_VERSION@ diff --git a/src/repoman/cpp_template/template/include/{{cpp_namespace}}/{{cpp_namespace}}.hpp.jinja b/src/repoman/cpp_template/template/include/{{cpp_namespace}}/{{cpp_namespace}}.hpp.jinja new file mode 100644 index 0000000..8901d27 --- /dev/null +++ b/src/repoman/cpp_template/template/include/{{cpp_namespace}}/{{cpp_namespace}}.hpp.jinja @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace {{ cpp_namespace }} { + +/// Return the library version from the generated Meson configuration. +std::string version(); + +/// Add two integers. +int add(int lhs, int rhs); + +} // namespace {{ cpp_namespace }} diff --git a/src/repoman/cpp_template/template/meson.build.jinja b/src/repoman/cpp_template/template/meson.build.jinja new file mode 100644 index 0000000..6740649 --- /dev/null +++ b/src/repoman/cpp_template/template/meson.build.jinja @@ -0,0 +1,56 @@ +project( + '{{ project_name }}', + 'cpp', + version: '0.1.0', + license: '{{ copyright_license }}', + default_options: [ + 'cpp_std={{ cpp_standard }}', + 'warning_level=2', + 'werror=true', + ], +) + +config = configuration_data() +config.set_quoted('{{ cpp_namespace | upper }}_VERSION', meson.project_version()) + +subdir('include/{{ cpp_namespace }}') + +public_include = include_directories('include') + +{{ cpp_library_name }}_sources = files( + 'src/{{ cpp_namespace }}.cpp', +) + +{{ cpp_library_name }}_lib = library( + '{{ cpp_library_name }}', + {{ cpp_library_name }}_sources, + include_directories: public_include, + install: true, +) + +{{ cpp_library_name }}_dep = declare_dependency( + include_directories: public_include, + link_with: {{ cpp_library_name }}_lib, +) + +if get_option('build_cli') + executable( + '{{ project_name }}', + 'src/cli.cpp', + dependencies: [{{ cpp_library_name }}_dep], + install: true, + ) +endif + +install_headers( + 'include/{{ cpp_namespace }}/{{ cpp_namespace }}.hpp', + subdir: '{{ cpp_namespace }}', +) + +if get_option('build_tests') + subdir('tests') +endif + +if get_option('build_examples') + subdir('examples') +endif diff --git a/src/repoman/cpp_template/template/meson_options.txt.jinja b/src/repoman/cpp_template/template/meson_options.txt.jinja new file mode 100644 index 0000000..b96ccb9 --- /dev/null +++ b/src/repoman/cpp_template/template/meson_options.txt.jinja @@ -0,0 +1,3 @@ +option('build_cli', type: 'boolean', value: {{ "true" if cpp_build_cli else "false" }}, description: 'Build the example command-line executable') +option('build_examples', type: 'boolean', value: {{ "true" if cpp_build_examples else "false" }}, description: 'Build example programs') +option('build_tests', type: 'boolean', value: {{ "true" if cpp_build_tests else "false" }}, description: 'Build test executables') diff --git a/src/repoman/cpp_template/template/pyproject.toml.jinja b/src/repoman/cpp_template/template/pyproject.toml.jinja new file mode 100644 index 0000000..34a8ba7 --- /dev/null +++ b/src/repoman/cpp_template/template/pyproject.toml.jinja @@ -0,0 +1,27 @@ +[project] +name = "{{ project_name }}-docs" +version = "0.1.0" +description = "Documentation and developer tooling for {{ project_name }}" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] + +[tool.uv] +package = false +default-groups = ["ci", "docs"] + +[dependency-groups] +ci = [ + "meson>=1.5.0", + "ninja>=1.11.1", +] +docs = [ + "markdown-callouts>=0.4.0", + "markdown-exec>=1.12.0", + "mkdocs-git-revision-date-localized-plugin>=1.5.0", + "mkdocs-material>=9.7.0", + "mkdocs-minify-plugin>=0.8.0", + "mkdocs-section-index>=0.3.10", + "properdocs>=1.6.7", + "pymdown-extensions>=10.12", +] diff --git a/src/repoman/cpp_template/template/scripts/colors.py.jinja b/src/repoman/cpp_template/template/scripts/colors.py.jinja new file mode 100644 index 0000000..ca59817 --- /dev/null +++ b/src/repoman/cpp_template/template/scripts/colors.py.jinja @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Simple script to print colored messages for Makefile.""" + +import sys + + +def main(): + """Print colored message based on level.""" + if len(sys.argv) < 3: + print("Usage: python3 scripts/colors.py [INFO|WARN|ERROR] message") + sys.exit(1) + + level = sys.argv[1].upper() + message = " ".join(sys.argv[2:]) + + colors = { + "INFO": "\033[0;32m", + "WARN": "\033[0;33m", + "ERROR": "\033[0;31m", + } + reset = "\033[0m" + + print(f"{colors.get(level, '')}{message}{reset}") + + +if __name__ == "__main__": + main() diff --git a/src/repoman/cpp_template/template/src/cli.cpp.jinja b/src/repoman/cpp_template/template/src/cli.cpp.jinja new file mode 100644 index 0000000..8c400f4 --- /dev/null +++ b/src/repoman/cpp_template/template/src/cli.cpp.jinja @@ -0,0 +1,30 @@ +#include "{{ cpp_namespace }}/{{ cpp_namespace }}.hpp" + +#include +#include +#include + +namespace { + +void print_usage(const char *program) { + std::cout << "Usage: " << program << " [--version] [lhs rhs]\n"; +} + +} // namespace + +int main(int argc, char **argv) { + if (argc == 2 && std::string{argv[1]} == "--version") { + std::cout << {{ cpp_namespace }}::version() << '\n'; + return 0; + } + + if (argc == 3) { + const int lhs = std::atoi(argv[1]); + const int rhs = std::atoi(argv[2]); + std::cout << {{ cpp_namespace }}::add(lhs, rhs) << '\n'; + return 0; + } + + print_usage(argv[0]); + return argc == 1 ? 0 : 2; +} diff --git a/src/repoman/cpp_template/template/src/{{cpp_namespace}}.cpp.jinja b/src/repoman/cpp_template/template/src/{{cpp_namespace}}.cpp.jinja new file mode 100644 index 0000000..26aefab --- /dev/null +++ b/src/repoman/cpp_template/template/src/{{cpp_namespace}}.cpp.jinja @@ -0,0 +1,11 @@ +#include "{{ cpp_namespace }}/{{ cpp_namespace }}.hpp" + +#include "{{ cpp_namespace }}/version.hpp" + +namespace {{ cpp_namespace }} { + +std::string version() { return {{ cpp_namespace | upper }}_VERSION; } + +int add(int lhs, int rhs) { return lhs + rhs; } + +} // namespace {{ cpp_namespace }} diff --git a/src/repoman/cpp_template/template/tests/meson.build.jinja b/src/repoman/cpp_template/template/tests/meson.build.jinja new file mode 100644 index 0000000..ba04510 --- /dev/null +++ b/src/repoman/cpp_template/template/tests/meson.build.jinja @@ -0,0 +1,7 @@ +test_{{ cpp_library_name }} = executable( + 'test_{{ cpp_library_name }}', + 'test_{{ cpp_namespace }}.cpp', + dependencies: [{{ cpp_library_name }}_dep], +) + +test('{{ cpp_library_name }}', test_{{ cpp_library_name }}) diff --git a/src/repoman/cpp_template/template/tests/test_{{cpp_namespace}}.cpp.jinja b/src/repoman/cpp_template/template/tests/test_{{cpp_namespace}}.cpp.jinja new file mode 100644 index 0000000..a57ed3e --- /dev/null +++ b/src/repoman/cpp_template/template/tests/test_{{cpp_namespace}}.cpp.jinja @@ -0,0 +1,10 @@ +#include "{{ cpp_namespace }}/{{ cpp_namespace }}.hpp" + +#include +#include + +int main() { + assert({{ cpp_namespace }}::add(2, 3) == 5); + assert(!{{ cpp_namespace }}::version().empty()); + return 0; +} diff --git a/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/ci.yml.jinja b/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/ci.yml.jinja new file mode 100644 index 0000000..db5957f --- /dev/null +++ b/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/ci.yml.jinja @@ -0,0 +1,121 @@ +name: ci + +on: + push: + pull_request: + branches: + - main + +defaults: + run: + shell: bash + +env: + LANG: en_US.utf-8 + LC_ALL: en_US.utf-8 + PYTHONIOENCODING: UTF-8 + PYTHON_VERSIONS: "" + +jobs: + + quality: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + + - name: Install C++ formatting tools + run: sudo apt-get update && sudo apt-get install -y clang-format + + - name: Install dependencies + run: make setup + + - name: Check if the documentation builds correctly + run: make check-docs + + - name: Check the project quality + run: make check + + - name: Store objects inventory for tests + uses: actions/upload-artifact@v4 + with: + name: objects.inv + path: site/objects.inv + + tests: + + needs: + - quality + + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: + - "3.11" + - "3.12" + - "3.13" + - "3.14" + resolution: + - highest + - lowest-direct + + exclude: + - os: macos-latest + resolution: lowest-direct + - os: windows-latest + resolution: lowest-direct + runs-on: {% raw %}${{ matrix.os }}{% endraw %} + continue-on-error: {% raw %}${{ matrix.python-version == '3.14' }}{% endraw %} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: {% raw %}${{ matrix.python-version }}{% endraw %} + allow-prereleases: true + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + cache-suffix: {% raw %}${{ matrix.resolution }}{% endraw %} + + - name: Install dependencies + env: + UV_RESOLUTION: {% raw %}${{ matrix.resolution }}{% endraw %} + run: make setup + + - name: Download objects inventory + uses: actions/download-artifact@v4 + with: + name: objects.inv + path: site/ + + - name: Run the test suite + run: make test diff --git a/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/docs.yml.jinja b/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/docs.yml.jinja new file mode 100644 index 0000000..ba23693 --- /dev/null +++ b/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/docs.yml.jinja @@ -0,0 +1,83 @@ +name: docs + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +defaults: + run: + shell: bash + +env: + LANG: en_US.utf-8 + LC_ALL: en_US.utf-8 + PYTHONIOENCODING: UTF-8 + +permissions: + contents: read + +concurrency: + group: docs-{% raw %}${{ github.ref }}{% endraw %} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + + - name: Install dependencies + run: make setup + + - name: Run tests + run: make test + + - name: Check docs in strict mode + run: uv run properdocs build --config-file=config/mkdocs.yml --strict + + - name: Upload Pages artifact + if: {% raw %}${{ github.event_name != 'pull_request' }}{% endraw %} + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + if: {% raw %}${{ github.event_name != 'pull_request' }}{% endraw %} + needs: + - build + runs-on: ubuntu-latest + + permissions: + contents: read + pages: write + id-token: write + + environment: + name: github-pages + url: {% raw %}${{ steps.deployment.outputs.page_url }}{% endraw %} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/release.yml.jinja b/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/release.yml.jinja new file mode 100644 index 0000000..ce0d433 --- /dev/null +++ b/src/repoman/cpp_template/template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/release.yml.jinja @@ -0,0 +1,24 @@ +name: release + +on: push +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Setup uv + uses: astral-sh/setup-uv@v5 + - name: Prepare release notes + run: uv tool run git-changelog --release-notes > release-notes.md + - name: Create release + uses: softprops/action-gh-release@v2 + with: + body_path: release-notes.md diff --git a/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/FUNDING.yml.jinja b/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/FUNDING.yml.jinja new file mode 100644 index 0000000..0bbabe3 --- /dev/null +++ b/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/FUNDING.yml.jinja @@ -0,0 +1,2 @@ +github: {{ author_username }} +polar: {{ author_username }} diff --git a/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/workflows/ci.yml.jinja b/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/workflows/ci.yml.jinja new file mode 100644 index 0000000..072d282 --- /dev/null +++ b/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/workflows/ci.yml.jinja @@ -0,0 +1,109 @@ +name: ci + +on: + push: + pull_request: + branches: + - main + +defaults: + run: + shell: bash + +env: + LANG: en_US.utf-8 + LC_ALL: en_US.utf-8 + PYTHONIOENCODING: UTF-8 + PYTHON_VERSIONS: "" + +jobs: + + quality: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + + - name: Install C++ formatting tools + run: sudo apt-get update && sudo apt-get install -y clang-format + + - name: Install dependencies + run: make setup + + - name: Check if the documentation builds correctly + run: make check-docs + + - name: Check the project quality + run: make check + + tests: + + needs: + - quality + + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: + - "3.11" + - "3.12" + - "3.13" + - "3.14" + resolution: + - highest + - lowest-direct + + exclude: + - os: macos-latest + resolution: lowest-direct + - os: windows-latest + resolution: lowest-direct + runs-on: {% raw %}${{ matrix.os }}{% endraw %} + continue-on-error: {% raw %}${{ matrix.python-version == '3.14' }}{% endraw %} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: {% raw %}${{ matrix.python-version }}{% endraw %} + allow-prereleases: true + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + cache-suffix: {% raw %}${{ matrix.resolution }}{% endraw %} + + - name: Install dependencies + env: + UV_RESOLUTION: {% raw %}${{ matrix.resolution }}{% endraw %} + run: make setup + + - name: Run the test suite + run: make test diff --git a/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/workflows/release.yml.jinja b/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/workflows/release.yml.jinja new file mode 100644 index 0000000..ce0d433 --- /dev/null +++ b/src/repoman/cpp_template/template/{% if repository_provider == 'gitlab' %}.gitlab{% endif %}/workflows/release.yml.jinja @@ -0,0 +1,24 @@ +name: release + +on: push +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Setup uv + uses: astral-sh/setup-uv@v5 + - name: Prepare release notes + run: uv tool run git-changelog --release-notes > release-notes.md + - name: Create release + uses: softprops/action-gh-release@v2 + with: + body_path: release-notes.md diff --git a/src/repoman/cpp_template/template/{{_copier_conf.answers_file}}.jinja b/src/repoman/cpp_template/template/{{_copier_conf.answers_file}}.jinja new file mode 100644 index 0000000..dd3290f --- /dev/null +++ b/src/repoman/cpp_template/template/{{_copier_conf.answers_file}}.jinja @@ -0,0 +1,2 @@ +# Changes here will be overwritten by Copier. +{{_copier_answers|to_nice_yaml}} diff --git a/src/repoman/main_template/README.md.jinja b/src/repoman/main_template/README.md.jinja index 16ea2de..9354d01 100644 --- a/src/repoman/main_template/README.md.jinja +++ b/src/repoman/main_template/README.md.jinja @@ -13,7 +13,7 @@ {% if repository_provider == "github" and (not insiders or public_release) -%} [![ci](https://github.com/{{ repository_namespace }}/{{ repository_name }}/actions/workflows/ci.yml/badge.svg)](https://github.com/{{ repository_namespace }}/{{ repository_name }}/actions/workflows/ci.yml) {% endif -%} -[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)]({{ docs_url }}/) +[![documentation](https://img.shields.io/badge/docs-properdocs-708FCC.svg?style=flat)]({{ docs_url }}/) {% if not insiders or public_release -%} [![pypi version](https://img.shields.io/pypi/v/{{ python_package_distribution_name }}.svg)](https://pypi.org/project/{{ python_package_distribution_name }}/) {% endif -%} diff --git a/src/repoman/main_template/config/vscode/launch.json.jinja b/src/repoman/main_template/config/vscode/launch.json.jinja index f7640e5..1ec9267 100644 --- a/src/repoman/main_template/config/vscode/launch.json.jinja +++ b/src/repoman/main_template/config/vscode/launch.json.jinja @@ -25,7 +25,7 @@ "name": "docs", "type": "debugpy", "request": "launch", - "module": "mkdocs", + "module": "properdocs", "justMyCode": false, "args": ["serve", "-v"], "cwd": "${workspaceFolder}", diff --git a/src/repoman/main_template/make_cmds/documentation.mk b/src/repoman/main_template/make_cmds/documentation.mk index 23cede0..1f2dae7 100644 --- a/src/repoman/main_template/make_cmds/documentation.mk +++ b/src/repoman/main_template/make_cmds/documentation.mk @@ -5,17 +5,17 @@ .PHONY: docs docs: ## Build documentation @$(call i, Building documentation) - uv run mkdocs build --config-file=config/mkdocs.yml + uv run properdocs build --config-file=config/mkdocs.yml .PHONY: docs-serve docs-serve: ## Serve documentation locally @$(call i, Serving documentation) - uv run mkdocs serve --config-file=config/mkdocs.yml + uv run properdocs serve --config-file=config/mkdocs.yml .PHONY: docs-check docs-check: ## Check documentation for issues @$(call i, Checking documentation) - uv run mkdocs build --config-file=config/mkdocs.yml --strict + uv run properdocs build --config-file=config/mkdocs.yml --strict .PHONY: check-docs check-docs: docs-check ## Alias for docs-check diff --git a/src/repoman/main_template/pyproject.toml.jinja b/src/repoman/main_template/pyproject.toml.jinja index e87117e..2a82b08 100644 --- a/src/repoman/main_template/pyproject.toml.jinja +++ b/src/repoman/main_template/pyproject.toml.jinja @@ -137,8 +137,8 @@ docs = [ "mkdocs-minify-plugin>=0.8.0", "pymdown-extensions>=10.12", "mkdocs-jupyter>=0.24.8", - "mkdocs>=1.6.1", "mkdocstrings[python]>=1.0.0", "mkdocs-table-reader-plugin>=3.1.0", "mkdocs-charts-plugin>=0.0.13", + "properdocs>=1.6.7", ] diff --git a/src/repoman/main_template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/docs.yml.jinja b/src/repoman/main_template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/docs.yml.jinja index 34acd2c..81ec26d 100644 --- a/src/repoman/main_template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/docs.yml.jinja +++ b/src/repoman/main_template/{% if repository_provider == 'github' %}.github{% endif %}/workflows/docs.yml.jinja @@ -60,7 +60,7 @@ jobs: python scripts/generate_coverage_badge.py coverage.xml docs/coverage-badge.json - name: Check docs in strict mode - run: uv run mkdocs build --config-file=config/mkdocs.yml --strict + run: uv run properdocs build --config-file=config/mkdocs.yml --strict - name: Upload Pages artifact if: {% raw %}${{ github.event_name != 'pull_request' }}{% endraw %} diff --git a/tests/test_cli/test_create.py b/tests/test_cli/test_create.py index 3061c76..98dc767 100644 --- a/tests/test_cli/test_create.py +++ b/tests/test_cli/test_create.py @@ -14,6 +14,8 @@ from typer.testing import CliRunner console = Console() +cpp_module = importlib.import_module("repoman.cli.commands.create.cpp") +create_module = importlib.import_module("repoman.cli.commands.create") docs_only_module = importlib.import_module("repoman.cli.commands.create.docs_only") fastapi_module = importlib.import_module("repoman.cli.commands.create.fastapi_mvc") library_module = importlib.import_module("repoman.cli.commands.create.library") @@ -73,6 +75,43 @@ def test_create_command_with_preset(cli_runner: CliRunner, cli_app: Typer) -> No assert result.exit_code == 0 +def test_create_command_with_cpp_preset_uses_cpp_template(cli_runner: CliRunner, cli_app: Typer) -> None: + """The cpp preset should route to the bundled C++ template.""" + with patch("repoman.cli.commands.create.run_create") as mock_run: + result = cli_runner.invoke( + cli_app, + ["create", "--dry-run", "--preset", "cpp", "--project_name", "my-cpp-project"], + input="", + ) + + assert result.exit_code == 0 + kwargs = mock_run.call_args.kwargs + assert kwargs["template_path"] == Path(create_module.__file__).parent.parent.parent.parent / "cpp_template" + assert kwargs["data"]["cpp_namespace"] == "my_cpp_project" + + +def test_cpp_subcommand_uses_cpp_preset() -> None: + """Build C++ projects with the cpp preset and bundled C++ template path.""" + template_root = Path(cpp_module.__file__).parent.parent.parent.parent / "cpp_template" + preset = {"cpp_namespace": "shared_lib", "cpp_library_name": "shared_lib"} + + with ( + patch("repoman.cli.commands.create.cpp.build_preset_data", return_value=preset) as mock_build, + patch("repoman.cli.commands.create.cpp.run_create") as mock_run, + ): + cpp_module.cpp("shared-lib", force=False, dry_run=False) + + mock_build.assert_called_once_with("cpp", "shared-lib") + mock_run.assert_called_once_with( + project_name="shared-lib", + output_dir=Path.cwd(), + template_path=template_root, + data=preset, + force=False, + dry_run=False, + ) + + def test_docs_only_subcommand_uses_docs_preset() -> None: """Build docs-only projects with the docs preset and default paths.""" template_root = Path(docs_only_module.__file__).parent.parent.parent.parent diff --git a/tests/test_copier.py b/tests/test_copier.py index e0dd545..b84580d 100644 --- a/tests/test_copier.py +++ b/tests/test_copier.py @@ -71,6 +71,23 @@ def test_build_preset_data_fastapi_preset() -> None: assert data.get("dataset_enabled") is False +def test_build_preset_data_cpp_preset() -> None: + """C++ preset adds C++ defaults and disables Python application features.""" + data = build_preset_data("cpp", "my-cpp-lib") + assert data.get("fastapi_enabled") is False + assert data.get("dataset_enabled") is False + assert data["python_package_command_line_name"] == "" + assert data["project_description"] == "" + assert data["repository_provider"] == "github" + assert data["copyright_license"] == "MIT" + assert data["cpp_namespace"] == "my_cpp_lib" + assert data["cpp_library_name"] == "my_cpp_lib" + assert data["cpp_standard"] == "c++17" + assert data["cpp_build_cli"] is True + assert data["cpp_build_examples"] is True + assert data["cpp_build_tests"] is True + + def test_build_copier_options_without_data() -> None: """build_copier_options without data returns only src_path and dst_path.""" template_path = Path("/tpl") @@ -115,4 +132,4 @@ def test_load_prompt_schema_default_returns_dict() -> None: def test_presets_keys() -> None: """PRESETS contains expected preset names.""" - assert set(PRESETS.keys()) == {"cli", "docs_only", "library", "fastapi"} + assert set(PRESETS.keys()) == {"cli", "cpp", "docs_only", "library", "fastapi"} diff --git a/tests/test_template/test_ci.py b/tests/test_template/test_ci.py index 473a1c7..21f2af9 100644 --- a/tests/test_template/test_ci.py +++ b/tests/test_template/test_ci.py @@ -239,7 +239,9 @@ def test_template_badges_and_urls_render_for_github(tmp_path: Path) -> None: repo_url = _expected_repo_url("github", "testuser", "test-project") assert "[![ci](https://github.com/testuser/test-project/actions/workflows/ci.yml/badge.svg)]" in readme - assert f"[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)]({docs_url}/)" in readme + assert ( + f"[![documentation](https://img.shields.io/badge/docs-properdocs-708FCC.svg?style=flat)]({docs_url}/)" in readme + ) assert "[![pypi version](https://img.shields.io/pypi/v/test-project.svg)]" in readme assert "[![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)" in readme assert "[![python](https://img.shields.io/badge/python-%3E%3D3.11-blue.svg?style=flat)](#installation)" in readme @@ -280,7 +282,9 @@ def test_template_badges_and_urls_render_for_gitlab(tmp_path: Path) -> None: repo_url = _expected_repo_url("gitlab", "testuser", "test-project") assert "[![ci]" not in readme - assert f"[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)]({docs_url}/)" in readme + assert ( + f"[![documentation](https://img.shields.io/badge/docs-properdocs-708FCC.svg?style=flat)]({docs_url}/)" in readme + ) assert "[![pypi version](https://img.shields.io/pypi/v/test-project.svg)]" in readme assert "[![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)" in readme assert "[![python](https://img.shields.io/badge/python-%3E%3D3.11-blue.svg?style=flat)](#installation)" in readme @@ -323,7 +327,9 @@ def test_template_badges_and_urls_render_for_azure(tmp_path: Path) -> None: repo_url = _expected_repo_url("azure", "testuser", "test-project") assert "[![ci]" not in readme - assert f"[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)]({docs_url}/)" in readme + assert ( + f"[![documentation](https://img.shields.io/badge/docs-properdocs-708FCC.svg?style=flat)]({docs_url}/)" in readme + ) assert "[![pypi version](https://img.shields.io/pypi/v/test-project.svg)]" in readme assert "[![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)" in readme assert "[![python](https://img.shields.io/badge/python-%3E%3D3.11-blue.svg?style=flat)](#installation)" in readme diff --git a/tests/test_template/test_cpp_template.py b/tests/test_template/test_cpp_template.py new file mode 100644 index 0000000..2a66167 --- /dev/null +++ b/tests/test_template/test_cpp_template.py @@ -0,0 +1,61 @@ +"""Tests for the bundled C++ template.""" + +from pathlib import Path + +from tests.template_testing import instantiate_template + + +def _read_text(path: Path) -> str: + """Read a generated text file using UTF-8.""" + return path.read_text(encoding="utf-8") + + +def test_cpp_template_renders_spinach_style_structure(tmp_path: Path) -> None: + """The C++ template should render the Meson/include/src/tests/examples shape.""" + template_path = Path(__file__).parent.parent.parent / "src" / "repoman" / "cpp_template" + project_dir = instantiate_template( + output_dir=tmp_path, + template_path=template_path, + project_name="sample-cpp", + copier_data={ + "ci": "github.com", + "project_description": "Sample C++ library", + "repository_provider": "github", + "repository_namespace": "testuser", + "repository_name": "sample-cpp", + "author_username": "testuser", + "author_fullname": "Test User", + "author_email": "test@example.com", + "copyright_holder": "Test User", + "copyright_holder_email": "test@example.com", + "copyright_date": "2025", + "copyright_license": "MIT", + "cpp_namespace": "sample_cpp", + "cpp_library_name": "sample_cpp", + "cpp_standard": "c++17", + "cpp_build_cli": True, + "cpp_build_examples": True, + "cpp_build_tests": True, + }, + ) + + assert (project_dir / "include" / "sample_cpp" / "sample_cpp.hpp").exists() + assert (project_dir / "include" / "sample_cpp" / "version.hpp.in").exists() + assert (project_dir / "src" / "sample_cpp.cpp").exists() + assert (project_dir / "src" / "cli.cpp").exists() + assert (project_dir / "tests" / "test_sample_cpp.cpp").exists() + assert (project_dir / "examples" / "basic.cpp").exists() + assert (project_dir / "Doxyfile").exists() + + meson = _read_text(project_dir / "meson.build") + makefile = _read_text(project_dir / "Makefile") + mkdocs = _read_text(project_dir / "config" / "mkdocs.yml") + github_ci = _read_text(project_dir / ".github" / "workflows" / "ci.yml") + + assert "subdir('include/sample_cpp')" in meson + assert "'cpp_std=c++17'" in meson + assert "make check-docs" in github_ci + assert "make check" in github_ci + assert "make test" in github_ci + assert 'site_url: "https://testuser.github.io/sample-cpp"' in mkdocs + assert "MESON ?= uv run meson" in makefile