Files
2026-07-13 12:12:21 +08:00

200 lines
8.6 KiB
Python

"""
hatch_build.py - Hatchling build hook for llmfit.
Injects the pre-built llmfit binary into each wheel via ``shared_scripts`` so
that the installer places it in the environment's scripts directory (e.g.
``.venv/bin/llmfit``). Also overrides the wheel platform tag so that wheels
built on different CI runners get the correct platform-specific name.
For editable installs (``uv sync``, ``uv run``), the locally compiled debug
binary (from ``cargo build``) is used. For release installs (``uv build``),
the release binary (from ``cargo build --release``) is used.
Release builds also support setting ``LLMFIT_PYTHON_PLATFORM_TAG``, in which
case the release binary for the corresponding Rust target is used instead
(e.g. after ``cargo build --release --target aarch64-unknown-linux-gnu``).
Environment variables
---------------------
LLMFIT_PYTHON_PLATFORM_TAG
Wheel platform tag to target (e.g. ``manylinux_2_17_x86_64``).
Not supported for editable installs.
LLMFIT_VERSION
Override the version read from ``Cargo.toml`` (e.g. ``0.9.8``).
Supported for both release and editable installs.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from pathlib import Path
import tomli
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
from hatchling.metadata.plugin.interface import MetadataHookInterface
from packaging.tags import sys_tags
# wheel_platform_tag -> (upstream_target, binary_name)
TARGET_CONFIGS: dict[str, tuple[str, str]] = {
"manylinux_2_17_x86_64": ("x86_64-unknown-linux-gnu", "llmfit"),
"manylinux_2_17_aarch64": ("aarch64-unknown-linux-gnu", "llmfit"),
"manylinux_2_39_riscv64": ("riscv64gc-unknown-linux-gnu", "llmfit"),
"musllinux_1_2_x86_64": ("x86_64-unknown-linux-musl", "llmfit"),
"musllinux_1_2_aarch64": ("aarch64-unknown-linux-musl", "llmfit"),
"macosx_10_12_x86_64": ("x86_64-apple-darwin", "llmfit"),
"macosx_11_0_arm64": ("aarch64-apple-darwin", "llmfit"),
"win_amd64": ("x86_64-pc-windows-msvc", "llmfit.exe"),
"win_arm64": ("aarch64-pc-windows-msvc", "llmfit.exe"),
}
class LlmfitMetadataHook(MetadataHookInterface):
"""Hatchling metadata hook that sets version and license-expression dynamically."""
PLUGIN_NAME = "llmfit version and license"
def update(self, metadata: dict) -> None:
"""Populate ``version`` and ``license-expression`` from ``Cargo.toml``.
Version resolution order:
1. ``LLMFIT_VERSION`` environment variable (e.g. ``0.9.8``).
2. The ``version`` field in ``[workspace.package]`` from ``Cargo.toml``.
"""
with (Path(self.root).parent / "Cargo.toml").open("rb") as f:
workspace_package: dict[str, str] = tomli.load(f)["workspace"]["package"]
version: str = os.environ.get("LLMFIT_VERSION") or workspace_package["version"]
if not re.match(r"^\d+\.\d+\.\d+$", version):
raise ValueError(f"Invalid version: {version!r}")
metadata["version"] = version
metadata["license-expression"] = workspace_package["license"]
class LlmfitBinaryBuildHook(BuildHookInterface):
"""Hatchling build hook that injects the llmfit binary into each wheel."""
PLUGIN_NAME = "llmfit binary"
@staticmethod
def _detect_platform() -> str:
"""Return the best platform tag for the current machine."""
best = next((t.platform for t in sys_tags() if t.platform in TARGET_CONFIGS), None)
if best is not None:
return best
first = next(t.platform for t in sys_tags())
raise RuntimeError(f"No suitable wheel platform found for runtime platform {first!r}.")
@staticmethod
def _find_binary_for_target(llmfit_root: Path, py_target: str) -> Path:
"""Find the binary compiled for a specific Rust target.
Looks in ``target/{upstream_target}/release/``, which is where Cargo
places the binary when built with ``--target``.
"""
upstream_target, binary_name = TARGET_CONFIGS[py_target]
bin_path = llmfit_root / "target" / upstream_target / "release" / binary_name
if not bin_path.is_file():
raise FileNotFoundError(
f"Binary not found at {bin_path}. Expected it to be built for target {upstream_target!r}.",
)
return bin_path
@staticmethod
def _find_local_binary(llmfit_root: Path) -> Path:
"""Find the locally compiled binary in default Cargo output locations.
Checks ``target/debug/`` first (from ``make build``), then
``target/release/`` (from ``make release``).
"""
binary_name = "llmfit.exe" if sys.platform == "win32" else "llmfit"
candidates = [
llmfit_root / "target" / "debug" / binary_name,
llmfit_root / "target" / "release" / binary_name,
]
for candidate in candidates:
if candidate.is_file():
return candidate
raise FileNotFoundError(
"No compiled binary found. Checked:\n"
+ "\n".join(f" {c}" for c in candidates)
+ "\nRun 'make build' first.",
)
@staticmethod
def _check_binary_version(bin_path: Path, expected_version: str) -> None:
"""Run the binary with ``--version`` and verify it matches the expected version.
Raises ``RuntimeError`` on a mismatch — this indicates a stale build.
"""
result = subprocess.run(
[str(bin_path), "--version"],
capture_output=True,
check=True,
text=True,
timeout=5,
)
output = result.stdout.strip() # e.g. "llmfit 0.9.8"
match = re.match(r"^llmfit v?(\d+\.\d+\.\d+)$", output)
if not match:
raise RuntimeError(f"Unexpected output from '{bin_path} --version': {output!r}")
binary_version = match.group(1)
if binary_version != expected_version:
raise RuntimeError(
f"Binary version mismatch: binary at {bin_path} reports {binary_version!r} "
f"but Cargo.toml (or LLMFIT_VERSION) says {expected_version!r}. "
"Run 'make build' to recompile.",
)
print(f" Binary version OK ({binary_version})")
def initialize(self, version: str, build_data: dict) -> None:
"""Locate the platform binary and configure the wheel before it is built."""
py_target_from_env = os.environ.get("LLMFIT_PYTHON_PLATFORM_TAG")
if version == "editable" and py_target_from_env:
raise ValueError(
"LLMFIT_PYTHON_PLATFORM_TAG is not supported for editable installs. "
"Let the build system detect the host platform instead.",
)
running_platform = self._detect_platform()
py_target = py_target_from_env or running_platform
if py_target not in TARGET_CONFIGS:
raise ValueError(
f"Unknown LLMFIT_PYTHON_PLATFORM_TAG={py_target!r}. Must be one of: {sorted(TARGET_CONFIGS)}",
)
upstream_target, binary_name = TARGET_CONFIGS[py_target]
pypi_version: str = self.metadata.version
print(f" target={upstream_target} version={pypi_version} wheel tag=py3-none-{py_target}")
llmfit_root = Path(self.root).parent
if version == "editable":
# For editable installs, look for target/debug/llmfit or target/release/llmfit (or llmfit.exe on Windows).
bin_path = self._find_local_binary(llmfit_root)
elif version == "standard":
# For release installs, look for e.g. target/x86_64-unknown-linux-gnu/release/llmfit on Linux.
bin_path = self._find_binary_for_target(llmfit_root, py_target)
else:
raise ValueError(f"Unknown version: {version!r}")
# Always check that the binary exists.
if not bin_path.is_file():
raise FileNotFoundError(
f"Binary not found at {bin_path}. The binary selection logic selected a file that does not exist.",
)
# If possible, check the self-reported version of the binary.
# If the binary was built for a different platform then it's not possible.
if py_target == running_platform:
self._check_binary_version(bin_path, pypi_version)
# Place the binary in the wheel's scripts directory so that the
# installer puts it in .venv/bin/ (or Scripts/ on Windows).
build_data["shared_scripts"][str(bin_path.absolute())] = binary_name
# Override the platform tag so the wheel gets the correct platform-specific name.
build_data["tag"] = f"py3-none-{py_target}"
build_data["pure_python"] = False