headroom/scripts/build_rust_extension.sh
chopratejas f5f465418b feat(rust): retire python diff_compressor, ship rust-only via pyo3
The python `DiffCompressor` is replaced by a thin pyo3-backed shim that
delegates to `headroom._core.DiffCompressor`. There is no python
implementation and no env-var fallback — the wheel is a hard import.

Why now: opt-in defaults don't drive python retirement. Byte-equal
parity was already proven across 27 fixtures (stage 3a); keeping a
shadow python impl behind a flag is a permanent maintenance cost with
no operational benefit. Stage 3b deletes ~700 lines of python parser /
scorer / formatter code; the rust crate has its own coverage.

Surface preserved:
- `headroom.transforms.diff_compressor.DiffCompressor` — same class
  name, same `__init__`, same `compress(content, context)` shape.
  Returns python `DiffCompressionResult` dataclasses so call sites that
  destructure with `asdict()` work unchanged.
- `DiffCompressorConfig` and `DiffCompressionResult` dataclasses kept.
- Sidecar `compress_with_stats(...)` exposes the rust-only
  `DiffCompressorStats` (per-file hunk drops, context lines trimmed,
  file_mode normalizations) for observability.

Removed:
- Python parser / scorer / formatter (~700 lines).
- Internal `DiffHunk` / `DiffFile` parser dataclasses (rust crate has
  parallel coverage).
- 12 tests that probed `_parse_diff` / `_score_hunks` or the deleted
  parser dataclasses. The 29 public-API tests in `test_diff_compressor.py`
  remain and now exercise the rust backend through the same import path.
- `HEADROOM_DIFF_COMPRESSOR_BACKEND` env var — no longer meaningful.

Build:
- `scripts/build_rust_extension.sh` runs `maturin develop` and symlinks
  the built `.so` into `headroom/` so `import headroom._core` resolves
  past the in-tree package shadowing the maturin overlay.
- `.gitignore` excludes the symlinks and allowlists the build script.

Tests:
- 544 transforms pass; 33 rust-vs-fixture parity tests guard the pyo3
  bridge against regressions (`tests/test_transforms/test_diff_compressor_rust_parity.py`).
- Mypy clean.
2026-04-26 09:15:37 -07:00

45 lines
1.7 KiB
Bash
Executable file

#!/usr/bin/env bash
# Build the Rust → Python extension (headroom._core) and link it into the
# in-tree `headroom/` package so `import headroom._core` resolves.
#
# Why a wrapper script: `maturin develop` builds the `.so` and installs it
# into the venv's site-packages, but the in-tree `headroom/` source
# directory (loaded via `pip install -e .`) shadows that on sys.path.
# Python finds `headroom/__init__.py` at the project root before reaching
# the maturin overlay, so `import headroom._core` fails. Symlinking the
# built `.so` into `headroom/` fixes the lookup with zero copies.
#
# Idempotent. Safe to run repeatedly. Requires `maturin` in PATH (i.e.
# inside the project venv).
set -euo pipefail
cd "$(dirname "$0")/.."
if ! command -v maturin >/dev/null 2>&1; then
echo "error: maturin not found. Activate the venv first:" >&2
echo " source .venv/bin/activate" >&2
exit 1
fi
# Build the wheel + install into the venv site-packages.
maturin develop -m crates/headroom-py/Cargo.toml
# Locate the built `.so`. `maturin develop` writes it under
# `crates/headroom-py/python/headroom/_core.cpython-<ver>-<platform>.so`.
SO_FILE=$(find crates/headroom-py/python/headroom -maxdepth 1 \
-name "_core.cpython-*.so" -o -name "_core.cpython-*.dylib" -o -name "_core.pyd" \
2>/dev/null | head -1)
if [[ -z "$SO_FILE" ]]; then
echo "error: maturin develop succeeded but produced no _core.* binary." >&2
exit 1
fi
# Symlink into the in-tree package dir.
LINK_NAME="headroom/$(basename "$SO_FILE")"
ln -sf "$(pwd)/$SO_FILE" "$LINK_NAME"
echo "linked: $LINK_NAME -> $SO_FILE"
# Smoke-test the import to fail loudly if anything is misconfigured.
python -c "from headroom._core import DiffCompressor; print('headroom._core OK:', DiffCompressor)"