Ship one Python wheel that exposes:
- a Python import surface backed by
PyO3(template_python_rust_cmd._native) - a command surface backed by a compiled Rust executable (
template-cli, shipped as a raw wheel script attemplate_python_rust_cmd-<ver>.data/scripts/— pip extracts it straight into the venv'sScripts//bin/on install, no Python launcher; see #7 for the Windowsos.execvrace that motivated this)
A consumer installs a single distribution and gets both deliverables. A maintainer maintains one workspace and one CI matrix to feed both.
This template implements the canonical Rust+Python CI shape from
zackees/zccache#835.
Five load-bearing pieces:
./ci.sh+ci.py. Bash wrapper + PEP 723 dispatcher. Every gate invocation goes through here so the--no-project --scriptflag combo (which suppresses the maturin auto-build trap) lives in one place.ci/gates/. Workspace-state checks. Each file exposesdef run() -> int. Canonical ordering inci.py::GATE_ORDER.ci/hooks/. Agent-intent guards wired through.claude/settings.json. Only fires during Claude/Codex sessions..github/workflows/ci.yml. Thin orchestration — one runner per platform, every step is./ci.sh <gate>. Finalreport-failuresstep collects step outcomes.action.yml+action/cleanup/action.yml. Composite action contract. Validated byci/gates/action_yaml.py(structural) +ci/gates/action_surface.py(runtime binary surface match).
- pure Rust domain logic
- no Python-specific concerns
- reusable from both the binary and the bindings crate
- the unit of behavior consistency between CLI and Python
- argument parsing
- command execution
- stdout/stderr policy
- exit-code policy
- thin translation layer over
template-core - subcommand surface is part of the composite-action contract —
ci/gates/action_surface.pyverifiesaction.yml's shell snippets only reference subcommands actually present in--help
PyO3module definitions (#[pymodule],#[pyfunction])- Python-friendly value conversion
- GIL boundary management
- thin translation layer over
template-core
- package version and re-exports (
__init__.py) - Python wrapper around the extension (
bindings.py) - typing stub for the PyO3 surface (
_native.pyi)
The template-cli binary is intentionally NOT under the package — it
ships as a raw wheel script (see Goal above). A Python caller that
wants to invoke the CLI from code should do
subprocess.run([shutil.which("template-cli"), ...]), not import
anything from this package.
| Concern | Home |
|---|---|
| Repo-state checks (every CI cycle) | ci/gates/*.py |
| Agent-intent checks (Claude sessions) | ci/hooks/*.py |
| File size budget (workspace-wide) | ci/gates/loc.py |
| File size budget (per-edit) | ci/hooks/loc_guard.py |
| README presence + size | ci/hooks/readme_guard.py |
Bare cargo / unsafe uv run shape |
ci/hooks/tool_guard.py |
- duplicating core logic in Python
- embedding CLI-only behavior inside the
PyO3module without an API use case - maintaining separate CI infrastructure for the Python and Rust sides
- inline shell logic in
.github/workflows/ci.yml - gates that hard-couple to the agent's session (those are hooks)