Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
include:
- os: macos-latest
python-version: "3.12"
Expand All @@ -90,6 +90,8 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
# Needed for 3.15 while it is still pre-release on the runners.
allow-prereleases: true

- name: Install package (dev extras)
run: python -m pip install -e ".[dev]"
Expand Down
27 changes: 9 additions & 18 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Publish** workflow: GitHub Release (`vX.Y.Z`) builds sdist/wheel and uploads
to PyPI via Trusted Publishing (OIDC). Manual `workflow_dispatch` can target
TestPyPI. Release tag must match `src/cppboot/_version.py`.
- Ruff + mypy in CI; distribution build + `twine check` job.
- Dev extras: `ruff`, `mypy`, `build`, `twine`.
- Opt-out flags for default-on behavior: `--no-git`, `--no-fmt`, `--no-community-docs`
(alongside existing `--no-vim`, `--no-ctags`, `--no-vscode`, `--no-github-actions`,
`--no-codespaces`).
- Offline license mode for deterministic tests (`ProjectOptions.offline_license`).
- pytest unit and integration suite with multi-Python CI.
- Official support for **Python 3.9+** (`requires-python = ">=3.9"`).
- CI matrix covers **3.9–3.15** (3.15 via `allow-prereleases` while pre-release).

### Changed

- Package version single-sourced from `src/cppboot/_version.py` (setuptools
dynamic version).
- Split the monolithic generator into a `cppboot.generate` package (templates,
tooling, orchestration) while keeping `cppboot.generator` as a stable facade.
- Added package `LICENSE`, `CHANGELOG.md`, and `py.typed` for a PyPI-ready layout.
- Opinionated defaults are **opt-out only**: help/CLI expose `--no-vim`,
`--no-ctags`, `--no-vscode`, etc. Positive mirrors (`--vim`, `--git`, …) removed
so the CLI matches “always on unless you disable it.”

## [0.1.0] - unreleased on PyPI
## [0.1.0] - 2026-07-27

Initial public development line: CMake C++20 scaffolds, VERSION single source,
Makefile/`build.bat`, VS Code, Codespaces, GitHub Actions for generated projects.
Initial public release on PyPI: CMake C++20 scaffolds, VERSION single source,
Makefile/`build.bat`, VS Code, Codespaces, GitHub Actions for generated projects,
multi-Python CI, GitHub Release → PyPI Trusted Publishing.
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,33 @@ file (single source of truth) and CLI **`--version` / `-V`**.

### Options

Requires **Python 3.9+** (CI: 3.9–3.15; Amazon Linux’s system 3.9 is supported).

| Flag | Default | Description |
|------|---------|-------------|
| `-n`, `--name` | (prompt) | Project name |
| `--license` | `apache-2.0` | License id |
| `--build-system` | `cmake` | Only `cmake` for now |
| `--with-modules` | off | C++20 modules scaffold |
| `--shared` | off | Shared library instead of static |
| `--vim` / `--no-vim` | **on** | Project-local `.vimrc` |
| `--ctags` / `--no-ctags` | **on** | Universal Ctags `.ctags` + `make tags` |
| `--vscode` / `--no-vscode` | **on** | VS Code + `CMakePresets.json` |
| `--github-actions` / `--no-github-actions` | **on** | Multi-OS CI / sanitizers / release workflows |
| `--codespaces` / `--no-codespaces` | **on** | GitHub Codespaces / Dev Container (`.devcontainer/`) |
| `--community-docs` / `--no-community-docs` | **on** | `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md` |
| `--git` / `--no-git` | **on** | `git init` + initial commit |
| `--fmt` / `--no-fmt` | **on** | Run `make fmt` after scaffolding |
| `--github` | **off** | Create GitHub remote with `gh` (opt-in) |
| `--with-modules` | off | C++20 modules scaffold (opt-in) |
| `--shared` | off | Shared library instead of static (opt-in) |
| `--github` | off | Create GitHub remote with `gh` (opt-in) |
| `--output-dir` | `.` | Parent directory for the new project |
| `-v`, `--verbose` | off | Verbose logging |
| `--version` | | Print cppboot version |
| `-h`, `--help` | | Help |

All default-on features have matching **`--no-*`** opt-outs.
**Opinionated defaults (always on).** Disable only with `--no-*`:

| Opt-out | Skips |
|---------|--------|
| `--no-vim` | Project-local `.vimrc` |
| `--no-ctags` | Universal Ctags `.ctags` + `make tags` |
| `--no-vscode` | VS Code + `CMakePresets.json` |
| `--no-github-actions` | CI / sanitizers / release workflows |
| `--no-codespaces` | Codespaces / Dev Container |
| `--no-community-docs` | CoC / CONTRIBUTING / SECURITY |
| `--no-git` | `git init` + initial commit |
| `--no-fmt` | `make fmt` after scaffolding |

## What you get

Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "cppboot"
dynamic = ["version"]
description = "Bootstrap a professional-grade C++ project environment"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.9"
license = { file = "LICENSE" }
authors = [{ name = "cppboot contributors" }]
keywords = ["c++", "cmake", "bootstrap", "scaffold", "project-generator"]
Expand All @@ -18,10 +18,13 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Code Generators",
"Typing :: Typed",
Expand Down Expand Up @@ -71,7 +74,7 @@ source = ["cppboot"]
branch = true

