ffi_artifacts.py (8402B)
1 """Exact staged native artifact inventory and source-tuple verification.""" 2 3 from __future__ import annotations 4 5 import hashlib 6 import plistlib 7 import re 8 from pathlib import Path 9 from typing import Any 10 11 import ffi_provenance as provenance 12 import ffi_source as source 13 import package_contract as contract 14 15 TARGETS = ("aarch64-apple-ios", "aarch64-apple-ios-sim", "aarch64-apple-darwin") 16 MAX_ARTIFACT_BYTES = 256 * 1024 * 1024 17 FRAMEWORK = "TeraFFI.xcframework" 18 MODULE = "TeraKitBindings" 19 MANIFEST = "provenance.json" 20 21 22 def expected_paths() -> set[str]: 23 paths = { 24 "generated/TeraKitBindings.swift", 25 "generated/TeraFFI.h", 26 "generated/TeraFFI.modulemap", 27 "headers/TeraFFI.h", 28 "headers/module.modulemap", 29 f"{FRAMEWORK}/Info.plist", 30 "api/TeraKitBindings.symbols.json", 31 "abi_symbols.json", 32 } 33 for target in TARGETS: 34 extension = "dylib" if target == TARGETS[-1] else "a" 35 paths.add(f"native/{target}/libtera_ffi.{extension}") 36 paths.add(f"source/{target}.json") 37 for platform in ("ios-arm64", "ios-arm64-simulator"): 38 for relative in ( 39 "libtera_ffi.a", 40 "Headers/TeraFFI.h", 41 "Headers/module.modulemap", 42 ): 43 paths.add(f"{FRAMEWORK}/{platform}/{relative}") 44 return paths 45 46 47 def regular_path(root: Path, relative: str) -> Path: 48 path = Path(relative) 49 if path.is_absolute() or str(path) != relative or ".." in path.parts: 50 raise source.ProvenanceError("native artifact path is invalid") 51 current = root 52 for part in path.parts: 53 current = current / part 54 if current.is_symlink(): 55 raise source.ProvenanceError("native artifact path contains a symlink") 56 if not current.is_file(): 57 raise source.ProvenanceError("native artifact file is missing") 58 return current 59 60 61 def file_record(root: Path, relative: str) -> dict[str, Any]: 62 path = regular_path(root, relative) 63 size = path.stat().st_size 64 if size <= 0 or size > MAX_ARTIFACT_BYTES: 65 raise source.ProvenanceError("native artifact exceeds its byte bound") 66 digest = hashlib.sha256() 67 with path.open("rb") as handle: 68 while data := handle.read(1024 * 1024): 69 digest.update(data) 70 if path.stat().st_size != size: 71 raise source.ProvenanceError("native artifact changed during verification") 72 return {"path": relative, "bytes": size, "sha256": digest.hexdigest()} 73 74 75 def inventory(root: Path) -> list[dict[str, Any]]: 76 paths = set() 77 for path in root.rglob("*"): 78 if path.is_symlink(): 79 raise source.ProvenanceError("native artifact inventory contains a symlink") 80 if path.is_file() and path.relative_to(root).as_posix() != MANIFEST: 81 paths.add(path.relative_to(root).as_posix()) 82 if paths != expected_paths(): 83 raise source.ProvenanceError("native artifact inventory differs") 84 return [file_record(root, relative) for relative in sorted(paths)] 85 86 87 def validate_module_files(root: Path) -> None: 88 generated = root / "generated" 89 header = contract._read_regular(generated / "TeraFFI.h") 90 modulemap = contract._read_regular(generated / "TeraFFI.modulemap") 91 if b"module TeraFFI {" not in modulemap or b'header "TeraFFI.h"' not in modulemap: 92 raise source.ProvenanceError("generated native module identity differs") 93 swift = contract._read_regular(generated / "TeraKitBindings.swift") 94 if b"import TeraFFI" not in swift: 95 raise source.ProvenanceError("generated Swift module does not import its FFI") 96 for prefix in ( 97 "headers", 98 f"{FRAMEWORK}/ios-arm64/Headers", 99 f"{FRAMEWORK}/ios-arm64-simulator/Headers", 100 ): 101 if contract._read_regular(root / prefix / "TeraFFI.h") != header: 102 raise source.ProvenanceError( 103 "packaged FFI header differs from generated header" 104 ) 105 if contract._read_regular(root / prefix / "module.modulemap") != modulemap: 106 raise source.ProvenanceError( 107 "packaged FFI module map differs from generated module map" 108 ) 109 110 111 def validate_framework(root: Path) -> None: 112 info = plistlib.loads(contract._read_regular(root / FRAMEWORK / "Info.plist")) 113 libraries = info.get("AvailableLibraries", []) 114 expected = { 115 "ios-arm64": None, 116 "ios-arm64-simulator": "simulator", 117 } 118 if len(libraries) != 2 or { 119 item.get("LibraryIdentifier") for item in libraries 120 } != set(expected): 121 raise source.ProvenanceError("XCFramework platform inventory differs") 122 for item in libraries: 123 validate_platform(item, expected[item["LibraryIdentifier"]]) 124 for target, platform in zip(TARGETS[:2], expected, strict=True): 125 first = file_record(root, f"native/{target}/libtera_ffi.a") 126 packaged = file_record(root, f"{FRAMEWORK}/{platform}/libtera_ffi.a") 127 if (first["bytes"], first["sha256"]) != (packaged["bytes"], packaged["sha256"]): 128 raise source.ProvenanceError( 129 "XCFramework library differs from its built target" 130 ) 131 132 133 def validate_platform(item: dict[str, Any], variant: str | None) -> None: 134 if ( 135 item.get("SupportedArchitectures") != ["arm64"] 136 or item.get("SupportedPlatform") != "ios" 137 or item.get("SupportedPlatformVariant") != variant 138 or item.get("LibraryPath") != "libtera_ffi.a" 139 or item.get("HeadersPath") != "Headers" 140 ): 141 raise source.ProvenanceError("XCFramework library contract differs") 142 143 144 def validate_abi(root: Path) -> None: 145 symbols = contract._read_json(root / "abi_symbols.json") 146 if set(symbols) != set(TARGETS): 147 raise source.ProvenanceError("native ABI target inventory differs") 148 host = symbols[TARGETS[-1]] 149 if not isinstance(host, list) or not host or host != sorted(set(host)): 150 raise source.ProvenanceError("native ABI symbols are invalid") 151 if any(symbols[target] != host for target in TARGETS): 152 raise source.ProvenanceError("native target ABI symbols differ") 153 header = contract._read_regular(root / "generated/TeraFFI.h").decode() 154 declared = set( 155 re.findall(r"\b((?:ffi|uniffi)_tera_ffi_[A-Za-z0-9_]+)\s*\(", header) 156 ) 157 if not declared or not declared.issubset(host): 158 raise source.ProvenanceError( 159 "generated header declares an unavailable native symbol" 160 ) 161 162 163 def manifest(root: Path, records: dict[str, dict[str, Any]]) -> dict[str, Any]: 164 if set(records) != set(TARGETS): 165 raise source.ProvenanceError("producer source target inventory differs") 166 source_tree = records[TARGETS[0]]["source"]["tree"] 167 for target, record in records.items(): 168 if ( 169 record["source"]["tree"] != source_tree 170 or record["build"]["target"] != target 171 ): 172 raise source.ProvenanceError("producer source tuples disagree") 173 provenance.verify_record( 174 contract._read_regular(root / "source" / f"{target}.json"), record 175 ) 176 validate_module_files(root) 177 validate_framework(root) 178 validate_abi(root) 179 validate_api(root) 180 return { 181 "schema": "radroots.artifact-manifest.v2", 182 "product": "tera", 183 "target": "ios", 184 "language": "swift", 185 "external_names": ["TeraFFI", "TeraKitBindings"], 186 "source": { 187 "repository": records[TARGETS[0]]["repository"], 188 "tree": source_tree, 189 }, 190 "source_records": {target: f"source/{target}.json" for target in TARGETS}, 191 "files": inventory(root), 192 "disposition": "local_candidate_not_installed", 193 } 194 195 196 def validate_api(root: Path) -> None: 197 value = contract._read_json(root / "api/TeraKitBindings.symbols.json") 198 if ( 199 value.get("schema") != "radroots.swift-api-snapshot.v1" 200 or value.get("module", {}).get("name") != MODULE 201 or not isinstance(value.get("symbols"), list) 202 or not value["symbols"] 203 ): 204 raise source.ProvenanceError("generated Swift API snapshot identity differs") 205 206 207 def check(root: Path, records: dict[str, dict[str, Any]]) -> dict[str, Any]: 208 expected = manifest(root, records) 209 if contract._read_regular(root / MANIFEST) != provenance.encoded(expected): 210 raise source.ProvenanceError("native artifact provenance is stale") 211 return expected