website/design/solidity-lab/pinned_solc.py

98 lines
3.8 KiB
Python

"""Fail-closed access to the lab's pinned Solidity compiler (review A3).
The pinned solc belongs to the lab's reproducibility contract: a pinned source
commit plus this exact compiler reproduce the generated artifacts
byte-for-byte. Two failure classes must never be conflated:
- ``CompilerUnavailable`` — the tool itself could not be run (no npx, no
network, timeout). Where the suites are optional (a local checkout) this may
skip, explicitly labelled; wherever they are mandatory gates (CI, release
checks that export ``CI=1``) it is a hard error.
- ``CompileFailed`` — the tool ran and rejected the source, or wrote no
artifacts. Always a hard failure: a compiler error must never become a
green skip.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import unittest
from pathlib import Path
SOLC_VERSION = "0.8.37"
TIMEOUT_SECONDS = 300
class CompilerUnavailable(Exception):
"""The pinned compiler tool itself could not be run."""
class CompileFailed(Exception):
"""The pinned compiler ran and rejected the source or wrote no artifacts."""
def tools_mandatory() -> bool:
"""True wherever the pinned toolchain is a required gate (CI sets CI=true)."""
return bool(os.environ.get("CI"))
def _solc_invocation() -> list[str]:
npx = shutil.which("npx")
if npx is None:
raise CompilerUnavailable("npx not found on PATH")
return [npx, "--yes", f"solc@{SOLC_VERSION}"]
def _run(command: list[str], cwd: Path | None = None) -> subprocess.CompletedProcess:
try:
return subprocess.run(command, capture_output=True, text=True,
timeout=TIMEOUT_SECONDS, cwd=str(cwd) if cwd else None)
except subprocess.TimeoutExpired as error:
raise CompilerUnavailable(
f"pinned solc {SOLC_VERSION} did not answer within {TIMEOUT_SECONDS}s") from error
except OSError as error:
raise CompilerUnavailable(f"could not run the pinned compiler: {error}") from error
def probe() -> None:
"""Prove the pinned compiler runs at all, independent of any source."""
result = _run(_solc_invocation() + ["--version"])
if result.returncode != 0 or SOLC_VERSION not in result.stdout:
detail = (result.stderr or result.stdout).strip()[:200]
raise CompilerUnavailable(f"pinned solc {SOLC_VERSION} did not run: {detail}")
def enforce_availability() -> None:
"""Probe the tool and apply the availability policy.
Raises ``unittest.SkipTest`` where the suites are optional (explicit,
labelled skip) and ``AssertionError`` wherever the toolchain is a
mandatory gate (CI, release checks exporting CI=1).
"""
try:
probe()
except CompilerUnavailable as error:
if tools_mandatory():
raise AssertionError(f"pinned solc is a mandatory gate in CI but unavailable: {error}") from error
raise unittest.SkipTest(
f"pinned solc unavailable in this environment (optional locally): {error}") from error
def compile_with_artifacts(source: Path, workdir: Path, stem: str) -> dict[str, bytes]:
"""Compile ``source`` with the pinned solc into ``workdir``.
Returns ``{"bin": ..., "abi": ...}``; every failure mode raises
``CompileFailed`` so callers can never mistake it for a skip.
"""
result = _run(_solc_invocation() + ["--bin", "--abi", str(source)], cwd=workdir)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()[:400]
raise CompileFailed(f"pinned solc {SOLC_VERSION} rejected {source.name}: {detail}")
binaries = sorted(workdir.glob(f"*_{stem}.bin"))
abis = sorted(workdir.glob(f"*_{stem}.abi"))
if not binaries or not abis:
raise CompileFailed(
f"pinned solc wrote no artifacts for {stem}: {result.stdout.strip()[:200]}")
return {"bin": binaries[0].read_bytes(), "abi": abis[0].read_bytes()}