field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

ffi_provenance.py (6808B)


      1 """Generate or check staged producer evidence, separate from installed artifacts."""
      2 
      3 from __future__ import annotations
      4 
      5 import argparse
      6 import hashlib
      7 import json
      8 import os
      9 import sys
     10 import tempfile
     11 from pathlib import Path
     12 from typing import Any
     13 
     14 import ffi_source as source
     15 import package_contract as contract
     16 
     17 
     18 def capture(root: Path, target: str) -> dict[str, Any]:
     19     source.reject_build_overrides()
     20     source.reject_cargo_configuration(root)
     21     config = source.producer_contract(root)
     22     build = config["build"]
     23     if target not in build["targets"]:
     24         raise source.ProvenanceError("producer target is not governed")
     25     snapshot = source.source_snapshot(root, config["source_inputs"])
     26     rustc = source.command(root, ["rustc", "-Vv"]).decode().strip()
     27     if f"release: {build['rust_version']}\n" not in rustc + "\n":
     28         raise source.ProvenanceError(
     29             "active Rust compiler differs from producer contract"
     30         )
     31     if f"host: {build['host']}\n" not in rustc + "\n":
     32         raise source.ProvenanceError("active Rust host differs from producer contract")
     33     result = {
     34         "schema": "tera.producer-source.v1",
     35         "repository": config["repository"],
     36         "source": snapshot,
     37         "foundation": contract._read_toml(root / config["foundation_lock"]),
     38         "cargo_lock_sha256": hashlib.sha256(
     39             source.read_source(root, "Cargo.lock")
     40         ).hexdigest(),
     41         "build": {
     42             **config["ffi"],
     43             "target": target,
     44             "profile": build["profile"],
     45             "ios_deployment_target": build["ios_deployment_target"],
     46             "rust_flags": build["rust_flags"],
     47             "package_rust_flags": source.library_rust_flags(build, target),
     48             "source_date_epoch": build["source_date_epoch"],
     49             "rustc": rustc,
     50             "symbol_reader": source.command(
     51                 root, [str(symbol_reader(root, build["host"])), "--version"]
     52             )
     53             .decode()
     54             .strip(),
     55             "apple_toolchain": apple_toolchain(root),
     56             "feature_graph": source.feature_graph(
     57                 root, config["ffi"]["package"], target
     58             ),
     59         },
     60         "generator": {
     61             **config["generator"],
     62             "target": build["host"],
     63             "profile": "dev",
     64             "profile_overrides": source.allowed_profile_overrides(),
     65             "feature_graph": source.feature_graph(
     66                 root, config["generator"]["package"], build["host"]
     67             ),
     68         },
     69         "disposition": "local_staged_source_only",
     70     }
     71     if source.source_snapshot(root, config["source_inputs"]) != snapshot:
     72         raise source.ProvenanceError("producer source changed during capture")
     73     return result
     74 
     75 
     76 def symbol_reader(root: Path, host: str) -> Path:
     77     sysroot = Path(
     78         source.command(root, ["rustc", "--print", "sysroot"]).decode().strip()
     79     )
     80     reader = sysroot / "lib/rustlib" / host / "bin/llvm-nm"
     81     if not reader.is_file() or not os.access(reader, os.X_OK):
     82         raise source.ProvenanceError("Rust toolchain llvm-tools component is required")
     83     return reader
     84 
     85 
     86 def apple_toolchain(root: Path) -> dict[str, str]:
     87     commands = {
     88         "xcode": ["xcodebuild", "-version"],
     89         "swift": ["xcrun", "swiftc", "--version"],
     90         "swiftformat": ["swiftformat", "--version"],
     91         "iphoneos_sdk": ["xcrun", "--sdk", "iphoneos", "--show-sdk-build-version"],
     92         "iphonesimulator_sdk": [
     93             "xcrun",
     94             "--sdk",
     95             "iphonesimulator",
     96             "--show-sdk-build-version",
     97         ],
     98     }
     99     return {
    100         name: source.command(root, argv).decode().strip()
    101         for name, argv in commands.items()
    102     }
    103 
    104 
    105 def encoded(value: dict[str, Any]) -> bytes:
    106     return (json.dumps(value, sort_keys=True, indent=2) + "\n").encode()
    107 
    108 
    109 def verify_record(actual: bytes, expected: dict[str, Any]) -> None:
    110     if actual != encoded(expected):
    111         raise source.ProvenanceError(
    112             "producer provenance differs from the exact source/build tuple"
    113         )
    114 
    115 
    116 def output_path(root: Path, target: str, requested: str | None) -> Path:
    117     external = os.environ.get("EXT_BUILD_PROJECT_DIR")
    118     if not external or not os.environ.get("EXT_BUILD_RUN_ACTIVE"):
    119         raise source.ProvenanceError("producer provenance requires extbuild")
    120     base = Path(external).resolve()
    121     path = (
    122         Path(requested)
    123         if requested
    124         else base / "target/tera_ffi/source" / f"{target}.json"
    125     )
    126     path = path.absolute()
    127     if not path.is_relative_to(base) or path.is_relative_to(root):
    128         raise source.ProvenanceError("producer evidence must use external build output")
    129     if path.resolve() != path or path.is_symlink():
    130         raise source.ProvenanceError("producer evidence output contains a symlink")
    131     return path
    132 
    133 
    134 def write_atomic(path: Path, data: bytes) -> None:
    135     path.parent.mkdir(parents=True, exist_ok=True)
    136     temporary: str | None = None
    137     try:
    138         with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as handle:
    139             temporary = handle.name
    140             handle.write(data)
    141             handle.flush()
    142             os.fsync(handle.fileno())
    143         os.replace(temporary, path)
    144         temporary = None
    145     finally:
    146         if temporary:
    147             Path(temporary).unlink(missing_ok=True)
    148 
    149 
    150 def main() -> int:
    151     parser = argparse.ArgumentParser(description=__doc__)
    152     parser.add_argument("mode", choices=("contract-check", "write", "check"))
    153     parser.add_argument("--target")
    154     parser.add_argument(
    155         "--repo-root", type=Path, default=Path(__file__).resolve().parent.parent
    156     )
    157     parser.add_argument("--output")
    158     args = parser.parse_args()
    159     try:
    160         root = args.repo_root.resolve()
    161         if args.mode == "contract-check":
    162             source.producer_contract(root)
    163             print(
    164                 "FFI producer contract verified; installed artifacts remain separately governed"
    165             )
    166             return 0
    167         if not args.target:
    168             raise source.ProvenanceError("producer target is required")
    169         path = output_path(root, args.target, args.output)
    170         record = capture(root, args.target)
    171         if args.mode == "write":
    172             write_atomic(path, encoded(record))
    173         verify_record(contract._read_regular(path), record)
    174     except (
    175         source.ProvenanceError,
    176         contract.PackageContractError,
    177         OSError,
    178         ValueError,
    179         KeyError,
    180     ) as error:
    181         print(f"FFI producer provenance: {error}", file=sys.stderr)
    182         return 1
    183     print(
    184         f"FFI producer source {args.mode}: {args.target}; tree={record['source']['tree']}; local staged source only"
    185     )
    186     return 0
    187 
    188 
    189 if __name__ == "__main__":
    190     raise SystemExit(main())