diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8b6e50..09dd732 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,6 +134,15 @@ jobs: shell: bash run: ./ci.sh test + # Linux-only inside the gate: soldr's compile daemon cannot spawn + # on GHA macOS/Windows runners (zackees/soldr#1300); the gate + # self-skips there. + - name: backend-smoke + id: backend_smoke + continue-on-error: true + shell: bash + run: ./ci.sh backend_smoke + - name: action-yaml id: action_yaml continue-on-error: true @@ -156,6 +165,7 @@ jobs: [ "${{ steps.clippy.outcome }}" = "failure" ] && fail+=("clippy") || true [ "${{ steps.ruff.outcome }}" = "failure" ] && fail+=("ruff") || true [ "${{ steps.test.outcome }}" = "failure" ] && fail+=("test") || true + [ "${{ steps.backend_smoke.outcome }}" = "failure" ] && fail+=("backend_smoke") || true [ "${{ steps.action_yaml.outcome }}" = "failure" ] && fail+=("action_yaml") || true [ "${{ steps.action_surface.outcome }}" = "failure" ] && fail+=("action_surface") || true if [ ${#fail[@]} -gt 0 ]; then diff --git a/ci.py b/ci.py index 046ffd7..ff052d0 100755 --- a/ci.py +++ b/ci.py @@ -41,6 +41,7 @@ "ruff", "build", "test", + "backend_smoke", "action_yaml", "action_surface", ] diff --git a/ci/build_wheel.py b/ci/build_wheel.py index 8908976..08e73e4 100644 --- a/ci/build_wheel.py +++ b/ci/build_wheel.py @@ -36,13 +36,13 @@ binary as a raw wheel script bypasses the Python launcher entirely. See fbuild#747 / zackees/template-python-rust-cmd#2 (items 1 + 10). """ + from __future__ import annotations import base64 import hashlib import json import os -import shutil import platform import subprocess import sys @@ -289,9 +289,7 @@ def inject_cli_into_wheel(binary: Path) -> Path: entries: dict[str, bytes] = {name: wf.read(name) for name in wf.namelist()} if record_arcname not in entries: - raise SystemExit( - f"wheel has no {record_arcname}; cannot inject CLI script" - ) + raise SystemExit(f"wheel has no {record_arcname}; cannot inject CLI script") # Append a RECORD row for the new script. RECORD's own row keeps # empty hash + size per spec — we preserve that. @@ -311,7 +309,7 @@ def inject_cli_into_wheel(binary: Path) -> Path: # pip installs land it +x. create_system=3 = Unix. if name == script_arcname: info.create_system = 3 - info.external_attr = (0o755 << 16) + info.external_attr = 0o755 << 16 wf.writestr(info, data) return wheel_path diff --git a/ci/gates/backend_smoke.py b/ci/gates/backend_smoke.py new file mode 100644 index 0000000..3690a1e --- /dev/null +++ b/ci/gates/backend_smoke.py @@ -0,0 +1,52 @@ +"""PEP 517 backend smoke: `uv build --wheel` through the soldr backend. + +The `[build-system]` in pyproject.toml routes wheel builds through the +soldr backend (which drives `maturin pep517` under a rustc-caching +wrapper). The `test` gate deliberately avoids that path (see its +docstring), so without this gate the backend swap would ship untested. + +This gate builds one wheel via the real backend — exactly what a +downstream `pip install template-python-rust-cmd` (sdist) or +`uv build` does. + +Linux-only: soldr's compile daemon cannot spawn on GHA macOS/Windows +runners (zackees/soldr#1300) — macOS fails with "embedded compile +dispatch failed after 30000ms budget: NotRunning" and Windows wedges +the step for ~an hour. On those platforms the gate skips (returns 0) +rather than burning an hour to report a known-upstream condition. +Re-enable everywhere once soldr#1300 is fixed and the pin in +pyproject.toml is bumped past the fixed release. +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def run() -> int: + if sys.platform != "linux": + print( + "backend_smoke: skipped on non-Linux (soldr compile daemon " + "cannot spawn on GHA macOS/Windows runners — " + "zackees/soldr#1300). The backend path is exercised on the " + "Linux lanes." + ) + return 0 + if shutil.which("uv") is None: + print("uv not on PATH; cannot run backend_smoke gate", file=sys.stderr) + return 1 + proc = subprocess.run( + ["uv", "build", "--wheel", "--out-dir", "dist/backend-smoke"], + cwd=ROOT, + check=False, + ) + return proc.returncode + + +if __name__ == "__main__": + raise SystemExit(run()) diff --git a/ci/gates/ruff.py b/ci/gates/ruff.py index f172f07..f83ee34 100644 --- a/ci/gates/ruff.py +++ b/ci/gates/ruff.py @@ -5,7 +5,8 @@ points at a real issue (unused imports, shadowed names, etc.). Invoked via `uv run --no-project --with ruff` so we get a hermetic ruff -without triggering the maturin build the surrounding pyproject.toml +without triggering the wheel build (soldr PEP 517 backend driving +maturin) the surrounding pyproject.toml otherwise demands. `--with ruff` provisions the dep at script-time even when the script itself has no PEP 723 deps declared for it. diff --git a/ci/gates/test.py b/ci/gates/test.py index 9510251..eb622b1 100644 --- a/ci/gates/test.py +++ b/ci/gates/test.py @@ -1,9 +1,12 @@ """Test gate: `cargo test --workspace` + `pytest`. This is one of the named entry points that legitimately *needs* the -maturin wheel built (pytest imports `template_python_rust_cmd._native`). -We invoke it via plain `uv run` (project context kept on purpose) so -maturin develop materializes the extension module before pytest runs. +extension module built (pytest imports `template_python_rust_cmd._native`). +We sync the dev deps with `--no-install-project` and let a direct +`maturin develop` materialize the extension module before pytest runs — +deliberately NOT via the PEP 517 backend (soldr), whose compile daemon +cannot spawn on GHA macOS/Windows runners (zackees/soldr#1300). The +backend path is covered by the `backend_smoke` gate on Linux. Reserve this opt-in to the build for named entry points — see [zccache#835 rule 5](https://github.com/zackees/zccache/issues/835). Other gates use @@ -38,13 +41,28 @@ def run() -> int: if rc != 0: return rc - # `maturin develop` needs the project context, so we do NOT pass - # --no-project here. This is the documented opt-in to the full - # build (see [zccache#835] rule 5). + # Materialize the dev dependency group (maturin, pytest) WITHOUT + # installing the project itself. A plain `uv run` here would sync + # the project editable through the PEP 517 backend (soldr), and + # soldr's compile daemon cannot spawn on GHA macOS/Windows runners + # (zackees/soldr#1300): macOS dies with "embedded compile dispatch + # failed after 30000ms budget: NotRunning", Windows wedges the step + # for an hour. The extension module is instead built by the direct + # `maturin develop` call below, which does not route through the + # backend. The backend itself is exercised by the `backend_smoke` + # gate on Linux, where the daemon spawns fine. + rc = _run(["uv", "sync", "--no-install-project"]) + if rc != 0: + return rc + + # `maturin develop` builds + installs the extension module into the + # synced venv directly (no PEP 517 backend involved). `--no-sync` + # keeps uv from re-syncing (and re-triggering the backend build). rc = _run( [ "uv", "run", + "--no-sync", "maturin", "develop", "--uv", @@ -55,7 +73,7 @@ def run() -> int: if rc != 0: return rc - rc = _run(["uv", "run", "pytest"]) + rc = _run(["uv", "run", "--no-sync", "pytest"]) return rc diff --git a/ci/hooks/tool_guard.py b/ci/hooks/tool_guard.py index 5a11782..f018317 100644 --- a/ci/hooks/tool_guard.py +++ b/ci/hooks/tool_guard.py @@ -298,7 +298,8 @@ def _check_segment(seg: str) -> tuple[str, str] | None: "Use `./ci.sh ` for lint/gate invocations, or run " "your build through a named entry point (./test, ./build, " "ci/build_wheel.py). Bare `uv run` walks up to pyproject.toml " - "and triggers the maturin wheel build before your script " + "and triggers the wheel build (soldr PEP 517 backend driving " + "maturin) before your script " "starts. Add `--no-project --script` to skip discovery and " "use the PEP 723 inline-deps path.", ) diff --git a/install b/install index 417002d..2f4d668 100755 --- a/install +++ b/install @@ -10,8 +10,8 @@ Verifies the toolchain shape this repo expects: - The pinned Rust toolchain (rust-toolchain.toml) is resolvable. - The dev dependency group is materialized. -Does NOT trigger a maturin build — that's reserved for `./test` / -`./build`. Use `./install` after a fresh clone to confirm the +Does NOT trigger a wheel build (soldr PEP 517 backend / maturin) — +that's reserved for `./test` / `./build`. Use `./install` after a fresh clone to confirm the environment is sane. """ diff --git a/pyproject.toml b/pyproject.toml index 5a4e90c..af2e20b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["maturin>=1.7,<2"] -build-backend = "maturin" +requires = ["soldr>=0.7.98"] +build-backend = "soldr" [project] name = "template-python-rust-cmd" diff --git a/tests/test_cli.py b/tests/test_cli.py index 1125e3a..8b31f71 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -15,6 +15,7 @@ script) is fundamentally not visible from the source tree anymore — there's no `_bin/` directory to look at. See #9. """ + from __future__ import annotations import os