Files
paperlib/paperlib_build_backend.py

166 lines
4.6 KiB
Python

"""A tiny local build backend for paperlib.
This keeps `uv run paperlib ...` working without requiring network access
to download a third-party build backend.
"""
from __future__ import annotations
import base64
import hashlib
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
PROJECT_ROOT = Path(__file__).resolve().parent
SRC_ROOT = PROJECT_ROOT / "src"
PACKAGE_ROOT = SRC_ROOT / "paperlib"
NAME = "paperlib"
VERSION = "0.1.0"
def _dist_info_dir() -> str:
return f"{NAME}-{VERSION}.dist-info"
def _wheel_name() -> str:
return f"{NAME}-{VERSION}-py3-none-any.whl"
def _metadata_contents() -> bytes:
return "\n".join(
[
"Metadata-Version: 2.1",
f"Name: {NAME}",
f"Version: {VERSION}",
"Summary: Local-first CLI toolkit for managing a paper library",
"",
]
).encode("utf-8")
def _wheel_contents() -> bytes:
return "\n".join(
[
"Wheel-Version: 1.0",
"Generator: paperlib_build_backend",
"Root-Is-Purelib: true",
"Tag: py3-none-any",
"",
]
).encode("utf-8")
def _entry_points_contents() -> bytes:
return b"[console_scripts]\npaperlib = paperlib.cli:main\n"
def _hash_bytes(data: bytes) -> tuple[str, str]:
digest = hashlib.sha256(data).digest()
encoded = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
return f"sha256={encoded}", str(len(data))
def _package_files() -> list[tuple[str, bytes]]:
files: list[tuple[str, bytes]] = []
for path in sorted(PACKAGE_ROOT.rglob("*")):
if path.is_file():
relative = path.relative_to(SRC_ROOT).as_posix()
files.append((relative, path.read_bytes()))
return files
def _build_records(files: list[tuple[str, bytes]]) -> bytes:
record_path = f"{_dist_info_dir()}/RECORD"
rows: list[list[str]] = []
for path, data in files:
digest, size = _hash_bytes(data)
rows.append([path, digest, size])
rows.append([record_path, "", ""])
output: list[str] = []
for row in rows:
output.append(",".join(row))
return ("\n".join(output) + "\n").encode("utf-8")
def _build_wheel_archive(target: Path) -> str:
files = _package_files()
files.extend(
[
(f"{_dist_info_dir()}/METADATA", _metadata_contents()),
(f"{_dist_info_dir()}/WHEEL", _wheel_contents()),
(f"{_dist_info_dir()}/entry_points.txt", _entry_points_contents()),
]
)
files.append((f"{_dist_info_dir()}/RECORD", _build_records(files)))
wheel_path = target / _wheel_name()
with ZipFile(wheel_path, "w", compression=ZIP_DEFLATED) as zf:
for archive_path, data in files:
zf.writestr(archive_path, data)
return wheel_path.name
def build_wheel(
wheel_directory: str,
config_settings: dict[str, object] | None = None,
metadata_directory: str | None = None,
) -> str:
"""Build a wheel for paperlib."""
del config_settings, metadata_directory
target = Path(wheel_directory)
target.mkdir(parents=True, exist_ok=True)
return _build_wheel_archive(target)
def build_editable(
wheel_directory: str,
config_settings: dict[str, object] | None = None,
metadata_directory: str | None = None,
) -> str:
"""Build an installable wheel for editable requests.
For now, we return a normal pure-Python wheel; that is sufficient for the
project's current bootstrap needs.
"""
return build_wheel(wheel_directory, config_settings, metadata_directory)
def get_requires_for_build_wheel(
config_settings: dict[str, object] | None = None,
) -> list[str]:
"""This backend has no external build requirements."""
del config_settings
return []
def get_requires_for_build_editable(
config_settings: dict[str, object] | None = None,
) -> list[str]:
"""This backend has no external build requirements."""
del config_settings
return []
def prepare_metadata_for_build_wheel(
metadata_directory: str,
config_settings: dict[str, object] | None = None,
) -> str:
"""Write minimal dist-info metadata for frontends that request it."""
del config_settings
dist_info = Path(metadata_directory) / _dist_info_dir()
dist_info.mkdir(parents=True, exist_ok=True)
(dist_info / "METADATA").write_bytes(_metadata_contents())
(dist_info / "WHEEL").write_bytes(_wheel_contents())
(dist_info / "entry_points.txt").write_bytes(_entry_points_contents())
(dist_info / "RECORD").write_text("", encoding="utf-8")
return dist_info.name
def prepare_metadata_for_build_editable(
metadata_directory: str,
config_settings: dict[str, object] | None = None,
) -> str:
"""Alias editable metadata generation to the wheel metadata implementation."""
return prepare_metadata_for_build_wheel(metadata_directory, config_settings)