# Copyright 2026 Alessandro Masat
# SPDX-License-Identifier: Apache-2.0
"""The `Payload` type and its (de)serialisation — everything `aether_dsc`
needs at IMPORT time, with zero third-party dependencies. Kept apart
from :mod:`aether_dsc.seal`, which needs `cuda-bindings` for its NVRTC-clean
gate: an ordinary consumer who only ever calls `payload()`/`.serve()` must
never pay for (or need installed) a CUDA binding just to unpack a blob.
"""
from __future__ import annotations
import contextlib
import hashlib
import io
import os
import shutil
import tarfile
import tempfile
import zlib
from pathlib import Path
from typing import Iterator, Mapping
__all__ = ["Payload", "PAYLOAD_DIR", "digest_of"]
#: Where a sealed blob lives once `python -m aether_dsc.seal` has written one
#: — `aether_dsc/_payload/<digest>.bin`, shipped as package data.
PAYLOAD_DIR = Path(__file__).resolve().parent / "_payload"
def digest_of(headers: Mapping[str, bytes]) -> str:
"""The payload's content digest: sha256 over the
SORTED `(name, bytes)` pairs. Sorting is what makes it order-independent
— two payloads built by walking the same files in a different order (a
different filesystem, a different `os.walk` order) still digest to the
same value, and a single renamed or altered file changes it.
"""
h = hashlib.sha256()
for name in sorted(headers):
encoded = name.encode()
h.update(len(encoded).to_bytes(4, "big"))
h.update(encoded)
data = headers[name]
h.update(len(data).to_bytes(8, "big"))
h.update(data)
return h.hexdigest()
#: The tar member :func:`_pack`/:func:`_unpack` use to carry the host-only
#: name set through the blob. Never a real header's own name — every real
#: payload entry spells ``aether/...``, ``rtc/...`` or ``plugin/...``, so a
#: bare dunder name with no such prefix cannot collide with one, now or as
#: the manifests grow.
_HOST_ONLY_MANIFEST_MEMBER = "__host_only_names__"
def _pack(headers: Mapping[str, bytes],
host_only_names: frozenset[str] = frozenset()) -> bytes:
"""Serialise `headers` (both sections — NVRTC-clean and host-only —
already merged into one mapping by the caller) to the on-disk blob
format: a zlib-compressed tar, one member per header plus one more
(:data:`_HOST_ONLY_MANIFEST_MEMBER`) naming which of those members are
host-only, newline-joined and sorted. Opacity, not secrecy, so a viewer
cannot casually `strings` or `cat` the payload."""
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
for name in sorted(headers):
data = headers[name]
info = tarfile.TarInfo(name=name)
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
if host_only_names:
data = "\n".join(sorted(host_only_names)).encode()
info = tarfile.TarInfo(name=_HOST_ONLY_MANIFEST_MEMBER)
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
return zlib.compress(buf.getvalue(), level=9)
def _unpack(blob: bytes) -> tuple[dict[str, bytes], frozenset[str]]:
"""The inverse of :func:`_pack`: ``(headers, host_only_names)`` — the
latter empty when the blob predates the host-only section (an older
payload sealed only the NVRTC-clean set)."""
raw = zlib.decompress(blob)
headers: dict[str, bytes] = {}
host_only_names: frozenset[str] = frozenset()
with tarfile.open(fileobj=io.BytesIO(raw), mode="r") as tf:
for member in tf.getmembers():
if not member.isfile():
continue
extracted = tf.extractfile(member)
data = extracted.read() if extracted else b""
if member.name == _HOST_ONLY_MANIFEST_MEMBER:
host_only_names = frozenset(
ln for ln in data.decode().splitlines() if ln)
else:
headers[member.name] = data
return headers, host_only_names
def _is_tmpfs(path: Path) -> bool:
"""Whether `path` is itself a tmpfs mount (prefer `/dev/shm` when it is
one). Read from `/proc/mounts` rather than assumed from the name —
`/dev/shm` is conventional, not guaranteed, and measurement found a box
where a private tmpfs mount is refused entirely (SELinux enforcing), so
this function only ever REPORTS what is already mounted, never mounts
anything itself."""
try:
target = str(path.resolve())
except OSError:
return False
try:
with open("/proc/mounts") as f:
for line in f:
parts = line.split()
if len(parts) >= 3 and parts[1] == target and parts[2] == "tmpfs":
return True
except OSError:
return False
return False
def _ram_dir() -> Path:
"""Where :meth:`Payload.serve` stages files: `/dev/shm` when it is a
tmpfs, else `$TMPDIR` (the measured fallback when a private mount
namespace is refused)."""
shm = Path("/dev/shm")
if shm.is_dir() and _is_tmpfs(shm):
return shm
return Path(os.environ.get("TMPDIR", "/tmp"))
[docs]
class Payload:
"""A sealed set of headers plus its content digest.
`headers` maps include-relative names (`aether/...`, `rtc/<std name>`,
`plugin/gref_layout.h`) to their bytes; `digest` is :func:`digest_of`
applied to that mapping — two `Payload` instances with the same file set and
content always carry the same digest, independent of how or in what
order their files were discovered (:mod:`aether_dsc.seal`'s own
order-independence guarantee, exercised by
`tests/test_aether_dsc.py::test_seal_digest_order_independent`).
`host_only_names` names the subset of `headers` that is the HOST-ONLY
section: sealed (so :meth:`serve` writes them out for a host `-I` root
exactly like every other entry) but never claimed NVRTC-clean and never
handed to an NVRTC compile — see :attr:`device_headers`. `digest` covers
`headers` whole, host-only entries included, so a changed host-only file
changes the digest too (the cache-validity term every compile keys on).
"""
__slots__ = ("digest", "headers", "host_only_names")
def __init__(self, digest: str, headers: Mapping[str, bytes],
host_only_names: frozenset[str] = frozenset()):
self.digest = digest
self.headers = dict(headers)
self.host_only_names = frozenset(host_only_names)
def __repr__(self) -> str: # pragma: no cover - debugging aid
return (f"Payload(digest={self.digest[:12]}…, {len(self.headers)} headers, "
f"{len(self.host_only_names)} host-only)")
@property
def device_headers(self) -> dict[str, bytes]:
"""`headers` minus the host-only section — what an NVRTC compile is
actually served (:mod:`hawk.compile.drivers`'s device path,
:mod:`hawk.compile.nvrtc`'s public ``device``/``cubin``): the
host-only headers are never NVRTC-clean-checked and must never
reach NVRTC even as an unreferenced, served-but-unused file."""
if not self.host_only_names:
return self.headers
return {k: v for k, v in self.headers.items() if k not in self.host_only_names}
[docs]
@contextlib.contextmanager
def serve(self) -> Iterator[Path]:
"""Materialise every header into a PRIVATE, owner-only directory for
the compile's lifetime, then remove it — on the normal exit path and
when the `with` block raises.
Prefers a tmpfs (`/dev/shm`, checked via `/proc/mounts` — never
assumed from the path alone) so nothing touches spinning or
networked storage; falls back to `$TMPDIR` otherwise. A private
mount namespace with its own tmpfs is an opportunistic upgrade this
function does not attempt: measurement found it refused outright on
an SELinux-enforcing box, so the plain RAM directory is the one
path every caller can rely on.
"""
base = _ram_dir()
base.mkdir(parents=True, exist_ok=True)
served = Path(tempfile.mkdtemp(prefix="aether_dsc_", dir=str(base)))
os.chmod(served, 0o700)
try:
for name, data in self.headers.items():
dest = served / name
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(data)
os.chmod(dest, 0o600)
yield served
finally:
shutil.rmtree(served, ignore_errors=True)