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
19 changes: 11 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,20 @@ is about to run, write it as a hook.

- Grow `template-core` first; expose through `template-cli` and
`template-py`. Don't let them diverge on core behavior.
- The wheel exposes `template_python_rust_cmd._native` (PyO3) AND a
packaged `template-cli` binary under
`src/template_python_rust_cmd/_bin/`. `ci/build_wheel.py` enforces
both are present.
- The wheel exposes `template_python_rust_cmd._native` (PyO3) AND
ships the cargo-built `template-cli[.exe]` as a raw wheel script at
`template_python_rust_cmd-<ver>.data/scripts/`. Pip extracts that
directly into the venv's `Scripts/` / `bin/` on install — no Python
shim sits in front of the binary. `ci/build_wheel.py::verify_artifacts()`
enforces both deliverables are present in the wheel.
- When changing user-visible commands, update [README.md](./README.md),
[UPDATE.md](./UPDATE.md), [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md),
and any affected `ci/gates/<name>.py`.
- The release pipeline stages the compiled `template-cli` binary into
`src/template_python_rust_cmd/_bin/` during the build, verifies the
resulting wheel contains both native deliverables, and removes the
staged binary afterward so the worktree stays clean.
- The release pipeline builds `template-cli` via cargo into the pinned
`CARGO_TARGET_DIR`, then `ci/build_wheel.py` post-processes the
maturin wheel to inject the binary at `.data/scripts/` with a fresh
RECORD row. There is no `_bin/` staging step under the package source
tree anymore; see #7 for the rationale (Windows `os.execv` race).

## Where to ask questions

Expand Down
12 changes: 7 additions & 5 deletions ENHANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ When growing the scaffold, the load-bearing decisions are:
## Invariants to preserve