[tool.ruff]
target-version = "py310"
target-version = "py39"
line-length = 100
src = ["src", "tests"]

Expand Down Expand Up @@ -99,6 +102,7 @@ quote-style = "double"
indent-style = "space"

[tool.mypy]
# Type-check against 3.10+ (current mypy floor). Runtime support includes 3.9 (see CI).
python_version = "3.10"
mypy_path = "src"
packages = ["cppboot"]
Expand Down
123 changes: 65 additions & 58 deletions src/cppboot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@
from cppboot.licenses import DEFAULT_LICENSE, LICENSE_CHOICES


def _add_opt_out(
parser: argparse.ArgumentParser,
*,
flag: str,
dest: str,
help_text: str,
) -> None:
"""Register a default-on feature that is disabled only via ``--no-*``."""
parser.add_argument(
flag,
action="store_false",
dest=dest,
default=True,
help=help_text,
)


def build_parser() -> argparse.ArgumentParser:
"""Create the argument parser."""
parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -51,43 +68,54 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Build the library as a shared library (default: static).",
)
parser.add_argument(
"--vim",
action=argparse.BooleanOptionalAction,
default=True,
help="Write a minimal project-local .vimrc (default: on). Use --no-vim to skip.",
)
parser.add_argument(
"--ctags",
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Write Universal Ctags config and make tags target (default: on). "
"Use --no-ctags to skip."
),
)
parser.add_argument(
"--vscode",
action=argparse.BooleanOptionalAction,
default=True,
help=("Write VS Code config + CMakePresets (default: on). Use --no-vscode to skip."),
)
parser.add_argument(
"--github-actions",
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Write cross-platform GitHub Actions CI (default: on). Use --no-github-actions to skip."
),
)
parser.add_argument(
"--codespaces",
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Write GitHub Codespaces / Dev Container config (default: on). "
"Use --no-codespaces to skip."
),
# Opinionated defaults: always on unless the user passes the matching --no-* flag.
_add_opt_out(
parser,
flag="--no-vim",
dest="vim",
help_text="Do not write a project-local .vimrc.",
)
_add_opt_out(
parser,
flag="--no-ctags",
dest="ctags",
help_text="Do not write Universal Ctags config / make tags target.",
)
_add_opt_out(
parser,
flag="--no-vscode",
dest="vscode",
help_text="Do not write VS Code config + CMakePresets.",
)
_add_opt_out(
parser,
flag="--no-github-actions",
dest="github_actions",
help_text="Do not write GitHub Actions CI / sanitizers / release workflows.",
)
_add_opt_out(
parser,
flag="--no-codespaces",
dest="codespaces",
help_text="Do not write GitHub Codespaces / Dev Container config.",
)
_add_opt_out(
parser,
flag="--no-git",
dest="git",
help_text="Do not run git init or create an initial commit.",
)
_add_opt_out(
parser,
flag="--no-fmt",
dest="fmt",
help_text="Do not run make fmt after scaffolding.",
)
_add_opt_out(
parser,
flag="--no-community-docs",
dest="community_docs",
help_text="Do not write CODE_OF_CONDUCT.md, CONTRIBUTING.md, or SECURITY.md.",
)
parser.add_argument(
"--github",
Expand All @@ -98,27 +126,6 @@ def build_parser() -> argparse.ArgumentParser:
"(default: off; opt-in only)."
),
)
parser.add_argument(
"--git",
action=argparse.BooleanOptionalAction,
default=True,
help=("Run git init and create an initial commit (default: on). Use --no-git to skip."),
)
parser.add_argument(
"--fmt",
action=argparse.BooleanOptionalAction,
default=True,
help=("Run make fmt after scaffolding (default: on). Use --no-fmt to skip."),
)
parser.add_argument(
"--community-docs",
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Write CODE_OF_CONDUCT.md, CONTRIBUTING.md, and SECURITY.md "
"(default: on). Use --no-community-docs to skip."
),
)
parser.add_argument(
"-v",
"--verbose",
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,38 @@ def test_parser_opt_outs() -> None:
assert args.community_docs is False


def test_help_lists_opt_outs_not_positive_toggles() -> None:
"""Opinionated features are default-on; only --no-* should appear in help."""
parser = build_parser()
option_strings = {opt for action in parser._actions for opt in action.option_strings}
for opt_out in (
"--no-vim",
"--no-ctags",
"--no-vscode",
"--no-github-actions",
"--no-codespaces",
"--no-git",
"--no-fmt",
"--no-community-docs",
):
assert opt_out in option_strings
# Positive mirrors must not exist (defaults are always on).
for positive in ("--vim", "--ctags", "--vscode", "--git", "--fmt", "--community-docs"):
assert positive not in option_strings
# Opt-in flags remain.
assert "--github" in option_strings
assert "--with-modules" in option_strings


def test_positive_toggle_flags_are_rejected() -> None:
with pytest.raises(SystemExit):
build_parser().parse_args(["-n", "demo", "--vim"])
with pytest.raises(SystemExit):
build_parser().parse_args(["-n", "demo", "--fmt"])
# Note: argparse may treat "--git" as an abbreviation of "--github" when
# allow_abbrev is true; the explicit option is only "--no-git".


def test_parser_opt_in_features() -> None:
args = build_parser().parse_args(["-n", "demo", "--with-modules", "--shared", "--github"])
assert args.with_modules is True
Expand Down
Loading