98 lines
3.8 KiB
Python
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()}
|