- `template-cli` and `template-py` never diverge on core behavior.
- The Python CLI shim in `src/template_python_rust_cmd/cli.py` stays
thin: it locates the packaged binary and delegates. No business
logic.
- There is no Python CLI shim. `template-cli[.exe]` ships as a raw
wheel script at `template_python_rust_cmd-<ver>.data/scripts/`; pip
drops it straight into the venv's `Scripts/` / `bin/` on install with
no Python wrapper. Adding `[project.scripts]` back would re-introduce
the Windows `os.execv` race fixed in #7 — don't.
- The wheel always contains both deliverables (PyO3 extension AND
staged native binary). `ci/build_wheel.py::verify_artifacts()`
enforces this — don't bypass it.
the raw `template-cli[.exe]` wheel script). `ci/build_wheel.py::
verify_artifacts()` enforces this — don't bypass it.
- The composite `action.yml` only references subcommands that exist
in `template-cli --help`. `ci/gates/action_surface.py` checks this.

Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ the rules and where they're implemented are also covered in
├── src/template_python_rust_cmd/
│ ├── __init__.py # package version + public imports
│ ├── _native.pyi # typing stub for the PyO3 surface
│ ├── bindings.py # Python wrapper around the extension
│ ├── cli.py # Python entry that delegates to the native binary
│ └── _bin/ # packaged native executable location (gitignored)
│ └── bindings.py # Python wrapper around the extension
│ # `template-cli[.exe]` is NOT under the package — it's injected into
│ # the wheel's <name>-<ver>.data/scripts/ directory by ci/build_wheel.py
│ # and pip drops it straight into the venv's Scripts/ (Win) or bin/
│ # (POSIX) on install. See src/template_python_rust_cmd/README.md.
├── tests/ # pytest fixtures + gate contract tests
└── docs/
├── ARCHITECTURE.md
Expand Down Expand Up @@ -91,9 +93,13 @@ gate produce noise instead of signal. See [zccache#835 rule 7](https://github.co
The wheel contains:

- the PyO3 extension module at `template_python_rust_cmd._native`, and
- the packaged native executable used by the `template-python-rust-cmd`
console script, staged into `src/template_python_rust_cmd/_bin/`
during build and removed afterward (gitignored).
- the cargo-built `template-cli[.exe]` binary at
`template_python_rust_cmd-<ver>.data/scripts/` — pip extracts this
straight into the venv's `Scripts/` (Windows) or `bin/` (POSIX)
directory on install, with no Python wrapper in front of it. See
[#7](https://github.com/zackees/template-python-rust-cmd/pull/7) for
why we avoid `[project.scripts]` (Windows `os.execv` is emulated and
races the shell prompt ahead of the child's stdout).

`./build_wheel.py` orchestrates the maturin build, verifies the wheel
contains both deliverables, and cleans up. `./publish.py` is the
Expand Down
20 changes: 13 additions & 7 deletions crates/template-cli/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# `template-cli`

The bare Rust binary shipped with the Python package. Built into
`target/release/template-cli{,.exe}`, then staged into
`src/template_python_rust_cmd/_bin/` by `ci/build_wheel.py`.
`target/release/template-cli{,.exe}` (or `$CARGO_TARGET_DIR/release/`
when the wheel-build path runs — see `ci/build_wheel.py` for the pin),
then **injected directly into the wheel** at
`template_python_rust_cmd-<ver>.data/scripts/` by
`ci/build_wheel.py::inject_cli_into_wheel()`. Pip extracts the binary
straight into the venv's `Scripts/` (Win) or `bin/` (POSIX) on install
— no Python launcher in front of it. See #7 / #2 for why.

## Responsibilities

Expand Down Expand Up @@ -31,11 +36,12 @@ you remove one, the cleanup step is the same in reverse.
## Why the binary is packaged into the wheel

A Python user doing `pip install template-python-rust-cmd` gets the
`template-cli` binary on PATH (via the entry-point script). They
don't need to install Rust, and they don't need a separate
distribution channel for the binary. The wheel is one artifact for
both deliverables; `ci/build_wheel.py` enforces that the staged
binary is present.
`template-cli` binary on PATH (the wheel ships it as a raw script in
`.data/scripts/`; pip handles the extraction). They don't need to
install Rust, and they don't need a separate distribution channel for
the binary. The wheel is one artifact for both deliverables;
`ci/build_wheel.py::verify_artifacts()` enforces that the script entry
is present alongside the PyO3 extension.

## Cross-compilation

Expand Down
12 changes: 10 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ 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`,
packaged at `template_python_rust_cmd._bin/`)
shipped as a raw wheel script at
`template_python_rust_cmd-<ver>.data/scripts/` — pip extracts it
straight into the venv's `Scripts/`/`bin/` on install, no Python
launcher; see #7 for the Windows `os.execv` race 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.
Expand Down Expand Up @@ -63,9 +66,14 @@ Five load-bearing pieces:

- package version and re-exports (`__init__.py`)
- Python wrapper around the extension (`bindings.py`)
- CLI shim that finds and execs the packaged native binary (`cli.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.

## Hook / Gate Split

| Concern | Home |
Expand Down
13 changes: 8 additions & 5 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ The canonical release sequence and the publish-script contract.
./ci.sh all
uv run python ci/build_wheel.py
```
`build_wheel.py` stages the compiled CLI binary into
`src/template_python_rust_cmd/_bin/`, drives maturin to produce
wheel + sdist, asserts both the PyO3 extension module and the
staged binary are present in the wheel, then removes the staged
binary.
`build_wheel.py` builds the CLI via cargo into the pinned
`CARGO_TARGET_DIR`, drives maturin to produce the wheel + sdist
(PyO3 extension only), then post-processes the wheel to inject the
cargo-built `template-cli[.exe]` at
`template_python_rust_cmd-<ver>.data/scripts/` with a fresh RECORD
row. `verify_artifacts()` asserts both the PyO3 extension module
and the raw `template-cli` wheel script are present. There is no
`_bin/` staging step under the package source tree — see #7.
4. Verify wheel and sdist by hand: `uv run --with twine twine check
dist/*`.
5. Set `_ENABLED = True` in `ci/publish.py` (deliberately not a CLI
Expand Down
Loading