diff --git a/scripts/channel_adapters/build_host_macos.py b/scripts/channel_adapters/build_host_macos.py index 0f2d0762e..20f57c1c6 100644 --- a/scripts/channel_adapters/build_host_macos.py +++ b/scripts/channel_adapters/build_host_macos.py @@ -185,8 +185,10 @@ def build_macos(build_lane: LaneConfig, request_path: Path) -> None: outputs = req["expected_outputs"] root_wheel = outputs["root_wheel"] core_wheel = outputs["core_wheel"] + speakers_analyze_wheel = outputs["speakers_analyze_wheel"] root_record = outputs["root_record"] core_record = outputs["core_record"] + speakers_analyze_record = outputs["speakers_analyze_record"] bundle = Path(sb["path"]) out_dir = Path(req["paths"]["output_dir"]) @@ -265,7 +267,14 @@ echo CHECKOUT_OK detail=(build.stderr or build.stdout or ""), ) - expected_files = [root_wheel, core_wheel, root_record, core_record] + expected_files = [ + root_wheel, + core_wheel, + speakers_analyze_wheel, + root_record, + core_record, + speakers_analyze_record, + ] quoted_expected_files = " ".join(shlex.quote(name) for name in expected_files) listing = ssh_run( build_lane, @@ -306,8 +315,8 @@ echo DIST_OK "bundle_bytes": sb["bytes"], }, "tool_evidence": tool_evidence, - "macos_wheels": [root_wheel, core_wheel], - "native_records": [root_record, core_record], + "macos_wheels": [root_wheel, core_wheel, speakers_analyze_wheel], + "native_records": [root_record, core_record, speakers_analyze_record], } write_json(resp_path, response) diff --git a/scripts/check_release_preflight.py b/scripts/check_release_preflight.py index 8ed181484..3f5d8a5ab 100644 --- a/scripts/check_release_preflight.py +++ b/scripts/check_release_preflight.py @@ -480,11 +480,11 @@ def finalize_macos_tool_evidence( for record in native_records if isinstance(record, Mapping) } - if set(roles) != {"root", "core"}: + if set(roles) != {"root", "core", "speakers-analyze"}: failures.append( Failure( - error="macOS signed tool finalizer requires both native records", - expected="root and core native records", + error="macOS signed tool finalizer requires all native records", + expected="root, core, and speakers-analyze native records", actual=", ".join(sorted(roles)) or "", repair="bash scripts/release.sh --candidate", ) diff --git a/scripts/check_rust_release_manifest.py b/scripts/check_rust_release_manifest.py index 1d0681944..32ef0272b 100644 --- a/scripts/check_rust_release_manifest.py +++ b/scripts/check_rust_release_manifest.py @@ -34,6 +34,8 @@ for _path in (str(ROOT), str(_SCRIPTS_DIR)): from check_wheel_contents import ( # noqa: E402 CORE_SCRIPT_NAMES, ROOT_LAUNCHER_NAMES, + SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR, + SPEAKERS_ANALYZE_SCRIPT_NAMES, release_artifacts, ) @@ -1277,7 +1279,9 @@ def _case_collision_failures(paths: Sequence[Path]) -> list[Failure]: return failures -def _model_name_failures(package_names: set[str], expected_count: int) -> list[Failure]: +def _model_name_failures( + package_names: set[str], expected_without_models_count: int +) -> list[Failure]: model_like = { name for name in package_names if name.startswith("solstone_journal_models-") } @@ -1301,11 +1305,11 @@ def _model_name_failures(package_names: set[str], expected_count: int) -> list[F repair="use the models version derived from package metadata", ) ) - if expected_count == 15 and model_like: + if len(package_names) == expected_without_models_count and model_like: failures.append( _failure( - "15-file candidate contains models archive leftover", - expected="no solstone_journal_models archives in a 15-file candidate", + "models-skipped candidate contains models archive leftover", + expected="no solstone_journal_models archives in a models-skipped candidate", actual=", ".join(sorted(model_like)), repair="remove skipped models artifacts from the release candidate", ) @@ -1343,11 +1347,18 @@ def classify_release_dir( failures.extend(_case_collision_failures(entries)) for entry in entries: failures.extend(_validate_regular_file(entry, label=entry.name)) - if len(entries) not in {15, 17}: + expected_without_models = set(expected_package_names(include_models=False)) + expected_with_models = set(expected_package_names(include_models=True)) + expected_without_models_count = len(expected_without_models) + 4 + expected_with_models_count = len(expected_with_models) + 4 + if len(entries) not in {expected_without_models_count, expected_with_models_count}: failures.append( _failure( - "release directory must contain exactly 15 or 17 files", - expected="15 files without models or 17 files with models", + "release directory must contain exactly the expected file count", + expected=( + f"{expected_without_models_count} files without models or " + f"{expected_with_models_count} files with models" + ), actual=str(len(entries)), repair="validate the exact release candidate payload directory", ) @@ -1365,13 +1376,11 @@ def classify_release_dir( repair="generate one companion manifest for each solstone_core artifact", ) ) - expected_without_models = set(expected_package_names(include_models=False)) - expected_with_models = set(expected_package_names(include_models=True)) - include_models = len(entries) == 17 + include_models = len(entries) == expected_with_models_count expected_packages = ( expected_with_models if include_models else expected_without_models ) - failures.extend(_model_name_failures(package_names, len(entries))) + failures.extend(_model_name_failures(package_names, len(expected_without_models))) unknown = package_names - expected_with_models if unknown: failures.append( @@ -1399,7 +1408,7 @@ def classify_release_dir( "release directory contains extra assets", expected=", ".join(sorted(expected_packages)), actual=", ".join(sorted(extra)), - repair="remove assets outside the exact 15/17-file release payload", + repair="remove assets outside the exact release payload", ) ) try: @@ -2011,6 +2020,10 @@ def write_inert_packages(dist_dir: Path, *, include_models: bool) -> None: version = name.removesuffix(".whl").split("-")[1] return f"solstone-{version}.data/scripts" + def speakers_analyze_data_prefix(name: str) -> str: + version = name.removesuffix(".whl").split("-")[1] + return f"solstone_core_speakers_analyze-{version}.data/scripts" + def record_hash(content: bytes) -> str: digest = hashlib.sha256(content).digest() encoded = base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=") @@ -2081,6 +2094,29 @@ def write_inert_packages(dist_dir: Path, *, include_models: bool) -> None: members, ) continue + if name.startswith("solstone_core_speakers_analyze-") and name.endswith(".whl"): + with zipfile.ZipFile(path, "w") as wheel: + meta_name, metadata = metadata_member(name) + members = {meta_name: metadata.encode("utf-8")} + wheel.writestr(meta_name, metadata) + for script_name in SPEAKERS_ANALYZE_SCRIPT_NAMES: + info = zipfile.ZipInfo( + f"{speakers_analyze_data_prefix(name)}/{script_name}" + ) + info.create_system = 3 + info.external_attr = 0o755 << 16 + content = f"inert {script_name} for {name}\n".encode("utf-8") + wheel.writestr(info, content) + members[info.filename] = content + write_record( + wheel, + ( + "solstone_core_speakers_analyze-" + f"{name.removesuffix('.whl').split('-')[1]}.dist-info" + ), + members, + ) + continue if name.startswith("solstone-") and name.endswith(".whl"): with zipfile.ZipFile(path, "w") as wheel: meta_name, metadata = metadata_member(name) @@ -2330,6 +2366,9 @@ def run_fixtures_mode() -> list[Failure]: INSTALL_SCRIPT_NAMES, PROOF_TARGETS, SCRUBBED_COMMAND_ENV, + SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS, + SPEAKERS_ANALYZE_RESPONSE_SCHEMA, + SPEAKERS_ANALYZE_SCRIPT_NAME, CommandResult, InstallObservation, _expected_install_members, @@ -2379,6 +2418,14 @@ def run_fixtures_mode() -> list[Failure]: for name in expected_package_names(include_models=False) if name.startswith("solstone_core-") and "macosx_14_0_arm64" in name ) + fixture_speakers_analyze_wheel = next( + name + for name in expected_package_names(include_models=False) + if name.startswith("solstone_core_speakers_analyze-") + and "macosx_14_0_arm64" in name + ) + fixture_speakers_analyze_executable = next(iter(SPEAKERS_ANALYZE_SCRIPT_NAMES)) + fixture_speakers_analyze_dylib = "libonnxruntime.1.25.0.dylib" native_records = [ { "role": "root", @@ -2439,6 +2486,50 @@ def run_fixtures_mode() -> list[Failure]: }, "notarization_status": "accepted", }, + { + "role": "speakers-analyze", + "wheel": { + "name": fixture_speakers_analyze_wheel, + "sha256": "a" * 64, + "bytes": 12, + }, + "member": { + "path": ( + "solstone_core_speakers_analyze-1.0.0.data/scripts/" + f"{fixture_speakers_analyze_executable}" + ), + "sha256": "b" * 64, + "bytes": 6, + }, + "members": { + fixture_speakers_analyze_executable: { + "path": ( + "solstone_core_speakers_analyze-1.0.0.data/scripts/" + f"{fixture_speakers_analyze_executable}" + ), + "sha256": "b" * 64, + "bytes": 6, + }, + fixture_speakers_analyze_dylib: { + "path": ( + "solstone_core_speakers_analyze-1.0.0.data/" + f"{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/" + f"{fixture_speakers_analyze_dylib}" + ), + "sha256": "c" * 64, + "bytes": 6, + }, + }, + "tools": fixture_native_tools("macos-arm64"), + "signing_mode": "signed-verified", + "signing": { + "signer_pinned": True, + "team_pinned": True, + "hardened_runtime": True, + "trusted_timestamp": True, + }, + "notarization_status": "accepted", + }, ] tool_evidence = {lane: fixture_lane_tool_evidence(lane) for lane in LANES} evidence_root = root / "target" / "release-evidence" @@ -2496,6 +2587,25 @@ def run_fixtures_mode() -> list[Failure]: } for name, expected in sorted(expected_members.items()) ] + smoke = { + name: CommandResult( + argv=(str(env_root / "bin" / name), "--version"), + exit_code=0, + stdout=f"{CORE_SMOKE_STDOUT[name]} {_current_version()}", + env=SCRUBBED_COMMAND_ENV, + ) + for name in INSTALL_SCRIPT_NAMES + } + if target in SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS: + smoke[SPEAKERS_ANALYZE_SCRIPT_NAME] = CommandResult( + argv=(str(env_root / "bin" / SPEAKERS_ANALYZE_SCRIPT_NAME),), + exit_code=0, + stdout=json.dumps( + {"schema": SPEAKERS_ANALYZE_RESPONSE_SCHEMA}, + separators=(",", ":"), + ), + env=SCRUBBED_COMMAND_ENV, + ) proof = build_install_proof( target=target, version=_current_version(), @@ -2527,15 +2637,7 @@ def run_fixtures_mode() -> list[Failure]: install_paths ), installed_members=tuple(installed_members), - smoke={ - name: CommandResult( - argv=(str(env_root / "bin" / name), "--version"), - exit_code=0, - stdout=f"{CORE_SMOKE_STDOUT[name]} {_current_version()}", - env=SCRUBBED_COMMAND_ENV, - ) - for name in INSTALL_SCRIPT_NAMES - }, + smoke=smoke, ), recorded_at=datetime(2026, 7, 20, 12, 30, tzinfo=UTC), ) diff --git a/scripts/check_wheel_contents.py b/scripts/check_wheel_contents.py index 34eb74f7c..e3c07ac7d 100644 --- a/scripts/check_wheel_contents.py +++ b/scripts/check_wheel_contents.py @@ -16,14 +16,25 @@ import tarfile import tomllib import zipfile from pathlib import Path -from typing import Literal +from typing import Literal, Sequence ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) +from scripts.stage_speakers_analyze_runtime import ( + NOTICE_INSTALL_DIR as SPEAKERS_ANALYZE_NOTICE_INSTALL_DIR, +) +from scripts.stage_speakers_analyze_runtime import ( + RUNTIME_INSTALL_DIR as SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR, +) +from scripts.stage_speakers_analyze_runtime import ( + TARGETS as SPEAKERS_ANALYZE_TARGETS, +) from solstone.think.probe import ( SOLSTONE_CORE_COVERED_PLATFORMS, SOLSTONE_CORE_PLATFORM_TAGS, + SOLSTONE_CORE_SPEAKERS_ANALYZE_COVERED_PLATFORMS, + SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS, CorePlatform, current_solstone_core_platform, is_solstone_core_covered_platform, @@ -44,11 +55,13 @@ EXPECTED_MODEL_SHA256 = { MAX_BASE_WHEEL_BYTES = 4 * 1024 * 1024 MAX_BASE_PLATFORM_WHEEL_BYTES = 6 * 1024 * 1024 MAX_CORE_WHEEL_BYTES = 30 * 1024 * 1024 +MAX_SPEAKERS_ANALYZE_WHEEL_BYTES = 30 * 1024 * 1024 PARAKEET_HELPER_MEMBER = ( "solstone/observe/transcribe/parakeet_helper/_bin/parakeet-helper" ) ROOT_LAUNCHER_NAMES = ("sol", "solstone") CORE_SCRIPT_NAMES = ("solstone-core",) +SPEAKERS_ANALYZE_SCRIPT_NAMES = ("solstone-core-speakers-analyze",) ELF_MAGIC = b"\x7fELF" ELF_CLASS_64 = 2 ELF_DATA_LITTLE_ENDIAN = 1 @@ -58,8 +71,13 @@ ELF_MACHINE = { } PT_DYNAMIC = 2 PT_INTERP = 3 +PT_LOAD = 1 DT_NULL = 0 DT_NEEDED = 1 +DT_STRTAB = 5 +DT_STRSZ = 10 +DT_RPATH = 15 +DT_RUNPATH = 29 MH_MAGIC_64 = 0xFEEDFACF FAT_MAGIC = 0xCAFEBABE FAT_CIGAM = 0xBEBAFECA @@ -146,6 +164,19 @@ CORE_REQUIRED_SDIST_MEMBERS = { CORE_TAG_PLATFORMS = { tag: platform for platform, tag in SOLSTONE_CORE_PLATFORM_TAGS.items() } +SPEAKERS_ANALYZE_TAG_PLATFORMS = { + tag: platform + for platform, tag in SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS.items() +} +SPEAKERS_ANALYZE_PLATFORM_TARGETS = { + ("linux", "x86_64"): "linux-x86_64", + ("linux", "aarch64"): "linux-aarch64", + ("darwin", "arm64"): "macos-arm64", +} +SPEAKERS_ANALYZE_RUNPATH = "$ORIGIN/../lib/solstone-core-speakers-analyze" +SPEAKERS_ANALYZE_FORBIDDEN_PROVIDER_RE = re.compile( + r"providers_(?:cuda|tensorrt|shared)", re.IGNORECASE +) def _is_base_wheel(path: Path) -> bool: @@ -160,6 +191,12 @@ def _is_core_wheel(path: Path) -> bool: return path.name.startswith("solstone_core-") and path.name.endswith(".whl") +def _is_speakers_analyze_wheel(path: Path) -> bool: + return path.name.startswith( + "solstone_core_speakers_analyze-" + ) and path.name.endswith(".whl") + + def _is_core_sdist(path: Path) -> bool: return path.name.startswith("solstone_core-") and path.name.endswith(".tar.gz") @@ -187,6 +224,14 @@ def _core_wheel_tag(path: Path) -> str: return stem.split("-")[-1] +def _wheel_version_from_name(path: Path, distribution: str) -> str: + stem = path.name.removesuffix(".whl") + prefix = f"{distribution}-" + if not stem.startswith(prefix): + raise ValueError(f"{path.name}: expected {distribution} wheel") + return stem.removeprefix(prefix).split("-", 1)[0] + + def _parse_core_platform(value: str) -> CorePlatform: try: system, machine = value.split("/", 1) @@ -417,6 +462,137 @@ def _check_elf_dynamic_entries( return errors +def _read_elf_c_string(content: bytes, offset: int) -> str | None: + if offset < 0 or offset >= len(content): + return None + end = content.find(b"\0", offset) + if end < 0: + return None + try: + return content[offset:end].decode("utf-8") + except UnicodeDecodeError: + return None + + +def _elf_vaddr_to_offset( + vaddr: int, load_segments: Sequence[tuple[int, int, int]] +) -> int | None: + for segment_vaddr, segment_offset, segment_size in load_segments: + if segment_vaddr <= vaddr < segment_vaddr + segment_size: + return segment_offset + (vaddr - segment_vaddr) + return None + + +def _elf_dynamic_strings(content: bytes) -> tuple[list[str], str | None, str | None]: + if len(content) < 64 or content[:4] != ELF_MAGIC: + return [], None, None + phoff = struct.unpack_from(" len(content): + return [], None, None + + load_segments: list[tuple[int, int, int]] = [] + dynamic_offset: int | None = None + dynamic_size = 0 + for index in range(phnum): + offset = phoff + phentsize * index + p_type = struct.unpack_from(" bool: + if len(content) < 64 or content[:4] != ELF_MAGIC: + return False + phoff = struct.unpack_from(" len(content): + return False + for index in range(phnum): + offset = phoff + phentsize * index + if struct.unpack_from(" tuple[int, ...] | None: + versions = [] + for match in re.finditer(rb"GLIBC_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?", content): + versions.append(tuple(int(part) for part in match.groups(default=b"0"))) + return max(versions) if versions else None + + +def _format_version(version: tuple[int, ...] | None) -> str: + if version is None: + return "" + parts = list(version) + while len(parts) > 2 and parts[-1] == 0: + parts.pop() + return ".".join(str(part) for part in parts) + + +def _declared_manylinux_floor(tag: str) -> tuple[int, int] | None: + match = re.fullmatch(r"manylinux_(?P[0-9]+)_(?P[0-9]+)_.+", tag) + if match is None: + return None + return (int(match.group("major")), int(match.group("minor"))) + + def _check_elf_binary( wheel_name: str, content: bytes, @@ -811,6 +987,308 @@ def check_core_wheel(path: Path, max_bytes: int) -> list[str]: return errors +def _speakers_analyze_expected_members( + path: Path, platform_tuple: CorePlatform +) -> tuple[set[str], str, str]: + version = _wheel_version_from_name(path, "solstone_core_speakers_analyze") + data_prefix = f"solstone_core_speakers_analyze-{version}.data" + dist_info_prefix = f"solstone_core_speakers_analyze-{version}.dist-info" + target_key = SPEAKERS_ANALYZE_PLATFORM_TARGETS[platform_tuple] + spec = SPEAKERS_ANALYZE_TARGETS[target_key] + binary_member = f"{data_prefix}/scripts/{SPEAKERS_ANALYZE_SCRIPT_NAMES[0]}" + library_member = ( + f"{data_prefix}/{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/" + f"{spec.runtime_staged_name}" + ) + notice_members = { + f"{data_prefix}/{SPEAKERS_ANALYZE_NOTICE_INSTALL_DIR.as_posix()}/" + f"{notice.staged_name}" + for notice in spec.notices + } + return ( + { + binary_member, + library_member, + *notice_members, + f"{dist_info_prefix}/METADATA", + f"{dist_info_prefix}/WHEEL", + f"{dist_info_prefix}/RECORD", + f"{dist_info_prefix}/sboms/solstone-core-speakers-analyze.cyclonedx.json", + }, + binary_member, + library_member, + ) + + +def _check_speakers_analyze_elf_binary( + wheel_name: str, + content: bytes, + library_content: bytes, + platform_tuple: CorePlatform, + tag: str, +) -> list[str]: + errors: list[str] = [] + repair = "make wheel-speakers-analyze-linux-x86_64" + machine_name = platform_tuple[1] + expected_machine = ELF_MACHINE[machine_name] + if len(content) < 64 or content[:4] != ELF_MAGIC: + return [ + _failure( + wheel_name, + "speakers analyze binary is not ELF64", + expected="ELF64 helper binary", + actual=content[:4].hex(), + repair=repair, + ) + ] + actual_machine = struct.unpack_from("", + repair=repair, + ) + ) + if runpath != SPEAKERS_ANALYZE_RUNPATH: + errors.append( + _failure( + wheel_name, + "speakers analyze ELF RUNPATH is wrong", + expected=SPEAKERS_ANALYZE_RUNPATH, + actual=runpath or "", + repair=repair, + ) + ) + if rpath is not None: + errors.append( + _failure( + wheel_name, + "speakers analyze ELF uses legacy RPATH", + expected="DT_RUNPATH only", + actual=rpath, + repair=repair, + ) + ) + + declared = _declared_manylinux_floor(tag) + binary_glibc = _max_glibc_version(content) + library_glibc = _max_glibc_version(library_content) + measured = max( + (version for version in (binary_glibc, library_glibc) if version is not None), + default=None, + ) + if declared is None: + errors.append( + _failure( + wheel_name, + "speakers analyze wheel tag does not declare a manylinux floor", + expected="manylinux_N_M platform tag", + actual=tag, + repair=repair, + ) + ) + elif measured is not None and declared < (measured[0], measured[1]): + errors.append( + _failure( + wheel_name, + "speakers analyze wheel tag understates GLIBC floor", + expected=f"declared floor >= measured GLIBC_{_format_version(measured)}", + actual=f"{tag} declares glibc {declared[0]}.{declared[1]}", + repair=repair, + ) + ) + return errors + + +def check_speakers_analyze_wheel(path: Path) -> list[str]: + errors: list[str] = [] + size = path.stat().st_size + if size > MAX_SPEAKERS_ANALYZE_WHEEL_BYTES: + errors.append( + _failure( + path.name, + "speakers analyze wheel is too large", + expected=f"<= {MAX_SPEAKERS_ANALYZE_WHEEL_BYTES} bytes", + actual=str(size), + repair="make wheel-speakers-analyze-linux-x86_64", + ) + ) + tag = _core_wheel_tag(path) + platform_tuple = SPEAKERS_ANALYZE_TAG_PLATFORMS.get(tag) + if platform_tuple is None: + errors.append( + _failure( + path.name, + "unsupported speakers analyze wheel tag", + expected=", ".join( + sorted(SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS.values()) + ), + actual=tag, + repair="make wheel-speakers-analyze-linux-x86_64", + ) + ) + return errors + + expected_members, binary_member, library_member = ( + _speakers_analyze_expected_members(path, platform_tuple) + ) + target_key = SPEAKERS_ANALYZE_PLATFORM_TARGETS[platform_tuple] + spec = SPEAKERS_ANALYZE_TARGETS[target_key] + with zipfile.ZipFile(path) as wheel: + names = set(wheel.namelist()) + if names != expected_members: + errors.append( + _failure( + path.name, + "speakers analyze wheel member set is wrong", + expected=", ".join(sorted(expected_members)), + actual=", ".join(sorted(names)) or "", + repair="make wheel-speakers-analyze-linux-x86_64", + ) + ) + provider_members = sorted( + name + for name in names + if SPEAKERS_ANALYZE_FORBIDDEN_PROVIDER_RE.search(Path(name).name) + ) + if provider_members: + errors.append( + _failure( + path.name, + "speakers analyze wheel contains unproven provider library", + expected="no providers_cuda, providers_tensorrt, or providers_shared libraries", + actual=", ".join(provider_members), + repair="python3 scripts/stage_speakers_analyze_runtime.py", + ) + ) + binary_infos = [ + info for info in wheel.infolist() if info.filename == binary_member + ] + if len(binary_infos) != 1: + errors.append( + _failure( + path.name, + "speakers analyze binary member count is wrong", + expected=f"exactly one {binary_member}", + actual=str(len(binary_infos)), + repair="make wheel-speakers-analyze-linux-x86_64", + ) + ) + else: + mode = (binary_infos[0].external_attr >> 16) & 0o777 + if mode & 0o111 == 0: + errors.append( + _failure( + path.name, + "speakers analyze binary is not executable", + expected="executable mode bit set", + actual=oct(mode), + repair="make wheel-speakers-analyze-linux-x86_64", + ) + ) + + try: + library_content = wheel.read(library_member) + except KeyError: + library_content = b"" + actual_library_sha = hashlib.sha256(library_content).hexdigest() + if actual_library_sha != spec.runtime_sha256: + errors.append( + _failure( + path.name, + "speakers analyze ONNX Runtime library digest mismatch", + expected=spec.runtime_sha256, + actual=actual_library_sha, + repair="python3 scripts/stage_speakers_analyze_runtime.py", + ) + ) + for notice in spec.notices: + notice_member = ( + f"solstone_core_speakers_analyze-{_wheel_version_from_name(path, 'solstone_core_speakers_analyze')}.data/" + f"{SPEAKERS_ANALYZE_NOTICE_INSTALL_DIR.as_posix()}/{notice.staged_name}" + ) + try: + notice_content = wheel.read(notice_member) + except KeyError: + notice_content = b"" + actual_notice_sha = hashlib.sha256(notice_content).hexdigest() + if actual_notice_sha != notice.sha256: + errors.append( + _failure( + path.name, + f"speakers analyze notice digest mismatch for {notice.staged_name}", + expected=notice.sha256, + actual=actual_notice_sha, + repair="python3 scripts/stage_speakers_analyze_runtime.py", + ) + ) + if binary_infos: + binary_content = wheel.read(binary_member) + if platform_tuple[0] == "linux": + errors.extend( + _check_speakers_analyze_elf_binary( + f"{path.name}:{binary_member}", + binary_content, + library_content, + platform_tuple, + tag, + ) + ) + else: + errors.extend( + _check_macho_binary( + f"{path.name}:{binary_member}", + binary_content, + platform_tuple, + binary_label="solstone-core-speakers-analyze", + ) + ) + errors.extend( + _check_macho_binary( + f"{path.name}:{library_member}", + library_content, + platform_tuple, + binary_label="libonnxruntime.1.25.0.dylib", + ) + ) + errors.extend(_check_record(path, wheel)) + return errors + + def check_core_sdist(path: Path) -> list[str]: errors: list[str] = [] with tarfile.open(path, "r:gz") as archive: @@ -844,6 +1322,18 @@ def _core_platforms_for_scope(scope: ReleaseScope) -> tuple[CorePlatform, ...]: ) +def _speakers_analyze_platforms_for_scope( + scope: ReleaseScope, +) -> tuple[CorePlatform, ...]: + if scope == "all-hosts": + return SOLSTONE_CORE_SPEAKERS_ANALYZE_COVERED_PLATFORMS + return tuple( + platform_tuple + for platform_tuple in SOLSTONE_CORE_SPEAKERS_ANALYZE_COVERED_PLATFORMS + if platform_tuple[0] == "linux" + ) + + def _release_artifact_members( dist_dir: Path, *, @@ -875,6 +1365,16 @@ def _release_artifact_members( f"core wheel for {platform_tuple[0]}/{platform_tuple[1]}", ) ) + for platform_tuple in _speakers_analyze_platforms_for_scope(release_scope): + tag = SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS[platform_tuple] + artifacts.append( + ( + dist_dir + / f"solstone_core_speakers_analyze-{version}-py3-none-{tag}.whl", + "speakers analyze helper wheel for " + f"{platform_tuple[0]}/{platform_tuple[1]}", + ) + ) if release_scope == "all-hosts": for platform_tuple in SOLSTONE_CORE_COVERED_PLATFORMS: if platform_tuple[0] != "darwin": @@ -966,14 +1466,25 @@ def check_dist( base_wheels = [path for path in wheels if _is_base_wheel(path)] models_wheels = [path for path in wheels if _is_models_wheel(path)] core_wheels = [path for path in wheels if _is_core_wheel(path)] + speakers_analyze_wheels = [ + path for path in wheels if _is_speakers_analyze_wheel(path) + ] core_sdists = sorted( path for path in dist_dir.glob("*.tar.gz") if _is_core_sdist(path) ) + helper_only_dist = ( + speakers_analyze_wheels + and not base_wheels + and not models_wheels + and not core_wheels + and not core_sdists + and release_scope is None + ) - if not base_wheels: + if not helper_only_dist and not base_wheels: errors.append(f"{dist_dir}: no solstone base wheel found") require_models_wheel = release_scope is None or models_decision == "publish" - if require_models_wheel and not models_wheels: + if not helper_only_dist and require_models_wheel and not models_wheels: errors.append(f"{dist_dir}: no solstone_journal_models wheel found") system, machine = current_solstone_core_platform() required_tags: dict[str, str] = {} @@ -986,12 +1497,14 @@ def check_dist( f"{platform_tuple[0]}/{platform_tuple[1]}" ) found_core_tags = {_core_wheel_tag(path) for path in core_wheels} - for tag, platform_name in sorted(required_tags.items()): - if tag not in found_core_tags: - errors.append( - f"{dist_dir}: no solstone_core wheel found for {platform_name} ({tag})" - ) - if not core_sdists: + if not helper_only_dist: + for tag, platform_name in sorted(required_tags.items()): + if tag not in found_core_tags: + errors.append( + f"{dist_dir}: no solstone_core wheel found for " + f"{platform_name} ({tag})" + ) + if not helper_only_dist and not core_sdists: errors.append(f"{dist_dir}: no solstone_core sdist found") for path in base_wheels: @@ -1000,6 +1513,8 @@ def check_dist( errors.extend(check_models_wheel(path, expected)) for path in core_wheels: errors.extend(check_core_wheel(path, MAX_CORE_WHEEL_BYTES)) + for path in speakers_analyze_wheels: + errors.extend(check_speakers_analyze_wheel(path)) for path in core_sdists: errors.extend(check_core_sdist(path)) if release_scope is not None: diff --git a/scripts/record_macos_native_wheel.py b/scripts/record_macos_native_wheel.py index 45db92641..3813da370 100644 --- a/scripts/record_macos_native_wheel.py +++ b/scripts/record_macos_native_wheel.py @@ -24,6 +24,9 @@ from scripts.check_rust_release_manifest import ( from scripts.check_wheel_contents import ( CORE_SCRIPT_NAMES, PARAKEET_HELPER_MEMBER, + SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR, + SPEAKERS_ANALYZE_SCRIPT_NAMES, + SPEAKERS_ANALYZE_TARGETS, core_wheel_script_members, ) from scripts.release_digest import file_sha256_size @@ -38,7 +41,7 @@ from scripts.release_tool_pins import ( tool_value_matches_pin, ) -NativeRole = Literal["root", "core"] +NativeRole = Literal["root", "core", "speakers-analyze"] KIND = "macos-native-record/v1" TARGET = { @@ -90,23 +93,61 @@ def _members_for_role( info for info in wheel.infolist() if info.filename == PARAKEET_HELPER_MEMBER ] return {"parakeet-helper": helpers[0]} if len(helpers) == 1 else None - scripts = core_wheel_script_members(wheel) - names = {Path(info.filename).name for info in scripts} - if len(scripts) != len(CORE_SCRIPT_NAMES) or names != set(CORE_SCRIPT_NAMES): + if role == "core": + scripts = core_wheel_script_members(wheel) + names = {Path(info.filename).name for info in scripts} + if len(scripts) != len(CORE_SCRIPT_NAMES) or names != set(CORE_SCRIPT_NAMES): + return None + return {Path(info.filename).name: info for info in scripts} + + script_members = [ + info + for info in wheel.infolist() + if info.filename.endswith(f".data/scripts/{SPEAKERS_ANALYZE_SCRIPT_NAMES[0]}") + ] + dylib_name = SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name + dylib_suffix = ( + f".data/{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/{dylib_name}" + ) + dylib_members = [ + info for info in wheel.infolist() if info.filename.endswith(dylib_suffix) + ] + if len(script_members) != 1 or len(dylib_members) != 1: return None - return {Path(info.filename).name: info for info in scripts} + return { + SPEAKERS_ANALYZE_SCRIPT_NAMES[0]: script_members[0], + dylib_name: dylib_members[0], + } def _expected_member_path(role: NativeRole) -> str: if role == "root": return PARAKEET_HELPER_MEMBER - return ", ".join(f".data/scripts/{name}" for name in CORE_SCRIPT_NAMES) + if role == "core": + return ", ".join(f".data/scripts/{name}" for name in CORE_SCRIPT_NAMES) + dylib_name = SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name + return ( + f".data/scripts/{SPEAKERS_ANALYZE_SCRIPT_NAMES[0]} and " + f".data/{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/{dylib_name}" + ) def _role_matches_wheel(role: NativeRole, wheel_name: str) -> bool: if role == "root": return wheel_name.startswith("solstone-") and wheel_name.endswith(".whl") - return wheel_name.startswith("solstone_core-") and wheel_name.endswith(".whl") + if role == "core": + return wheel_name.startswith("solstone_core-") and wheel_name.endswith(".whl") + return wheel_name.startswith( + "solstone_core_speakers_analyze-" + ) and wheel_name.endswith(".whl") + + +def _primary_member_name(role: NativeRole) -> str: + if role == "root": + return "parakeet-helper" + if role == "core": + return "solstone-core" + return SPEAKERS_ANALYZE_SCRIPT_NAMES[0] def _read_members( @@ -255,7 +296,7 @@ def _facts_by_member( if set(signing_facts) != {"members"}: return {}, [ _failure( - "macOS core signing facts key set is wrong", + f"macOS {role} signing facts key set is wrong", expected="members", actual=", ".join(sorted(str(key) for key in signing_facts)) or "", @@ -266,29 +307,37 @@ def _facts_by_member( if not isinstance(members, Mapping): return {}, [ _failure( - "macOS core signing facts are missing members", - expected="members object keyed by solstone-core", + f"macOS {role} signing facts are missing members", + expected=f"members object keyed by {_expected_member_path(role)}", actual=type(members).__name__, repair="python3 scripts/check_rust_release_manifest.py", ) ] - if set(members) != set(CORE_SCRIPT_NAMES): + expected_names = ( + set(CORE_SCRIPT_NAMES) + if role == "core" + else { + SPEAKERS_ANALYZE_SCRIPT_NAMES[0], + SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name, + } + ) + if set(members) != expected_names: return {}, [ _failure( - "macOS core signing facts member set is wrong", - expected=", ".join(CORE_SCRIPT_NAMES), + f"macOS {role} signing facts member set is wrong", + expected=", ".join(sorted(expected_names)), actual=", ".join(sorted(str(key) for key in members)) or "", repair="python3 scripts/check_rust_release_manifest.py", ) ] failures: list[Failure] = [] normalized: dict[str, Mapping[str, Any]] = {} - for name in sorted(CORE_SCRIPT_NAMES): + for name in sorted(expected_names): facts = members.get(name) if not isinstance(facts, Mapping): failures.append( _failure( - f"macOS core signing facts for {name} are invalid", + f"macOS {role} signing facts for {name} are invalid", expected="JSON object", actual=type(facts).__name__, repair="python3 scripts/check_rust_release_manifest.py", @@ -376,8 +425,8 @@ def build_macos_native_record( if len(tool_payloads) != 1: failures.append( _failure( - "macOS signing tool facts differ across core members", - expected="identical signing tool facts for every core member", + f"macOS signing tool facts differ across {role} members", + expected=f"identical signing tool facts for every {role} member", actual=str(len(tool_payloads)), repair="python3 scripts/check_rust_release_manifest.py", ) @@ -386,7 +435,7 @@ def build_macos_native_record( raise NativeRecordError(failures) wheel_sha256, wheel_bytes = file_sha256_size(wheel_path) - primary_name = "parakeet-helper" if role == "root" else "solstone-core" + primary_name = _primary_member_name(role) primary_facts = facts_by_member[primary_name] tools = primary_facts["tools"] record: dict[str, Any] = { @@ -530,7 +579,7 @@ def validate_macos_native_record( name: _member_entry(member_path, member_bytes) for name, (member_path, member_bytes) in wheel_members.items() } - primary_name = "parakeet-helper" if role == "root" else "solstone-core" + primary_name = _primary_member_name(role) expected_member = expected_members[primary_name] if record.get("member") != expected_member: failures.append( @@ -613,7 +662,9 @@ def write_macos_native_record( def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() - parser.add_argument("--role", choices=("root", "core"), required=True) + parser.add_argument( + "--role", choices=("root", "core", "speakers-analyze"), required=True + ) parser.add_argument("--wheel", type=Path, required=True) parser.add_argument("--signing-facts", type=Path, required=True) parser.add_argument("--out", type=Path, required=True) diff --git a/scripts/release_build_host.py b/scripts/release_build_host.py index 7eb0c60ea..d34d2c30d 100644 --- a/scripts/release_build_host.py +++ b/scripts/release_build_host.py @@ -34,7 +34,10 @@ FileCopier = Callable[[Path, Path], object] MACOS_ROOT_RECORD = "macos-native-root.json" MACOS_CORE_RECORD = "macos-native-core.json" -EXPECTED_NATIVE_RECORDS = frozenset((MACOS_ROOT_RECORD, MACOS_CORE_RECORD)) +MACOS_SPEAKERS_ANALYZE_RECORD = "macos-native-speakers-analyze.json" +EXPECTED_NATIVE_RECORDS = frozenset( + (MACOS_ROOT_RECORD, MACOS_CORE_RECORD, MACOS_SPEAKERS_ANALYZE_RECORD) +) REQUEST_KEYS = frozenset( ( "schema_version", @@ -48,7 +51,14 @@ REQUEST_KEYS = frozenset( ) REQUEST_BUNDLE_KEYS = frozenset(("path", "source_commit", "sha256", "bytes")) REQUEST_OUTPUT_KEYS = frozenset( - ("root_wheel", "core_wheel", "root_record", "core_record") + ( + "root_wheel", + "core_wheel", + "speakers_analyze_wheel", + "root_record", + "core_record", + "speakers_analyze_record", + ) ) REQUEST_PATH_KEYS = frozenset(("response", "output_dir")) RESPONSE_KEYS = frozenset( @@ -493,15 +503,19 @@ def _validate_source_bundle( def _wheel_role(name: str) -> str | None: - expected_root, expected_core = _expected_macos_wheel_names() + expected_root, expected_core, expected_speakers_analyze = ( + _expected_macos_wheel_names() + ) if name == expected_core: return "core" if name == expected_root: return "root" + if name == expected_speakers_analyze: + return "speakers-analyze" return None -def _expected_macos_wheel_names() -> tuple[str, str]: +def _expected_macos_wheel_names() -> tuple[str, str, str]: expected_wheels = expected_package_names(include_models=False) expected_root = next( item @@ -513,11 +527,19 @@ def _expected_macos_wheel_names() -> tuple[str, str]: for item in expected_wheels if item.startswith("solstone_core-") and "macosx_14_0_arm64" in item ) - return expected_root, expected_core + expected_speakers_analyze = next( + item + for item in expected_wheels + if item.startswith("solstone_core_speakers_analyze-") + and "macosx_14_0_arm64" in item + ) + return expected_root, expected_core, expected_speakers_analyze def _expected_release_version() -> str: - expected_root, _expected_core = _expected_macos_wheel_names() + expected_root, _expected_core, _expected_speakers_analyze = ( + _expected_macos_wheel_names() + ) return expected_root.removeprefix("solstone-").split("-", 1)[0] @@ -698,7 +720,7 @@ def _names_from_payload( failures.append( _failure( "build-host returned unexpected macOS wheel", - expected="root and core macOS arm64 wheels", + expected="root, core, and speakers-analyze macOS arm64 wheels", actual=safe, repair="bash scripts/release.sh --candidate", ) @@ -708,7 +730,9 @@ def _names_from_payload( failures.append( _failure( "build-host returned duplicate macOS wheel role", - expected="one root wheel and one core wheel", + expected=( + "one root wheel, one core wheel, and one speakers-analyze wheel" + ), actual=role, repair="bash scripts/release.sh --candidate", ) @@ -717,11 +741,13 @@ def _names_from_payload( safe_records = tuple( safe for value in raw_records if (safe := _safe_basename(value)) is not None ) - if set(wheel_by_role) != {"root", "core"}: + if set(wheel_by_role) != {"root", "core", "speakers-analyze"}: failures.append( _failure( "build-host returned wrong macOS wheel set", - expected="one root wheel and one core wheel", + expected=( + "one root wheel, one core wheel, and one speakers-analyze wheel" + ), actual=", ".join(sorted(wheel_by_role)) or "", repair="bash scripts/release.sh --candidate", ) @@ -738,8 +764,12 @@ def _names_from_payload( if failures: raise BuildHostError(failures) return ( - (wheel_by_role["root"], wheel_by_role["core"]), - (MACOS_ROOT_RECORD, MACOS_CORE_RECORD), + ( + wheel_by_role["root"], + wheel_by_role["core"], + wheel_by_role["speakers-analyze"], + ), + (MACOS_ROOT_RECORD, MACOS_CORE_RECORD, MACOS_SPEAKERS_ANALYZE_RECORD), ) @@ -888,7 +918,7 @@ class ExternalBuildHostChannel: source_bundle: SourceBundle, expected_commit: str, ) -> dict[str, object]: - root_wheel, core_wheel = _expected_macos_wheel_names() + root_wheel, core_wheel, speakers_analyze_wheel = _expected_macos_wheel_names() payload: dict[str, object] = { "schema_version": 1, "cohort_id": cohort_id, @@ -903,8 +933,10 @@ class ExternalBuildHostChannel: "expected_outputs": { "root_wheel": root_wheel, "core_wheel": core_wheel, + "speakers_analyze_wheel": speakers_analyze_wheel, "root_record": MACOS_ROOT_RECORD, "core_record": MACOS_CORE_RECORD, + "speakers_analyze_record": MACOS_SPEAKERS_ANALYZE_RECORD, }, "paths": { "response": "response.json", diff --git a/scripts/release_candidate_driver.py b/scripts/release_candidate_driver.py index ff762603e..7c752b4d9 100644 --- a/scripts/release_candidate_driver.py +++ b/scripts/release_candidate_driver.py @@ -105,9 +105,23 @@ CORE_X86_64_MATURIN_ARGS = ( CORE_AARCH64_MATURIN_ARGS = ( "--locked --zig --compatibility manylinux2014 --target aarch64-unknown-linux-musl" ) +# The GLIBC_2.34 host-build floor was measured in prep, not anticipated. These +# helper lanes must stay on zig GNU targets so a manylinux_2_27 tag remains true. +# `--auditwheel skip` preserves the data/lib RUNPATH layout, so maturin no +# longer enforces the floor; check_wheel_contents' criterion-14 ELF floor check +# is the guard against a regressed host GNU build being mislabeled. +SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS = ( + "--locked --zig --compatibility manylinux_2_27 --auditwheel skip " + "--target x86_64-unknown-linux-gnu" +) +SPEAKERS_ANALYZE_AARCH64_MATURIN_ARGS = ( + "--locked --zig --compatibility manylinux_2_27 --auditwheel skip " + "--target aarch64-unknown-linux-gnu" +) ROOT_WORKSPACE_PACKAGE = "solstone" MODELS_WORKSPACE_PACKAGE = "solstone-journal-models" CORE_WORKSPACE_PACKAGE = "solstone-core" +SPEAKERS_ANALYZE_WORKSPACE_PACKAGE = "solstone-core-speakers-analyze" RESERVED_CANDIDATE_DIRNAME = "release-candidate" DistPreflightOperation = Literal["cleanup", "inventory"] @@ -580,6 +594,7 @@ def _default_clean_outputs(root: Path, version: str) -> None: "solstone-journal", "solstone-journal-cuda", "solstone-journal-models", + SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, ): package_dir = root / "packages" / package egg_infos, glob_failures = _owned_glob( @@ -594,6 +609,7 @@ def _default_clean_outputs(root: Path, version: str) -> None: Path("target") / "release-transfer" / version, _source_bundle_staging_path(root, version).relative_to(root), _zig_cache_root(root).relative_to(root), + Path("packages") / SPEAKERS_ANALYZE_WORKSPACE_PACKAGE / "wheel-data", ): failures.extend(_remove_owned_relative(root, relative)) if verdict.reserved_state == "directory": @@ -648,6 +664,46 @@ def validate_linux_maturin_args(args: str, *, target: str) -> list[Failure]: return [] +def _speakers_analyze_linux_maturin_tokens(target: str) -> tuple[str, ...]: + return ( + "--locked", + "--zig", + "--compatibility", + "manylinux_2_27", + "--auditwheel", + "skip", + "--target", + target, + ) + + +def validate_speakers_analyze_linux_maturin_args( + args: str, *, target: str +) -> list[Failure]: + try: + tokens = tuple(shlex.split(args)) + except ValueError as exc: + return [ + _failure( + "speakers analyze Linux maturin arguments are not parseable", + expected="exact speakers analyze Linux maturin token contract", + actual=str(exc), + repair="bash scripts/release.sh --candidate", + ) + ] + expected = _speakers_analyze_linux_maturin_tokens(target) + if tokens != expected: + return [ + _failure( + "speakers analyze Linux maturin arguments do not match release contract", + expected=" ".join(expected), + actual=" ".join(tokens), + repair="bash scripts/release.sh --candidate", + ) + ] + return [] + + def _create_zig_cache_dirs(root: Path) -> tuple[Path, Path]: cache_root = _zig_cache_root(root) cache_root_label = cache_root.relative_to(root).as_posix() @@ -712,7 +768,7 @@ def _expected_local_build_commands( package_builds = tuple( (("uv", "build", "--package", package), CORE_X86_64_MATURIN_ARGS) for package in _expected_local_build_packages(include_models=include_models) - if package != CORE_WORKSPACE_PACKAGE + if package not in {CORE_WORKSPACE_PACKAGE, SPEAKERS_ANALYZE_WORKSPACE_PACKAGE} ) core_sdist = ( ("uv", "build", "--package", CORE_WORKSPACE_PACKAGE, "--sdist"), @@ -727,12 +783,54 @@ def _expected_local_build_commands( ("uv", "build", core_sdist_path, "--wheel", "--out-dir", "dist"), CORE_AARCH64_MATURIN_ARGS, ) + x86_64_helper_stage = ( + ( + "python3", + "scripts/stage_speakers_analyze_runtime.py", + "--target", + "linux-x86_64", + ), + "", + ) + x86_64_helper = ( + ( + "uv", + "build", + "--package", + SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, + "--wheel", + ), + SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS, + ) + aarch64_helper_stage = ( + ( + "python3", + "scripts/stage_speakers_analyze_runtime.py", + "--target", + "linux-aarch64", + ), + "", + ) + aarch64_helper = ( + ( + "uv", + "build", + "--package", + SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, + "--wheel", + ), + SPEAKERS_ANALYZE_AARCH64_MATURIN_ARGS, + ) return ( render_check, *package_builds, core_sdist, x86_64_core, aarch64_core, + x86_64_helper_stage, + x86_64_helper, + aarch64_helper_stage, + aarch64_helper, ) @@ -746,9 +844,17 @@ def _default_build_local_dist( (CORE_X86_64_MATURIN_ARGS, "x86_64-unknown-linux-musl"), (CORE_AARCH64_MATURIN_ARGS, "aarch64-unknown-linux-musl"), ) + speakers_analyze_contracts = ( + (SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS, "x86_64-unknown-linux-gnu"), + (SPEAKERS_ANALYZE_AARCH64_MATURIN_ARGS, "aarch64-unknown-linux-gnu"), + ) failures: list[Failure] = [] for args, target in linux_contracts: failures.extend(validate_linux_maturin_args(args, target=target)) + for args, target in speakers_analyze_contracts: + failures.extend( + validate_speakers_analyze_linux_maturin_args(args, target=target) + ) if failures: raise DriverError(failures) version = _project_version(root) @@ -792,6 +898,12 @@ def _default_build_local_dist( # Leave anything else in dist for the inventory gate to reject. _remove_uv_dist_gitignore(root / "dist") _validate_local_dist_inventory(root / "dist", include_models=include_models) + cleanup_failures = _remove_owned_relative( + root, + Path("packages") / SPEAKERS_ANALYZE_WORKSPACE_PACKAGE / "wheel-data", + ) + if cleanup_failures: + raise DriverError(cleanup_failures) def _remove_uv_dist_gitignore(dist_dir: Path) -> None: @@ -1047,10 +1159,18 @@ def _macos_wheel_role(path: Path) -> str | None: for item in expected_wheels if item.startswith("solstone_core-") and "macosx_14_0_arm64" in item ) + expected_speakers_analyze = next( + item + for item in expected_wheels + if item.startswith("solstone_core_speakers_analyze-") + and "macosx_14_0_arm64" in item + ) if name == expected_core: return "core" if name == expected_root: return "root" + if name == expected_speakers_analyze: + return "speakers-analyze" return None @@ -1168,6 +1288,8 @@ def _native_record_role(path: Path) -> str | None: return "root" if path.name == "macos-native-core.json": return "core" + if path.name == "macos-native-speakers-analyze.json": + return "speakers-analyze" return None @@ -1185,7 +1307,7 @@ def _native_record_payloads( failures.append( _failure( "build-host macOS wheel role is invalid", - expected="root or core macOS arm64 wheel", + expected="root, core, or speakers-analyze macOS arm64 wheel", actual=wheel.name, repair="bash scripts/release.sh --candidate", ) @@ -1195,7 +1317,7 @@ def _native_record_payloads( failures.append( _failure( "build-host macOS wheel role is duplicated", - expected="one root wheel and one core wheel", + expected="one root wheel, one core wheel, and one speakers-analyze wheel", actual=role, repair="bash scripts/release.sh --candidate", ) @@ -1209,7 +1331,10 @@ def _native_record_payloads( failures.append( _failure( "build-host native record role is invalid", - expected="macos-native-root.json and macos-native-core.json", + expected=( + "macos-native-root.json, macos-native-core.json, and " + "macos-native-speakers-analyze.json" + ), actual=path.name, repair="bash scripts/release.sh --candidate", ) @@ -1219,7 +1344,10 @@ def _native_record_payloads( failures.append( _failure( "build-host native record role is duplicated", - expected="one root record and one core record", + expected=( + "one root record, one core record, and one speakers-analyze " + "record" + ), actual=role, repair="bash scripts/release.sh --candidate", ) @@ -1248,20 +1376,20 @@ def _native_record_payloads( ) ) records.append(payload) - if set(wheel_by_role) != {"root", "core"}: + if set(wheel_by_role) != {"root", "core", "speakers-analyze"}: failures.append( _failure( "build-host macOS wheel set is incomplete", - expected="root and core macOS wheels", + expected="root, core, and speakers-analyze macOS wheels", actual=", ".join(sorted(wheel_by_role)) or "", repair="bash scripts/release.sh --candidate", ) ) - if record_roles != {"root", "core"}: + if record_roles != {"root", "core", "speakers-analyze"}: failures.append( _failure( "build-host native record set is incomplete", - expected="root and core native records", + expected="root, core, and speakers-analyze native records", actual=", ".join(sorted(record_roles)) or "", repair="bash scripts/release.sh --candidate", ) @@ -1278,11 +1406,11 @@ def _native_records_by_role( failures: list[Failure] = [] for record in native_records: role = record.get("role") - if role not in {"root", "core"}: + if role not in {"root", "core", "speakers-analyze"}: failures.append( _failure( "native record role is invalid", - expected="root or core", + expected="root, core, or speakers-analyze", actual=str(role), repair="bash scripts/release.sh --candidate", ) @@ -1292,17 +1420,20 @@ def _native_records_by_role( failures.append( _failure( "native record role is duplicated", - expected="one root record and one core record", + expected=( + "one root record, one core record, and one " + "speakers-analyze record" + ), actual=str(role), repair="bash scripts/release.sh --candidate", ) ) records[str(role)] = record - if set(records) != {"root", "core"}: + if set(records) != {"root", "core", "speakers-analyze"}: failures.append( _failure( "native record set is incomplete", - expected="root and core native records", + expected="root, core, and speakers-analyze native records", actual=", ".join(sorted(records)) or "", repair="bash scripts/release.sh --candidate", ) @@ -1545,11 +1676,11 @@ def _lane_evidence_from_full_tool_evidence( def _copy_macos_wheels(host_result: BuildHostResult, dist_dir: Path) -> None: failures: list[Failure] = [] - if len(host_result.macos_wheels) != 2: + if len(host_result.macos_wheels) != 3: failures.append( _failure( "build-host macOS wheel set has wrong size", - expected="exactly two macOS wheels", + expected="exactly three macOS wheels", actual=str(len(host_result.macos_wheels)), repair="bash scripts/release.sh --candidate", ) diff --git a/scripts/release_install_smoke.py b/scripts/release_install_smoke.py index 6e3c6fe8c..3b8e60989 100644 --- a/scripts/release_install_smoke.py +++ b/scripts/release_install_smoke.py @@ -7,6 +7,7 @@ from __future__ import annotations import hashlib +import json import os import re import shutil @@ -28,7 +29,11 @@ from scripts.check_rust_release_manifest import ( Failure, canonical_json_bytes, ) -from scripts.check_wheel_contents import CORE_SCRIPT_NAMES, ROOT_LAUNCHER_NAMES +from scripts.check_wheel_contents import ( + CORE_SCRIPT_NAMES, + ROOT_LAUNCHER_NAMES, + SPEAKERS_ANALYZE_SCRIPT_NAMES, +) from scripts.release_digest import file_sha256_size from scripts.release_public_evidence import validate_public_evidence_tree @@ -56,6 +61,7 @@ TOP_LEVEL_KEYS = frozenset( ) ) PROOF_KIND = "solstone-native-install-proof" +ROOT = Path(__file__).resolve().parent.parent CORE_SMOKE_STDOUT = { "sol": "sol (solstone)", "solstone": "sol (solstone)", @@ -63,6 +69,10 @@ CORE_SMOKE_STDOUT = { } # Version smoke spans root launchers plus the core member. INSTALL_SCRIPT_NAMES = ROOT_LAUNCHER_NAMES + CORE_SCRIPT_NAMES +SPEAKERS_ANALYZE_SCRIPT_NAME = SPEAKERS_ANALYZE_SCRIPT_NAMES[0] +SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS = frozenset(("linux-x86_64-musl",)) +SPEAKERS_ANALYZE_RESPONSE_SCHEMA = "solstone-speaker-analyze-response-v1" +SPEAKERS_ANALYZE_REQUEST_SCHEMA = "solstone-speaker-analyze-request-v1" ENVROOT = "ENVROOT" CANDIDATE = "CANDIDATE" RETAINED_PROOF_REPAIR = ( @@ -168,9 +178,12 @@ def _env_bin(env_root: Path, name: str) -> Path: return env_root / "bin" / name -def _run_command(argv: Sequence[str]) -> CommandResult: +def _run_command( + argv: Sequence[str], *, input_text: str | None = None +) -> CommandResult: result = subprocess.run( list(argv), + input=input_text, capture_output=True, text=True, check=False, @@ -239,6 +252,10 @@ def _select_names_for_target(target: str, names: Sequence[str]) -> tuple[str, .. for name in sorted(names): if not name.endswith(".whl"): continue + if name.startswith("solstone_core_speakers_analyze-"): + if target == "linux-x86_64-musl" and "manylinux_2_27_x86_64" in name: + selected.append(name) + continue if name.startswith("solstone_core-"): if target == "linux-x86_64-musl" and "x86_64" in name: selected.append(name) @@ -319,6 +336,41 @@ def _find_single(root: Path, name: str) -> Path | None: return matches[0] if len(matches) == 1 else None +def _speakers_analyze_request(env_root: Path) -> str: + work_dir = env_root / "speakers-analyze-smoke" + work_dir.mkdir(parents=True, exist_ok=True) + audio_path = work_dir / "audio.f32le" + audio_path.write_bytes(b"\0" * 4 * 16000) + payload_path = work_dir / "statement-embedding.f32le" + interval_payload_path = work_dir / "interval-embedding.f32le" + assets = ( + ROOT + / "packages" + / "solstone-journal-models" + / "solstone_journal_models" + / "assets" + ) + request = { + "schema": SPEAKERS_ANALYZE_REQUEST_SCHEMA, + "sample_rate_hz": 16000, + "full_audio_f32le_path": str(audio_path), + "reduced_audio_f32le_path": None, + "models": { + "pyannote_segmentation_onnx_path": str( + assets / "pyannote-segmentation-3.0.onnx" + ), + "wespeaker_onnx_path": str(assets / "wespeaker-resnet34-256.onnx"), + }, + "output_payload_f32le_path": str(payload_path), + "interval_embedding_payload_f32le_path": str(interval_payload_path), + "statement_embedding": { + "spans": [{"statement_id": 1, "start_s": 0.0, "end_s": 0.5}] + }, + "diarization": {"spans": [{"statement_id": 1, "start_s": 0.0, "end_s": 0.5}]}, + } + return json.dumps(request, sort_keys=True) + + def _distribution_from_wheel_metadata(path: Path) -> Mapping[str, str] | None: try: with zipfile.ZipFile(path) as wheel: @@ -386,7 +438,10 @@ def _default_observe_install( ) after = _solstone_distributions(env_python) installed_members: list[Mapping[str, Any]] = [] - executable_paths = {name: _env_bin(env_root, name) for name in INSTALL_SCRIPT_NAMES} + executable_names = list(INSTALL_SCRIPT_NAMES) + if target in SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS: + executable_names.append(SPEAKERS_ANALYZE_SCRIPT_NAME) + executable_paths = {name: _env_bin(env_root, name) for name in executable_names} for name, executable_path in executable_paths.items(): if executable_path.is_file() or executable_path.is_symlink(): installed_members.append(_installed_member(executable_path, name)) @@ -395,7 +450,21 @@ def _default_observe_install( installed_members.append(_installed_member(helper_path, "parakeet-helper")) smoke: dict[str, CommandResult] = {} for name, executable_path in executable_paths.items(): - if executable_path.exists() or executable_path.is_symlink(): + if name == SPEAKERS_ANALYZE_SCRIPT_NAME: + if executable_path.exists() or executable_path.is_symlink(): + smoke[name] = _run_command( + (str(executable_path),), + input_text=_speakers_analyze_request(env_root), + ) + else: + smoke[name] = CommandResult( + argv=(str(executable_path),), + exit_code=127, + stdout="", + stderr="missing executable", + env=SCRUBBED_COMMAND_ENV, + ) + elif executable_path.exists() or executable_path.is_symlink(): smoke[name] = _run_command((str(executable_path), "--version")) else: smoke[name] = CommandResult( @@ -532,6 +601,15 @@ def _root_wheel_paths(install_paths: Sequence[Path]) -> tuple[Path, ...]: ) +def _speakers_analyze_wheel_paths(install_paths: Sequence[Path]) -> tuple[Path, ...]: + return tuple( + path + for path in install_paths + if path.name.startswith("solstone_core_speakers_analyze-") + and path.name.endswith(".whl") + ) + + def _root_launcher_members_from_wheel( wheel_path: Path, ) -> tuple[Mapping[str, Mapping[str, Any]], list[Failure]]: @@ -578,6 +656,48 @@ def _root_launcher_members_from_wheel( ] +def _speakers_analyze_members_from_wheel( + wheel_path: Path, +) -> tuple[Mapping[str, Mapping[str, Any]], list[Failure]]: + try: + with zipfile.ZipFile(wheel_path) as wheel: + scripts = sorted( + info + for info in wheel.infolist() + if info.filename.endswith( + f".data/scripts/{SPEAKERS_ANALYZE_SCRIPT_NAME}" + ) + ) + if len(scripts) != 1: + return {}, [ + _failure( + "install proof speakers-analyze member set is invalid", + expected=SPEAKERS_ANALYZE_SCRIPT_NAME, + actual=", ".join(Path(info.filename).name for info in scripts) + or "", + repair="python3 scripts/check_wheel_contents.py", + ) + ] + script = scripts[0] + content = wheel.read(script) + return { + SPEAKERS_ANALYZE_SCRIPT_NAME: { + "path": script.filename, + "sha256": hashlib.sha256(content).hexdigest(), + "bytes": len(content), + } + }, [] + except (OSError, zipfile.BadZipFile): + return {}, [ + _failure( + "install proof speakers-analyze wheel is unreadable", + expected="readable speakers-analyze wheel", + actual=wheel_path.name, + repair="python3 scripts/check_wheel_contents.py", + ) + ] + + def _expected_install_members( ledger_payload: Mapping[str, Any], target: str, @@ -621,6 +741,24 @@ def _expected_install_members( ) continue members[name] = member + speakers_wheels = _speakers_analyze_wheel_paths(install_paths) + if target in SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS: + if len(speakers_wheels) != 1: + failures.append( + _failure( + "install proof speakers-analyze wheel selection is invalid", + expected="exactly one speakers-analyze helper wheel", + actual=", ".join(path.name for path in speakers_wheels) + or "", + repair="python3 scripts/check_rust_release_manifest.py", + ) + ) + else: + speakers_members, speakers_failures = _speakers_analyze_members_from_wheel( + speakers_wheels[0] + ) + failures.extend(speakers_failures) + members.update(speakers_members) return members, failures @@ -683,6 +821,47 @@ def _env_failures(label: str, env: Mapping[str, str]) -> list[Failure]: return failures +def _expected_smoke_names(target: str) -> set[str]: + names = set(INSTALL_SCRIPT_NAMES) + if target in SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS: + names.add(SPEAKERS_ANALYZE_SCRIPT_NAME) + return names + + +def _expected_smoke_argv(name: str) -> tuple[str, ...]: + if name == SPEAKERS_ANALYZE_SCRIPT_NAME: + return (f"{ENVROOT}/bin/{name}",) + return (f"{ENVROOT}/bin/{name}", "--version") + + +def _validate_speakers_analyze_stdout(stdout: str, *, repair: str) -> list[Failure]: + try: + payload = json.loads(stdout) + except json.JSONDecodeError as exc: + return [ + _failure( + "install proof speakers-analyze smoke stdout is not JSON", + expected=f"{SPEAKERS_ANALYZE_RESPONSE_SCHEMA} JSON response", + actual=str(exc), + repair=repair, + ) + ] + if not isinstance(payload, Mapping) or payload.get("schema") != ( + SPEAKERS_ANALYZE_RESPONSE_SCHEMA + ): + return [ + _failure( + "install proof speakers-analyze smoke response schema is invalid", + expected=SPEAKERS_ANALYZE_RESPONSE_SCHEMA, + actual=repr( + payload.get("schema") if isinstance(payload, Mapping) else payload + ), + repair=repair, + ) + ] + return [] + + def _expected_install_argv( *, env_root: Path, @@ -963,7 +1142,7 @@ def _validate_observation( repair="python3 scripts/check_rust_release_manifest.py", ) ) - expected_smoke_names = set(INSTALL_SCRIPT_NAMES) + expected_smoke_names = _expected_smoke_names(target) if set(observation.smoke) != expected_smoke_names: failures.append( _failure( @@ -982,7 +1161,7 @@ def _validate_observation( ) for token in normalize_argv(result.argv) ) - expected_smoke_argv = (f"{ENVROOT}/bin/{name}", "--version") + expected_smoke_argv = _expected_smoke_argv(name) if normalized_smoke_argv != expected_smoke_argv: failures.append( _failure( @@ -997,21 +1176,33 @@ def _validate_observation( failures.append( _failure( "install proof smoke command failed", - expected=f"{name} version smoke exit 0", + expected=( + f"{name} real-inference smoke exit 0" + if name == SPEAKERS_ANALYZE_SCRIPT_NAME + else f"{name} version smoke exit 0" + ), actual=str(result.exit_code), repair="python3 scripts/check_rust_release_manifest.py", ) ) - expected_stdout = f"{CORE_SMOKE_STDOUT.get(name, name)} {version}" - if result.stdout != expected_stdout: - failures.append( - _failure( - "install proof smoke stdout is not exact", - expected=expected_stdout, - actual=result.stdout, + if name == SPEAKERS_ANALYZE_SCRIPT_NAME: + failures.extend( + _validate_speakers_analyze_stdout( + result.stdout, repair="python3 scripts/check_rust_release_manifest.py", ) ) + else: + expected_stdout = f"{CORE_SMOKE_STDOUT.get(name, name)} {version}" + if result.stdout != expected_stdout: + failures.append( + _failure( + "install proof smoke stdout is not exact", + expected=expected_stdout, + actual=result.stdout, + repair="python3 scripts/check_rust_release_manifest.py", + ) + ) return failures @@ -1479,16 +1670,17 @@ def _validate_proof_semantics( ) smoke = proof.get("smoke") smoke_items = smoke if isinstance(smoke, Mapping) else {} - if set(smoke_items) != set(INSTALL_SCRIPT_NAMES): + expected_smoke_names = _expected_smoke_names(target) + if set(smoke_items) != expected_smoke_names: failures.append( _failure( "install proof smoke command set does not match release executables", - expected=", ".join(INSTALL_SCRIPT_NAMES), + expected=", ".join(sorted(expected_smoke_names)), actual=", ".join(sorted(str(key) for key in smoke_items)) or "", repair="python3 scripts/check_rust_release_manifest.py", ) ) - for name in INSTALL_SCRIPT_NAMES: + for name in sorted(expected_smoke_names): smoke_entry = smoke_items.get(name) if isinstance(smoke_items, Mapping) else {} if not isinstance(smoke_entry, Mapping): failures.append( @@ -1500,12 +1692,15 @@ def _validate_proof_semantics( ) ) continue - expected_stdout = f"{CORE_SMOKE_STDOUT[name]} {version}" expected_smoke = { - "argv": [f"{ENVROOT}/bin/{name}", "--version"], + "argv": list(_expected_smoke_argv(name)), "env": dict(SCRUBBED_COMMAND_ENV), "exit_code": 0, - "stdout": expected_stdout, + "stdout": ( + str(smoke_entry.get("stdout")) + if name == SPEAKERS_ANALYZE_SCRIPT_NAME + else f"{CORE_SMOKE_STDOUT[name]} {version}" + ), "stderr": "", } if dict(smoke_entry) != expected_smoke: @@ -1517,6 +1712,13 @@ def _validate_proof_semantics( repair="python3 scripts/check_rust_release_manifest.py", ) ) + if name == SPEAKERS_ANALYZE_SCRIPT_NAME: + failures.extend( + _validate_speakers_analyze_stdout( + str(smoke_entry.get("stdout")), + repair="python3 scripts/check_rust_release_manifest.py", + ) + ) return failures @@ -1903,11 +2105,12 @@ def validate_install_proof( ) ) smoke = proof.get("smoke") - if not isinstance(smoke, Mapping) or set(smoke) != set(INSTALL_SCRIPT_NAMES): + expected_smoke_names = _expected_smoke_names(target) + if not isinstance(smoke, Mapping) or set(smoke) != expected_smoke_names: failures.append( _failure( "install proof smoke section is invalid", - expected=", ".join(INSTALL_SCRIPT_NAMES) + " smoke results", + expected=", ".join(sorted(expected_smoke_names)) + " smoke results", actual=repr(smoke), repair="python3 scripts/check_rust_release_manifest.py", ) diff --git a/scripts/release_ledger.py b/scripts/release_ledger.py index 1c7ffc754..2e1e63f03 100644 --- a/scripts/release_ledger.py +++ b/scripts/release_ledger.py @@ -423,11 +423,11 @@ def _native_summary(records: Sequence[Mapping[str, Any]]) -> dict[str, Any]: failures: list[Failure] = [] for record in records: role = record.get("role") - if role not in {"root", "core"}: + if role not in {"root", "core", "speakers-analyze"}: failures.append( _failure( "macOS native record role is invalid", - expected="root or core", + expected="root, core, or speakers-analyze", actual=str(role), repair="python3 scripts/check_rust_release_manifest.py", ) @@ -437,18 +437,18 @@ def _native_summary(records: Sequence[Mapping[str, Any]]) -> dict[str, Any]: failures.append( _failure( "macOS native record role is duplicated", - expected="one root and one core record", + expected="one root, one core, and one speakers-analyze record", actual=str(role), repair="python3 scripts/check_rust_release_manifest.py", ) ) continue by_role[str(role)] = record - if set(by_role) != {"root", "core"}: + if set(by_role) != {"root", "core", "speakers-analyze"}: failures.append( _failure( "macOS native record set is incomplete", - expected="exactly root and core records", + expected="exactly root, core, and speakers-analyze records", actual=", ".join(sorted(by_role)) or "", repair="python3 scripts/check_rust_release_manifest.py", ) @@ -481,6 +481,7 @@ def _native_summary(records: Sequence[Mapping[str, Any]]) -> dict[str, Any]: return { "macos_root_helper": summarize(by_role["root"]), "macos_core_script": summarize(by_role["core"]), + "macos_speakers_analyze": summarize(by_role["speakers-analyze"]), } @@ -491,11 +492,11 @@ def _macos_records_by_role( by_role: dict[str, Mapping[str, Any]] = {} for record in records: role = record.get("role") - if role not in {"root", "core"}: + if role not in {"root", "core", "speakers-analyze"}: failures.append( _failure( "macOS native record role is invalid", - expected="root or core", + expected="root, core, or speakers-analyze", actual=str(role), repair="python3 scripts/check_rust_release_manifest.py", ) @@ -505,18 +506,18 @@ def _macos_records_by_role( failures.append( _failure( "macOS native record role is duplicated", - expected="one root and one core record", + expected="one root, one core, and one speakers-analyze record", actual=str(role), repair="python3 scripts/check_rust_release_manifest.py", ) ) continue by_role[str(role)] = record - if set(by_role) != {"root", "core"}: + if set(by_role) != {"root", "core", "speakers-analyze"}: failures.append( _failure( "macOS native record set is incomplete", - expected="exactly root and core records", + expected="exactly root, core, and speakers-analyze records", actual=", ".join(sorted(by_role)) or "", repair="python3 scripts/check_rust_release_manifest.py", ) @@ -710,7 +711,8 @@ def _native_members( if root_wheel_name is None: raise AssertionError("root wheel name missing without failures") return _native_members_from_wheels( - release_dir, root_wheel_name=str(root_wheel_name["name"]) + release_dir, + root_wheel_name=str(root_wheel_name["name"]), ) diff --git a/tests/helpers/release_candidate_fixtures.py b/tests/helpers/release_candidate_fixtures.py index b4892fa3e..f2b1ca0c7 100644 --- a/tests/helpers/release_candidate_fixtures.py +++ b/tests/helpers/release_candidate_fixtures.py @@ -27,6 +27,8 @@ from scripts.check_wheel_contents import ( CPU_TYPE_ARM64, ELF_MACHINE, EXPECTED_MODEL_SHA256, + SPEAKERS_ANALYZE_SCRIPT_NAMES, + SPEAKERS_ANALYZE_TARGETS, ) from scripts.release_advisory_policy import PolicyRun from scripts.release_build_host import BuildHostResult, SourceBundle @@ -46,8 +48,10 @@ from tests.helpers.release_wheel_fixtures import ( minimal_elf, minimal_macho, record_hash, + speakers_analyze_elf, write_core_wheel, write_platform_base_wheel, + write_speakers_analyze_wheel, ) SOURCE_COMMIT = "a" * 40 @@ -57,6 +61,11 @@ _LINUX_X86_CORE = minimal_elf(ELF_MACHINE["x86_64"]) _LINUX_AARCH64_CORE = minimal_elf(ELF_MACHINE["aarch64"]) MACOS_CORE = minimal_macho(CPU_TYPE_ARM64) MACOS_HELPER = minimal_macho(CPU_TYPE_ARM64) +MACOS_SPEAKERS_ANALYZE = minimal_macho(CPU_TYPE_ARM64) +MACOS_ONNXRUNTIME = minimal_macho(CPU_TYPE_ARM64) +SPEAKERS_ANALYZE_RUNTIME_BYTES = b"fixture onnxruntime GLIBC_2.27\n" +SPEAKERS_ANALYZE_LICENSE_BYTES = b"fixture onnxruntime license\n" +SPEAKERS_ANALYZE_THIRD_PARTY_NOTICE_BYTES = b"fixture onnxruntime notices\n" _ZIP_DATE_TIME = (2026, 7, 20, 12, 0, 0) TombstoneMutation = Literal[ @@ -177,6 +186,22 @@ def _write_linux_core_wheels(dist_dir: Path) -> None: ) +def _write_linux_speakers_analyze_wheels(dist_dir: Path) -> None: + for tag, machine in ( + ("manylinux_2_27_x86_64", "x86_64"), + ("manylinux_2_27_aarch64", "aarch64"), + ): + write_speakers_analyze_wheel( + dist_dir, + tag=tag, + binary=speakers_analyze_elf(ELF_MACHINE[machine]), + library=SPEAKERS_ANALYZE_RUNTIME_BYTES, + license_notice=SPEAKERS_ANALYZE_LICENSE_BYTES, + third_party_notice=SPEAKERS_ANALYZE_THIRD_PARTY_NOTICE_BYTES, + version=checker._current_version(), + ) + + def _write_core_sdist(path: Path) -> None: version = checker._current_version() with path.open("wb") as raw: @@ -214,7 +239,7 @@ def _write_models_wheel(path: Path) -> None: wheel.writestr(asset_info, (assets_dir / basename).read_bytes()) -def macos_wheel_names() -> tuple[str, str]: +def macos_wheel_names() -> tuple[str, str, str]: names = checker.expected_package_names(include_models=False) root = next( name @@ -226,7 +251,13 @@ def macos_wheel_names() -> tuple[str, str]: for name in names if name.startswith("solstone_core-") and "macosx_14_0_arm64" in name ) - return root, core + speakers_analyze = next( + name + for name in names + if name.startswith("solstone_core_speakers_analyze-") + and "macosx_14_0_arm64" in name + ) + return root, core, speakers_analyze def _facts(content: bytes) -> dict[str, Any]: @@ -250,21 +281,33 @@ def _core_facts(content: bytes) -> dict[str, Any]: return {"members": {name: _facts(content) for name in CORE_SCRIPT_NAMES}} +def _speakers_analyze_facts(script: bytes, dylib: bytes) -> dict[str, Any]: + return { + "members": { + SPEAKERS_ANALYZE_SCRIPT_NAMES[0]: _facts(script), + SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name: _facts(dylib), + } + } + + def write_macos_host_outputs( output_dir: Path, *, mutate: str | None = None, ) -> BuildHostResult: output_dir.mkdir(parents=True, exist_ok=True) - root_name, core_name = macos_wheel_names() + root_name, core_name, speakers_analyze_name = macos_wheel_names() root_wheel = output_dir / root_name core_wheel = output_dir / core_name + speakers_analyze_wheel = output_dir / speakers_analyze_name if mutate == "wrong_tag": root_wheel = output_dir / root_name.replace( "macosx_14_0_arm64", "manylinux2014_x86_64" ) root_bytes = MACOS_HELPER core_bytes = MACOS_CORE + speakers_bytes = MACOS_SPEAKERS_ANALYZE + onnxruntime_bytes = MACOS_ONNXRUNTIME write_platform_base_wheel( root_wheel.parent, helper_binary=root_bytes, @@ -278,6 +321,15 @@ def write_macos_host_outputs( binary=core_bytes, version=checker._current_version(), ) + write_speakers_analyze_wheel( + speakers_analyze_wheel.parent, + tag="macosx_14_0_arm64", + binary=speakers_bytes, + library=onnxruntime_bytes, + license_notice=SPEAKERS_ANALYZE_LICENSE_BYTES, + third_party_notice=SPEAKERS_ANALYZE_THIRD_PARTY_NOTICE_BYTES, + version=checker._current_version(), + ) root_record = native.build_macos_native_record( role="root", wheel_path=root_wheel, @@ -292,6 +344,13 @@ def write_macos_host_outputs( source_commit=SOURCE_COMMIT, core_lock_sha256=LOCK_SHA, ) + speakers_record = native.build_macos_native_record( + role="speakers-analyze", + wheel_path=speakers_analyze_wheel, + signing_facts=_speakers_analyze_facts(speakers_bytes, onnxruntime_bytes), + source_commit=SOURCE_COMMIT, + core_lock_sha256=LOCK_SHA, + ) if mutate == "record_role": root_record["role"] = "core" if mutate == "member": @@ -306,6 +365,7 @@ def write_macos_host_outputs( root_record["wheel"]["sha256"] = "0" * 64 root_record_path = output_dir / "macos-native-root.json" core_record_path = output_dir / "macos-native-core.json" + speakers_record_path = output_dir / "macos-native-speakers-analyze.json" if mutate == "record_paths_swapped": root_record_path, core_record_path = core_record_path, root_record_path root_record_path.write_text( @@ -314,9 +374,12 @@ def write_macos_host_outputs( core_record_path.write_text( json.dumps(core_record, sort_keys=True), encoding="utf-8" ) + speakers_record_path.write_text( + json.dumps(speakers_record, sort_keys=True), encoding="utf-8" + ) return BuildHostResult( - macos_wheels=(root_wheel, core_wheel), - native_records=(root_record_path, core_record_path), + macos_wheels=(root_wheel, core_wheel, speakers_analyze_wheel), + native_records=(root_record_path, core_record_path, speakers_record_path), tool_evidence=pins.fixture_presign_lane_tool_evidence("macos-arm64"), ) @@ -368,6 +431,48 @@ def _proof_observation( "symlink": False, } ) + helper_wheels = [ + path + for path in install_paths + if path.name.startswith("solstone_core_speakers_analyze-") + and "manylinux_2_27_x86_64" in path.name + ] + if helper_wheels: + with zipfile.ZipFile(helper_wheels[0]) as wheel: + helper_member = next( + info + for info in wheel.infolist() + if info.filename.endswith( + ".data/scripts/solstone-core-speakers-analyze" + ) + ) + helper_bytes = wheel.read(helper_member) + helper_path = env_root / "bin" / "solstone-core-speakers-analyze" + helper_path.write_bytes(helper_bytes) + members.append( + { + "name": "solstone-core-speakers-analyze", + "path": helper_path, + "sha256": hashlib.sha256(helper_bytes).hexdigest(), + "symlink": False, + } + ) + smoke_results = { + name: CommandResult( + argv=(str(env_root / "bin" / name), "--version"), + exit_code=0, + stdout=f"{CORE_SMOKE_STDOUT[name]} {version}", + env=SCRUBBED_COMMAND_ENV, + ) + for name in INSTALL_SCRIPT_NAMES + } + if helper_wheels: + smoke_results["solstone-core-speakers-analyze"] = CommandResult( + argv=(str(env_root / "bin" / "solstone-core-speakers-analyze"),), + exit_code=0, + stdout='{"schema":"solstone-speaker-analyze-response-v1"}', + env=SCRUBBED_COMMAND_ENV, + ) return InstallObservation( env_root=env_root, preexisting_distributions=(), @@ -387,15 +492,7 @@ def _proof_observation( ), installed_distributions=expected_distribution_entries(install_paths), installed_members=tuple(members), - smoke={ - name: CommandResult( - argv=(str(env_root / "bin" / name), "--version"), - exit_code=0, - stdout=f"{CORE_SMOKE_STDOUT[name]} {version}", - env=SCRUBBED_COMMAND_ENV, - ) - for name in INSTALL_SCRIPT_NAMES - }, + smoke=smoke_results, ) @@ -423,6 +520,10 @@ def services( path = dist / name if name.startswith("solstone_journal_models-") and name.endswith(".whl"): _write_models_wheel(path) + elif name.startswith("solstone_core_speakers_analyze-") and name.endswith( + ".whl" + ): + continue elif name.endswith(".whl"): _write_metadata_wheel(path) elif name.startswith("solstone_core-") and name.endswith(".tar.gz"): @@ -430,6 +531,7 @@ def services( else: path.write_bytes(b"fixture package") _write_linux_core_wheels(repo_root / "dist") + _write_linux_speakers_analyze_wheels(repo_root / "dist") def create_source_bundle( _repo: Path, commit: str, output_path: Path diff --git a/tests/helpers/release_wheel_fixtures.py b/tests/helpers/release_wheel_fixtures.py index d471ac12d..b897c372d 100644 --- a/tests/helpers/release_wheel_fixtures.py +++ b/tests/helpers/release_wheel_fixtures.py @@ -63,6 +63,93 @@ def minimal_elf( return bytes(content) +def speakers_analyze_elf( + machine: int, + *, + needed: Sequence[str] = ("libonnxruntime.so.1", "libc.so.6"), + runpath: str | None = checker.SPEAKERS_ANALYZE_RUNPATH, + rpath: str | None = None, + include_interp: bool = True, + glibc: str = "2.27", +) -> bytes: + phnum = 3 if include_interp else 2 + phoff = ELF_HEADER_SIZE + headers_end = ELF_HEADER_SIZE + ELF_PROGRAM_HEADER_SIZE * phnum + interp = b"/lib64/ld-linux-x86-64.so.2\0" + interp_offset = headers_end + dynamic_offset = interp_offset + (len(interp) if include_interp else 0) + + dynstr = bytearray(b"\0") + needed_offsets: list[int] = [] + for value in needed: + needed_offsets.append(len(dynstr)) + dynstr.extend(value.encode("utf-8") + b"\0") + runpath_offset: int | None = None + if runpath is not None: + runpath_offset = len(dynstr) + dynstr.extend(runpath.encode("utf-8") + b"\0") + rpath_offset: int | None = None + if rpath is not None: + rpath_offset = len(dynstr) + dynstr.extend(rpath.encode("utf-8") + b"\0") + + dynamic_entries = 1 + len(needed_offsets) + int(runpath_offset is not None) + dynamic_entries += int(rpath_offset is not None) + 1 + dynamic_size = dynamic_entries * 16 + dynstr_offset = dynamic_offset + dynamic_size + glibc_marker = f"\0GLIBC_{glibc}\0".encode("ascii") + total_size = dynstr_offset + len(dynstr) + len(glibc_marker) + base_vaddr = 0x400000 + content = bytearray(total_size) + content[:16] = b"\x7fELF\x02\x01\x01" + b"\0" * 9 + struct.pack_into(" None: + phdr = phoff + ELF_PROGRAM_HEADER_SIZE * index + struct.pack_into(" bytes: content = bytearray(32) struct.pack_into(" Path: + path.mkdir(parents=True, exist_ok=True) + wheel_path = path / f"solstone_core_speakers_analyze-{version}-py3-none-{tag}.whl" + data_prefix = f"solstone_core_speakers_analyze-{version}.data" + dist_info_prefix = f"solstone_core_speakers_analyze-{version}.dist-info" + platform_tuple = checker.SPEAKERS_ANALYZE_TAG_PLATFORMS[tag] + spec = checker.SPEAKERS_ANALYZE_TARGETS[ + checker.SPEAKERS_ANALYZE_PLATFORM_TARGETS[platform_tuple] + ] + if binary is None: + if platform_tuple[0] == "darwin": + binary = minimal_macho(checker.CPU_TYPE_ARM64) + else: + machine = ( + checker.ELF_MACHINE["aarch64"] + if platform_tuple[1] == "aarch64" + else checker.ELF_MACHINE["x86_64"] + ) + binary = speakers_analyze_elf(machine) + members = { + f"{data_prefix}/{checker.SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/{spec.runtime_staged_name}": library, + f"{data_prefix}/{checker.SPEAKERS_ANALYZE_NOTICE_INSTALL_DIR.as_posix()}/onnxruntime-LICENSE.txt": license_notice, + f"{data_prefix}/{checker.SPEAKERS_ANALYZE_NOTICE_INSTALL_DIR.as_posix()}/onnxruntime-ThirdPartyNotices.txt": third_party_notice, + f"{data_prefix}/scripts/solstone-core-speakers-analyze": binary, + f"{dist_info_prefix}/METADATA": ( + f"Name: solstone-core-speakers-analyze\nVersion: {version}\n".encode() + ), + f"{dist_info_prefix}/WHEEL": b"Wheel-Version: 1.0\n", + f"{dist_info_prefix}/sboms/solstone-core-speakers-analyze.cyclonedx.json": b"{}", + } + if extra_members: + members.update(extra_members) + if omit_member is not None: + members.pop(omit_member, None) + rows = [ + f"{name},{record_hash(content)},{len(content)}" + for name, content in members.items() + ] + rows.append(f"{dist_info_prefix}/RECORD,,") + record = "\n".join(rows).encode("utf-8") + if not record_ok: + record = record.replace(b"sha256=", b"sha256=broken", 1) + with zipfile.ZipFile(wheel_path, "w") as wheel: + for name, content in members.items(): + mode = ( + 0o755 + if Path(name).name in checker.SPEAKERS_ANALYZE_SCRIPT_NAMES + and executable + else 0o644 + ) + _write_member(wheel, name, content, mode=mode) + _write_member(wheel, f"{dist_info_prefix}/RECORD", record) + return wheel_path + + def write_platform_base_wheel( path: Path, *, diff --git a/tests/test_channel_adapters.py b/tests/test_channel_adapters.py index 3bb498d36..62e03a2ec 100644 --- a/tests/test_channel_adapters.py +++ b/tests/test_channel_adapters.py @@ -88,11 +88,24 @@ def _write_metadata_wheel(path: Path) -> None: for name, content in members.items() ) members[record_name] = f"{record}\n{record_name},,".encode("utf-8") + if path.name.startswith("solstone_core_speakers_analyze-"): + script_name = smoke.SPEAKERS_ANALYZE_SCRIPT_NAME + script_path = ( + f"solstone_core_speakers_analyze-{version}.data/scripts/{script_name}" + ) + members[script_path] = f"#!/bin/sh\necho {script_name}\n".encode("utf-8") + record_name = f"solstone_core_speakers_analyze-{version}.dist-info/RECORD" + record = "\n".join( + f"{name},{record_hash(content)},{len(content)}" + for name, content in members.items() + ) + members[record_name] = f"{record}\n{record_name},,".encode("utf-8") for name, content in members.items(): info = zipfile.ZipInfo(name) info.external_attr = ( 0o755 << 16 - if Path(name).name in smoke.ROOT_LAUNCHER_NAMES + if Path(name).name + in (*smoke.ROOT_LAUNCHER_NAMES, smoke.SPEAKERS_ANALYZE_SCRIPT_NAME) else 0o644 << 16 ) wheel.writestr(info, content) @@ -171,6 +184,7 @@ def _write_proof_request(tmp_path: Path) -> tuple[Path, dict[str, Any], dict[str for name in ( "solstone-1.0.0-py3-none-any.whl", "solstone_core-1.0.0-py3-none-linux_x86_64.whl", + "solstone_core_speakers_analyze-1.0.0-py3-none-manylinux_2_27_x86_64.whl", ): _write_metadata_wheel(candidate_dir / name) native_members = { @@ -236,10 +250,19 @@ def _write_valid_install_proof( executable_paths = { name: env_root / "bin" / name for name in smoke.INSTALL_SCRIPT_NAMES } + if request_payload["target"] in smoke.SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS: + executable_paths[smoke.SPEAKERS_ANALYZE_SCRIPT_NAME] = ( + env_root / "bin" / smoke.SPEAKERS_ANALYZE_SCRIPT_NAME + ) for name in smoke.ROOT_LAUNCHER_NAMES: executable_paths[name].write_bytes(ROOT_LAUNCHER_BYTES[name]) for name in smoke.CORE_SCRIPT_NAMES: executable_paths[name].write_text(name, encoding="utf-8") + if smoke.SPEAKERS_ANALYZE_SCRIPT_NAME in executable_paths: + executable_paths[smoke.SPEAKERS_ANALYZE_SCRIPT_NAME].write_text( + smoke.SPEAKERS_ANALYZE_SCRIPT_NAME, + encoding="utf-8", + ) install_paths = smoke.target_install_paths_from_ledger( ledger, target=request_payload["target"], @@ -290,13 +313,26 @@ def _write_valid_install_proof( for name, path in sorted(executable_paths.items()) ), smoke={ - name: smoke.CommandResult( - argv=(str(path), "--version"), - exit_code=0, - stdout=( - f"{smoke.CORE_SMOKE_STDOUT[name]} {request_payload['version']}" - ), - env=smoke.SCRUBBED_COMMAND_ENV, + name: ( + smoke.CommandResult( + argv=(str(path),), + exit_code=0, + stdout=json.dumps( + {"schema": smoke.SPEAKERS_ANALYZE_RESPONSE_SCHEMA}, + separators=(",", ":"), + ), + env=smoke.SCRUBBED_COMMAND_ENV, + ) + if name == smoke.SPEAKERS_ANALYZE_SCRIPT_NAME + else smoke.CommandResult( + argv=(str(path), "--version"), + exit_code=0, + stdout=( + f"{smoke.CORE_SMOKE_STDOUT[name]} " + f"{request_payload['version']}" + ), + env=smoke.SCRUBBED_COMMAND_ENV, + ) ) for name, path in sorted(executable_paths.items()) }, @@ -354,10 +390,11 @@ def test_build_request_response_round_trip_through_rail_parser( evidence = build_rail._validate_macos_tool_evidence(response) wheel_names, record_names = build_rail._names_from_payload(response) assert set(evidence) == set(expected_presign_lane_tool_evidence("macos-arm64")) - assert len(wheel_names) == 2 + assert len(wheel_names) == 3 assert tuple(record_names) == ( build_rail.MACOS_ROOT_RECORD, build_rail.MACOS_CORE_RECORD, + build_rail.MACOS_SPEAKERS_ANALYZE_RECORD, ) diff --git a/tests/test_check_release_preflight.py b/tests/test_check_release_preflight.py index 264856d98..1aceef675 100644 --- a/tests/test_check_release_preflight.py +++ b/tests/test_check_release_preflight.py @@ -571,7 +571,11 @@ def test_macos_tool_finalizer_requires_valid_native_records() -> None: final, failures = preflight.finalize_macos_tool_evidence( preflight_evidence, - (_native_record("root"), _native_record("core")), + ( + _native_record("root"), + _native_record("core"), + _native_record("speakers-analyze"), + ), ) assert failures == [] @@ -582,7 +586,11 @@ def test_macos_tool_finalizer_requires_valid_native_records() -> None: bad_record["notarization_status"] = "rejected" final, failures = preflight.finalize_macos_tool_evidence( preflight_evidence, - (bad_record, _native_record("core")), + ( + bad_record, + _native_record("core"), + _native_record("speakers-analyze"), + ), ) assert final is None diff --git a/tests/test_check_rust_release_manifest.py b/tests/test_check_rust_release_manifest.py index ef851a236..a73e1a20b 100644 --- a/tests/test_check_rust_release_manifest.py +++ b/tests/test_check_rust_release_manifest.py @@ -514,20 +514,28 @@ def test_generate_manifest_valid_lane_shapes( ) -def test_release_dir_accepts_exact_15_without_models(tmp_path: Path) -> None: +def _expected_release_file_count(*, include_models: bool) -> int: + return len(checker.expected_package_names(include_models=include_models)) + 4 + + +def test_release_dir_accepts_exact_without_models(tmp_path: Path) -> None: release_dir = _candidate(tmp_path) - assert len(list(release_dir.iterdir())) == 15 + assert len(list(release_dir.iterdir())) == _expected_release_file_count( + include_models=False + ) assert ( checker.validate_release_dir(release_dir, expected_source_commit=VALID_COMMIT) == [] ) -def test_release_dir_accepts_exact_17_with_models(tmp_path: Path) -> None: +def test_release_dir_accepts_exact_with_models(tmp_path: Path) -> None: release_dir = _candidate(tmp_path, include_models=True) - assert len(list(release_dir.iterdir())) == 17 + assert len(list(release_dir.iterdir())) == _expected_release_file_count( + include_models=True + ) assert ( checker.validate_release_dir(release_dir, expected_source_commit=VALID_COMMIT) == [] @@ -573,7 +581,7 @@ def test_release_dir_rejects_skipped_model_leftover(tmp_path: Path) -> None: release_dir, expected_source_commit=VALID_COMMIT ) - _assert_error(failures, "15-file candidate contains models archive leftover") + _assert_error(failures, "models-skipped candidate contains models archive leftover") def test_release_dir_rejects_unknown_missing_extra_assets_and_case_collision( @@ -1558,7 +1566,9 @@ def test_build_and_promote_candidate_success_is_whole_directory_rename( assert failures == [] assert ready.is_dir() assert not (tmp_path / "ready.staging").exists() - assert len(list(ready.iterdir())) == 15 + assert len(list(ready.iterdir())) == _expected_release_file_count( + include_models=False + ) assert ( checker.validate_release_dir(ready, expected_source_commit=VALID_COMMIT) == [] ) diff --git a/tests/test_check_wheel_contents.py b/tests/test_check_wheel_contents.py index 1ff60a909..8dfe660f5 100644 --- a/tests/test_check_wheel_contents.py +++ b/tests/test_check_wheel_contents.py @@ -8,6 +8,7 @@ import subprocess import sys import tarfile import zipfile +from dataclasses import replace from io import BytesIO from pathlib import Path @@ -18,12 +19,17 @@ from tests.helpers.release_wheel_fixtures import ( minimal_fat_macho, minimal_macho, record_hash, + speakers_analyze_elf, write_core_wheel, write_platform_base_wheel, + write_speakers_analyze_wheel, ) SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "check_wheel_contents.py" CPU_TYPE_X86_64 = 0x01000007 +SPEAKERS_LIBRARY = b"fixture libonnxruntime.so.1 GLIBC_2.27\n" +SPEAKERS_LICENSE = b"fixture license\n" +SPEAKERS_THIRD_PARTY_NOTICE = b"fixture third party notice\n" def _write_member( @@ -38,6 +44,29 @@ def _write_member( wheel.writestr(info, content) +def _patch_speakers_fixture_hashes(monkeypatch) -> None: + spec = checker.SPEAKERS_ANALYZE_TARGETS["linux-x86_64"] + notices = ( + replace( + spec.notices[0], + sha256=checker.hashlib.sha256(SPEAKERS_LICENSE).hexdigest(), + ), + replace( + spec.notices[1], + sha256=checker.hashlib.sha256(SPEAKERS_THIRD_PARTY_NOTICE).hexdigest(), + ), + ) + monkeypatch.setitem( + checker.SPEAKERS_ANALYZE_TARGETS, + "linux-x86_64", + replace( + spec, + runtime_sha256=checker.hashlib.sha256(SPEAKERS_LIBRARY).hexdigest(), + notices=notices, + ), + ) + + def test_script_runs_without_site_packages_from_outside_repo(tmp_path: Path) -> None: env = os.environ.copy() env.pop("PYTHONPATH", None) @@ -87,6 +116,16 @@ def test_core_wheel_validator_rejects_bare_linux_tag(tmp_path: Path) -> None: assert any("bare linux tag" in error for error in errors) +def test_core_wheel_validator_rejects_tag_outside_probe_constants( + tmp_path: Path, +) -> None: + wheel = write_core_wheel(tmp_path, tag="manylinux_2_28_x86_64") + + errors = checker.check_core_wheel(wheel, checker.MAX_CORE_WHEEL_BYTES) + + assert any("unsupported solstone-core wheel tag" in error for error in errors) + + def test_core_wheel_validator_rejects_non_executable_binary( tmp_path: Path, ) -> None: @@ -187,6 +226,155 @@ def test_core_wheel_validator_rejects_fat_macho_without_arm64(tmp_path: Path) -> assert any("fat Mach-O has no arm64 slice" in error for error in errors) +def test_speakers_analyze_wheel_validator_accepts_pinned_layout( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + wheel = write_speakers_analyze_wheel( + tmp_path, + library=SPEAKERS_LIBRARY, + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + ) + + assert checker.check_speakers_analyze_wheel(wheel) == [] + + +def test_speakers_analyze_wheel_validator_requires_exact_member_set( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + wheel = write_speakers_analyze_wheel( + tmp_path, + library=SPEAKERS_LIBRARY, + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + extra_members={"unexpected.txt": b"x"}, + ) + + errors = checker.check_speakers_analyze_wheel(wheel) + + assert any( + "speakers analyze wheel member set is wrong" in error for error in errors + ) + + +def test_speakers_analyze_wheel_validator_requires_sbom( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + wheel = write_speakers_analyze_wheel( + tmp_path, + library=SPEAKERS_LIBRARY, + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + omit_member=( + "solstone_core_speakers_analyze-1.2.3.dist-info/sboms/" + "solstone-core-speakers-analyze.cyclonedx.json" + ), + ) + + errors = checker.check_speakers_analyze_wheel(wheel) + + assert any( + "speakers analyze wheel member set is wrong" in error for error in errors + ) + + +def test_speakers_analyze_wheel_validator_rejects_provider_libraries( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + wheel = write_speakers_analyze_wheel( + tmp_path, + library=SPEAKERS_LIBRARY, + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + extra_members={ + "solstone_core_speakers_analyze-1.2.3.data/data/lib/" + "solstone-core-speakers-analyze/libonnxruntime_providers_shared.so": b"x" + }, + ) + + errors = checker.check_speakers_analyze_wheel(wheel) + + assert any("contains unproven provider library" in error for error in errors) + + +def test_speakers_analyze_wheel_validator_rejects_notice_hash_drift( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + wheel = write_speakers_analyze_wheel( + tmp_path, + library=SPEAKERS_LIBRARY, + license_notice=b"changed license\n", + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + ) + + errors = checker.check_speakers_analyze_wheel(wheel) + + assert any("notice digest mismatch" in error for error in errors) + + +def test_speakers_analyze_wheel_validator_requires_dynamic_elf_contract( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + binary = speakers_analyze_elf( + checker.ELF_MACHINE["x86_64"], + needed=("libc.so.6",), + runpath="/wrong", + include_interp=False, + ) + wheel = write_speakers_analyze_wheel( + tmp_path, + binary=binary, + library=SPEAKERS_LIBRARY, + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + ) + + errors = checker.check_speakers_analyze_wheel(wheel) + + assert any("missing PT_INTERP" in error for error in errors) + assert any("does not need ONNX Runtime" in error for error in errors) + assert any("RUNPATH is wrong" in error for error in errors) + + +def test_speakers_analyze_wheel_validator_rejects_understated_glibc_floor( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + wheel = write_speakers_analyze_wheel( + tmp_path, + binary=speakers_analyze_elf(checker.ELF_MACHINE["x86_64"], glibc="2.34"), + library=SPEAKERS_LIBRARY, + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + ) + + errors = checker.check_speakers_analyze_wheel(wheel) + + assert any("wheel tag understates GLIBC floor" in error for error in errors) + + +def test_speakers_analyze_helper_only_dist_is_checkable( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + write_speakers_analyze_wheel( + tmp_path, + library=SPEAKERS_LIBRARY, + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + ) + + errors = checker.check_dist(tmp_path, {}, checker.MAX_BASE_WHEEL_BYTES) + + assert errors == [] + + def test_base_wheel_validator_rejects_tests_path_segment(tmp_path: Path) -> None: clean = _write_minimal_wheel(tmp_path, "solstone") @@ -367,6 +555,24 @@ def test_release_artifacts_derive_core_tags_from_probe(tmp_path: Path) -> None: ) +def test_release_artifacts_derive_speakers_analyze_tags_from_probe( + tmp_path: Path, +) -> None: + artifacts = checker.release_artifacts( + tmp_path, + release_scope="all-hosts", + models_decision="skip", + ) + artifact_names = {path.name for path in artifacts} + + for tag in checker.SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS.values(): + assert any( + name.startswith("solstone_core_speakers_analyze-") + and name.endswith(f"-py3-none-{tag}.whl") + for name in artifact_names + ) + + def test_release_artifacts_include_models_only_when_gate_publishes( tmp_path: Path, ) -> None: diff --git a/tests/test_release_build_host.py b/tests/test_release_build_host.py index 8177e9f22..d0d812a39 100644 --- a/tests/test_release_build_host.py +++ b/tests/test_release_build_host.py @@ -20,7 +20,7 @@ from scripts.release_build_host import SourceBundle SOURCE_COMMIT = "a" * 40 -def _expected_macos_wheels() -> tuple[str, str]: +def _expected_macos_wheels() -> tuple[str, str, str]: names = checker.expected_package_names(include_models=False) root = next( name @@ -32,10 +32,16 @@ def _expected_macos_wheels() -> tuple[str, str]: for name in names if name.startswith("solstone_core-") and "macosx_14_0_arm64" in name ) - return root, core + speakers_analyze = next( + name + for name in names + if name.startswith("solstone_core_speakers_analyze-") + and "macosx_14_0_arm64" in name + ) + return root, core, speakers_analyze -ROOT_WHEEL, CORE_WHEEL = _expected_macos_wheels() +ROOT_WHEEL, CORE_WHEEL, SPEAKERS_ANALYZE_WHEEL = _expected_macos_wheels() def _run_git(repo: Path, argv: Sequence[str]) -> str: @@ -246,15 +252,18 @@ def _channel( assert request["expected_outputs"] == { "root_wheel": ROOT_WHEEL, "core_wheel": CORE_WHEEL, + "speakers_analyze_wheel": SPEAKERS_ANALYZE_WHEEL, "root_record": build_host.MACOS_ROOT_RECORD, "core_record": build_host.MACOS_CORE_RECORD, + "speakers_analyze_record": (build_host.MACOS_SPEAKERS_ANALYZE_RECORD), } if during_build is not None: during_build(cwd) - wheel_names = [ROOT_WHEEL, CORE_WHEEL] + wheel_names = [ROOT_WHEEL, CORE_WHEEL, SPEAKERS_ANALYZE_WHEEL] record_names = [ build_host.MACOS_ROOT_RECORD, build_host.MACOS_CORE_RECORD, + build_host.MACOS_SPEAKERS_ANALYZE_RECORD, ] if write_files: _write_expected( @@ -324,10 +333,15 @@ def test_external_channel_validates_attestation_and_uses_shlex( output_dir=output_dir, ) - assert [path.name for path in result.macos_wheels] == [ROOT_WHEEL, CORE_WHEEL] + assert [path.name for path in result.macos_wheels] == [ + ROOT_WHEEL, + CORE_WHEEL, + SPEAKERS_ANALYZE_WHEEL, + ] assert [path.name for path in result.native_records] == [ build_host.MACOS_ROOT_RECORD, build_host.MACOS_CORE_RECORD, + build_host.MACOS_SPEAKERS_ANALYZE_RECORD, ] assert calls == [ ("adapter", "quoted arg", "build-macos", "request.json"), @@ -741,7 +755,7 @@ def test_external_channel_rejects_unsafe_filename( bundle = _source_bundle(tmp_path) def mutate(payload: dict[str, object]) -> dict[str, object]: - payload["macos_wheels"] = [bad_name, CORE_WHEEL] + payload["macos_wheels"] = [bad_name, CORE_WHEEL, SPEAKERS_ANALYZE_WHEEL] return payload channel, _calls = _channel( @@ -770,7 +784,7 @@ def test_external_channel_rejects_one_byte_filename_skew_before_acceptance( skewed_name = f"{ROOT_WHEEL[:-5]}x.whl" def mutate(payload: dict[str, object]) -> dict[str, object]: - payload["macos_wheels"] = [skewed_name, CORE_WHEEL] + payload["macos_wheels"] = [skewed_name, CORE_WHEEL, SPEAKERS_ANALYZE_WHEEL] return payload channel, _calls = _channel( @@ -934,7 +948,7 @@ def test_external_channel_rejects_duplicates_extras_and_stale_output( bundle = _source_bundle(tmp_path) def duplicate_payload(payload: dict[str, object]) -> dict[str, object]: - payload["macos_wheels"] = [ROOT_WHEEL, ROOT_WHEEL] + payload["macos_wheels"] = [ROOT_WHEEL, ROOT_WHEEL, SPEAKERS_ANALYZE_WHEEL] return payload duplicate, _calls = _channel( @@ -950,7 +964,12 @@ def test_external_channel_rejects_duplicates_extras_and_stale_output( ) def extra_payload(payload: dict[str, object]) -> dict[str, object]: - payload["macos_wheels"] = [ROOT_WHEEL, CORE_WHEEL, "extra.whl"] + payload["macos_wheels"] = [ + ROOT_WHEEL, + CORE_WHEEL, + SPEAKERS_ANALYZE_WHEEL, + "extra.whl", + ] return payload extra, _calls = _channel( diff --git a/tests/test_release_candidate_driver.py b/tests/test_release_candidate_driver.py index 2a3071e89..1e2f77e05 100644 --- a/tests/test_release_candidate_driver.py +++ b/tests/test_release_candidate_driver.py @@ -10,6 +10,7 @@ import os import shutil import stat import subprocess +import sys import tarfile import tomllib import zipfile @@ -22,6 +23,7 @@ from typing import Any import pytest import scripts.check_rust_release_manifest as checker +import scripts.check_wheel_contents as wheel_checker import scripts.release_build_host as release_build_host import scripts.release_candidate_driver as driver import scripts.release_ledger as ledger @@ -36,7 +38,12 @@ from tests.helpers.release_candidate_fixtures import ( LOCK_SHA, MACOS_CORE, MACOS_HELPER, + MACOS_ONNXRUNTIME, + MACOS_SPEAKERS_ANALYZE, SOURCE_COMMIT, + SPEAKERS_ANALYZE_LICENSE_BYTES, + SPEAKERS_ANALYZE_RUNTIME_BYTES, + SPEAKERS_ANALYZE_THIRD_PARTY_NOTICE_BYTES, write_core_unsupported_tombstone_record, ) from tests.helpers.release_candidate_fixtures import ( @@ -68,6 +75,48 @@ assert not PRIOR_RETAINED_VERSION.startswith(checker._current_version()) assert not checker._current_version().startswith(PRIOR_RETAINED_VERSION) +@pytest.fixture(autouse=True) +def _patch_speakers_analyze_fixture_hashes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + patched_targets = {} + for target, spec in tuple(wheel_checker.SPEAKERS_ANALYZE_TARGETS.items()): + runtime = ( + MACOS_ONNXRUNTIME + if target == "macos-arm64" + else SPEAKERS_ANALYZE_RUNTIME_BYTES + ) + notices = ( + replace( + spec.notices[0], + sha256=hashlib.sha256(SPEAKERS_ANALYZE_LICENSE_BYTES).hexdigest(), + ), + replace( + spec.notices[1], + sha256=hashlib.sha256( + SPEAKERS_ANALYZE_THIRD_PARTY_NOTICE_BYTES + ).hexdigest(), + ), + ) + patched_targets[target] = replace( + spec, + runtime_sha256=hashlib.sha256(runtime).hexdigest(), + notices=notices, + ) + for module in ( + wheel_checker, + sys.modules.get("check_wheel_contents"), + ): + if module is None: + continue + for target, spec in patched_targets.items(): + monkeypatch.setitem( + module.SPEAKERS_ANALYZE_TARGETS, + target, + spec, + ) + + def _local_dist_names_for_build_argv( argv: Sequence[str], *, include_models: bool ) -> set[str]: @@ -79,6 +128,19 @@ def _local_dist_names_for_build_argv( for name in expected if name.startswith("solstone_core-") and name.endswith(".tar.gz") } + if args == ( + "uv", + "build", + "--package", + driver.SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, + "--wheel", + ): + return { + name + for name in expected + if name.startswith("solstone_core_speakers_analyze-") + and name.endswith(".whl") + } if len(args) == 4 and args[:3] == ("uv", "build", "--package"): package = args[3] prefix = f"{package.replace('-', '_')}-" @@ -165,6 +227,23 @@ def _fabricate_local_dist_for_build_argv( if remaining: (dist / remaining[0]).write_bytes(b"package") return + if args == ( + "uv", + "build", + "--package", + driver.SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, + "--wheel", + ): + helper_wheels = { + name + for name in driver._expected_local_dist_names(include_models=include_models) + if name.startswith("solstone_core_speakers_analyze-") + and name.endswith(".whl") + } + remaining = sorted(name for name in helper_wheels if not (dist / name).exists()) + if remaining: + (dist / remaining[0]).write_bytes(b"package") + return for name in names: (dist / name).write_bytes(b"package") @@ -480,7 +559,7 @@ def test_fake_all_host_candidate_and_recovery_are_deterministic( name.startswith("solstone_core-") and "manylinux2014_aarch64" in name for name in release_names ) - root_name, core_name = _macos_wheel_names() + root_name, core_name, speakers_analyze_name = _macos_wheel_names() with zipfile.ZipFile(first.release_dir / root_name) as wheel: assert wheel.read(PARAKEET_HELPER_MEMBER) == MACOS_HELPER with zipfile.ZipFile(first.release_dir / core_name) as wheel: @@ -490,6 +569,22 @@ def test_fake_all_host_candidate_and_recovery_are_deterministic( if Path(member.filename).name == "solstone-core" ) assert wheel.read(member) == MACOS_CORE + with zipfile.ZipFile(first.release_dir / speakers_analyze_name) as wheel: + script_member = next( + member + for member in wheel.infolist() + if member.filename.endswith(".data/scripts/solstone-core-speakers-analyze") + ) + dylib_member = next( + member + for member in wheel.infolist() + if member.filename.endswith( + ".data/data/lib/solstone-core-speakers-analyze/" + "libonnxruntime.1.25.0.dylib" + ) + ) + assert wheel.read(script_member) == MACOS_SPEAKERS_ANALYZE + assert wheel.read(dylib_member) == MACOS_ONNXRUNTIME recovered = _recover(first_root) assert recovered.heading == "retained-candidate-valid" @@ -1855,6 +1950,12 @@ def test_default_build_local_dist_uses_exact_linux_contract_and_scrubbed_env( expected_aarch64_env = _expected_scrubbed_env( tmp_path, driver.CORE_AARCH64_MATURIN_ARGS ) + expected_helper_x86_env = _expected_scrubbed_env( + tmp_path, driver.SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS + ) + expected_helper_aarch64_env = _expected_scrubbed_env( + tmp_path, driver.SPEAKERS_ANALYZE_AARCH64_MATURIN_ARGS + ) core_sdist_path = f"dist/solstone_core-{checker._current_version()}.tar.gz" assert calls == [ ( @@ -1885,6 +1986,44 @@ def test_default_build_local_dist_uses_exact_linux_contract_and_scrubbed_env( ("uv", "build", core_sdist_path, "--wheel", "--out-dir", "dist"), expected_aarch64_env, ), + ( + ( + "python3", + "scripts/stage_speakers_analyze_runtime.py", + "--target", + "linux-x86_64", + ), + _expected_scrubbed_env(tmp_path, ""), + ), + ( + ( + "uv", + "build", + "--package", + driver.SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, + "--wheel", + ), + expected_helper_x86_env, + ), + ( + ( + "python3", + "scripts/stage_speakers_analyze_runtime.py", + "--target", + "linux-aarch64", + ), + _expected_scrubbed_env(tmp_path, ""), + ), + ( + ( + "uv", + "build", + "--package", + driver.SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, + "--wheel", + ), + expected_helper_aarch64_env, + ), ] assert all("--exclude" not in argv for argv, _env in calls) assert [ @@ -1895,6 +2034,8 @@ def test_default_build_local_dist_uses_exact_linux_contract_and_scrubbed_env( assert [env["MATURIN_PEP517_ARGS"] for argv, env in calls if "--wheel" in argv] == [ driver.CORE_X86_64_MATURIN_ARGS, driver.CORE_AARCH64_MATURIN_ARGS, + driver.SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS, + driver.SPEAKERS_ANALYZE_AARCH64_MATURIN_ARGS, ] assert all("AMBIENT_RELEASE_TOKEN" not in env for _argv, env in calls) for _argv, env in calls: @@ -1953,6 +2094,32 @@ def test_default_build_local_dist_honors_include_models_build_selection( ("uv", "build", "--package", "solstone-core", "--sdist"), ("uv", "build", core_sdist_path, "--wheel", "--out-dir", "dist"), ("uv", "build", core_sdist_path, "--wheel", "--out-dir", "dist"), + ( + "python3", + "scripts/stage_speakers_analyze_runtime.py", + "--target", + "linux-x86_64", + ), + ( + "uv", + "build", + "--package", + driver.SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, + "--wheel", + ), + ( + "python3", + "scripts/stage_speakers_analyze_runtime.py", + "--target", + "linux-aarch64", + ), + ( + "uv", + "build", + "--package", + driver.SPEAKERS_ANALYZE_WORKSPACE_PACKAGE, + "--wheel", + ), ] assert all("--exclude" not in call for call in calls) assert {path.name for path in (tmp_path / "dist").iterdir()} == set( @@ -2376,6 +2543,11 @@ def test_fresh_cleanup_removes_nested_egg_infos_request_siblings_and_staging( / "packages" / "solstone-journal-models" / "solstone_journal_models.egg-info", + root + / "packages" + / driver.SPEAKERS_ANALYZE_WORKSPACE_PACKAGE + / "solstone_core_speakers_analyze.egg-info", + root / "packages" / driver.SPEAKERS_ANALYZE_WORKSPACE_PACKAGE / "wheel-data", root / "target" / "release-transfer" / f".{version}.request-abc123", root / "target" / "release-transfer" / f".{version}.source.bundle", root / "target" / "release-evidence" / f"{version}.staging", @@ -2446,6 +2618,33 @@ def test_linux_maturin_contract_rejects_missing_or_wrong_tokens(args: str) -> No assert failures +@pytest.mark.parametrize( + "args", + [ + driver.SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS.replace("--locked ", ""), + driver.SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS.replace("--zig ", ""), + driver.SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS.replace( + "--compatibility manylinux_2_27 ", "" + ), + driver.SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS.replace("--auditwheel skip ", ""), + driver.SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS.replace( + "--target x86_64-unknown-linux-gnu", "" + ), + driver.SPEAKERS_ANALYZE_X86_64_MATURIN_ARGS.replace( + "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl" + ), + ], +) +def test_speakers_analyze_linux_maturin_contract_rejects_missing_or_wrong_tokens( + args: str, +) -> None: + failures = driver.validate_speakers_analyze_linux_maturin_args( + args, + target="x86_64-unknown-linux-gnu", + ) + assert failures + + @pytest.mark.parametrize( "mutation", [ @@ -2475,7 +2674,7 @@ def test_candidate_revalidates_macos_wheel_bytes_after_copy_before_ledger( services = _services(root) def cleanup(paths: Sequence[Path]) -> None: - root_name, _core_name = _macos_wheel_names() + root_name, _core_name, _speakers_name = _macos_wheel_names() wheel_path = root / "dist" / root_name if wheel_path.exists(): write_platform_base_wheel( diff --git a/tests/test_release_install_smoke.py b/tests/test_release_install_smoke.py index b002368cd..f21ee203c 100644 --- a/tests/test_release_install_smoke.py +++ b/tests/test_release_install_smoke.py @@ -14,10 +14,16 @@ from typing import Any import pytest import scripts.check_rust_release_manifest as checker +import scripts.check_wheel_contents as wheel_checker import scripts.release_install_smoke as smoke from scripts.release_digest import candidate_digest, file_sha256_size from scripts.release_public_evidence import validate_public_evidence_tree -from tests.helpers.release_wheel_fixtures import ROOT_LAUNCHER_BYTES, record_hash +from tests.helpers.release_wheel_fixtures import ( + ROOT_LAUNCHER_BYTES, + record_hash, + speakers_analyze_elf, + write_speakers_analyze_wheel, +) SOURCE_COMMIT = "a" * 40 CORE_LOCK = "b" * 64 @@ -70,6 +76,23 @@ def _write_metadata_wheel(path: Path) -> None: wheel.writestr(info, content) +def _write_speakers_analyze_wheel(path: Path) -> None: + tag = path.name.removesuffix(".whl").split("-")[-1] + binary = None + if "manylinux" in tag: + machine = "aarch64" if "aarch64" in tag else "x86_64" + binary = speakers_analyze_elf(wheel_checker.ELF_MACHINE[machine]) + write_speakers_analyze_wheel( + path.parent, + tag=tag, + version=path.name.removesuffix(".whl").split("-")[1], + binary=binary, + library=b"fixture onnxruntime GLIBC_2.27\n", + license_notice=b"fixture license\n", + third_party_notice=b"fixture notices\n", + ) + + def _candidate(tmp_path: Path) -> tuple[Path, list[Path]]: candidate = tmp_path / "candidate" candidate.mkdir() @@ -78,13 +101,17 @@ def _candidate(tmp_path: Path) -> tuple[Path, list[Path]]: if name.endswith(".whl") and ( name.startswith("solstone-") or name.startswith("solstone_core-") + or name.startswith("solstone_core_speakers_analyze-") or name.startswith("solstone_journal-") or name.startswith("solstone_journal_cuda-") ): wanted.append(name) paths = [candidate / name for name in wanted] for path in paths: - _write_metadata_wheel(path) + if path.name.startswith("solstone_core_speakers_analyze-"): + _write_speakers_analyze_wheel(path) + else: + _write_metadata_wheel(path) return candidate, paths @@ -111,6 +138,13 @@ def _ledger_payload(digest: str, candidate: Path) -> dict: "macos_root_helper": { "member": {"path": "parakeet-helper", "sha256": "e" * 64, "bytes": 6} }, + "macos_speakers_analyze": { + "member": { + "path": "solstone-core-speakers-analyze", + "sha256": "f" * 64, + "bytes": 7, + } + }, }, "native_members": { "linux-x86_64-musl": _core_member_payload("linux-x86"), @@ -143,6 +177,24 @@ def _observation( (env_root / "bin" / "python").write_bytes(b"python") if macos: (env_root / "bin" / "parakeet-helper").write_bytes(b"helper") + helper_wheels = [ + path + for path in install_paths + if path.name.startswith("solstone_core_speakers_analyze-") + and "manylinux_2_27_x86_64" in path.name + ] + helper_bytes = b"" + if helper_wheels: + with zipfile.ZipFile(helper_wheels[0]) as wheel: + helper_member = next( + info + for info in wheel.infolist() + if info.filename.endswith( + ".data/scripts/solstone-core-speakers-analyze" + ) + ) + helper_bytes = wheel.read(helper_member) + (env_root / "bin" / "solstone-core-speakers-analyze").write_bytes(helper_bytes) members = [ { "name": name, @@ -170,6 +222,33 @@ def _observation( "symlink": False, } ) + if helper_wheels: + members.append( + { + "name": "solstone-core-speakers-analyze", + "path": env_root / "bin" / "solstone-core-speakers-analyze", + "sha256": file_sha256_size( + env_root / "bin" / "solstone-core-speakers-analyze" + )[0], + "symlink": False, + } + ) + smoke_results = { + name: smoke.CommandResult( + argv=(str(env_root / "bin" / name), "--version"), + exit_code=0, + stdout=f"{smoke.CORE_SMOKE_STDOUT[name]} 1.0.0", + env=smoke.SCRUBBED_COMMAND_ENV, + ) + for name in smoke.INSTALL_SCRIPT_NAMES + } + if helper_wheels: + smoke_results["solstone-core-speakers-analyze"] = smoke.CommandResult( + argv=(str(env_root / "bin" / "solstone-core-speakers-analyze"),), + exit_code=0, + stdout='{"schema":"solstone-speaker-analyze-response-v1"}', + env=smoke.SCRUBBED_COMMAND_ENV, + ) return smoke.InstallObservation( env_root=env_root, preexisting_distributions=(), @@ -189,15 +268,7 @@ def _observation( ), installed_distributions=smoke.expected_distribution_entries(install_paths), installed_members=tuple(members), - smoke={ - name: smoke.CommandResult( - argv=(str(env_root / "bin" / name), "--version"), - exit_code=0, - stdout=f"{smoke.CORE_SMOKE_STDOUT[name]} 1.0.0", - env=smoke.SCRUBBED_COMMAND_ENV, - ) - for name in smoke.INSTALL_SCRIPT_NAMES - }, + smoke=smoke_results, ) diff --git a/tests/test_release_ledger.py b/tests/test_release_ledger.py index 86e51a6cb..151dadc83 100644 --- a/tests/test_release_ledger.py +++ b/tests/test_release_ledger.py @@ -14,7 +14,12 @@ import scripts.check_rust_release_manifest as checker import scripts.release_ledger as ledger import scripts.release_tool_pins as pins from scripts.check_rust_release_manifest import canonical_json_bytes -from scripts.check_wheel_contents import CORE_SCRIPT_NAMES +from scripts.check_wheel_contents import ( + CORE_SCRIPT_NAMES, + SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR, + SPEAKERS_ANALYZE_SCRIPT_NAMES, + SPEAKERS_ANALYZE_TARGETS, +) from scripts.release_advisory_policy import PolicyRun SOURCE_COMMIT = "a" * 40 @@ -59,16 +64,16 @@ def _policy() -> PolicyRun: def _native(role: str, wheel_name: str, member_path: str) -> dict: - members = ( - { + if role == "root": + members = { "parakeet-helper": { "path": member_path, "sha256": "f" * 64, "bytes": 6, } } - if role == "root" - else { + elif role == "core": + members = { name: { "path": f"solstone_core-1.2.3.data/scripts/{name}", "sha256": "f" * 64, @@ -76,7 +81,23 @@ def _native(role: str, wheel_name: str, member_path: str) -> dict: } for name in CORE_SCRIPT_NAMES } - ) + else: + dylib_name = SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name + members = { + SPEAKERS_ANALYZE_SCRIPT_NAMES[0]: { + "path": member_path, + "sha256": "f" * 64, + "bytes": 6, + }, + dylib_name: { + "path": ( + "solstone_core_speakers_analyze-1.2.3.data/" + f"{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/{dylib_name}" + ), + "sha256": "f" * 64, + "bytes": 6, + }, + } return { "schema_version": 1, "kind": "macos-native-record/v1", @@ -120,6 +141,12 @@ def _native_records() -> list[dict]: for name in checker.expected_package_names(include_models=False) if name.startswith("solstone_core-") and "macosx_14_0_arm64" in name ) + speakers_wheel = next( + name + for name in checker.expected_package_names(include_models=False) + if name.startswith("solstone_core_speakers_analyze-") + and "macosx_14_0_arm64" in name + ) return [ _native( "root", @@ -131,6 +158,11 @@ def _native_records() -> list[dict]: core_wheel, "solstone_core-1.2.3.data/scripts/solstone-core", ), + _native( + "speakers-analyze", + speakers_wheel, + "solstone_core_speakers_analyze-1.2.3.data/scripts/solstone-core-speakers-analyze", + ), ] @@ -160,6 +192,27 @@ def _candidate(root: Path) -> Path: info.external_attr = 0o755 << 16 with zipfile.ZipFile(candidate / root_wheel, "w") as wheel: wheel.writestr(info, b"macos helper") + speakers_wheel = next( + name + for name in checker.expected_package_names(include_models=False) + if name.startswith("solstone_core_speakers_analyze-") + and "macosx_14_0_arm64" in name + ) + speakers_version = speakers_wheel.removesuffix(".whl").split("-")[1] + speakers_prefix = f"solstone_core_speakers_analyze-{speakers_version}.data" + dylib_name = SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name + with zipfile.ZipFile(candidate / speakers_wheel, "w") as wheel: + for member in ( + f"{speakers_prefix}/scripts/{SPEAKERS_ANALYZE_SCRIPT_NAMES[0]}", + ( + f"{speakers_prefix}/" + f"{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/{dylib_name}" + ), + ): + info = zipfile.ZipInfo(member) + info.create_system = 3 + info.external_attr = 0o755 << 16 + wheel.writestr(info, b"native") return candidate @@ -290,7 +343,7 @@ def test_ledger_rejects_raw_signer_team_and_uuid_evidence(tmp_path: Path) -> Non ) -def test_ledger_requires_exactly_two_native_records(tmp_path: Path) -> None: +def test_ledger_requires_exactly_three_native_records(tmp_path: Path) -> None: with pytest.raises(ledger.LedgerError) as exc: ledger.build_ledger( version="1.2.3", diff --git a/tests/test_release_native_records.py b/tests/test_release_native_records.py index fb9a1d22b..2918652bf 100644 --- a/tests/test_release_native_records.py +++ b/tests/test_release_native_records.py @@ -14,7 +14,13 @@ import pytest import scripts.record_macos_native_wheel as native import scripts.release_tool_pins as pins -from scripts.check_wheel_contents import CORE_SCRIPT_NAMES, PARAKEET_HELPER_MEMBER +from scripts.check_wheel_contents import ( + CORE_SCRIPT_NAMES, + PARAKEET_HELPER_MEMBER, + SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR, + SPEAKERS_ANALYZE_SCRIPT_NAMES, + SPEAKERS_ANALYZE_TARGETS, +) SOURCE_COMMIT = "a" * 40 CORE_LOCK = "b" * 64 @@ -45,6 +51,35 @@ def _core_wheel(tmp_path: Path, content: bytes = b"core-script") -> Path: return path +def _speakers_analyze_wheel( + tmp_path: Path, + *, + script: bytes = b"speakers-script", + dylib: bytes = b"onnxruntime-dylib", +) -> Path: + path = ( + tmp_path / "solstone_core_speakers_analyze-1.2.3-py3-none-macosx_14_0_arm64.whl" + ) + data_prefix = "solstone_core_speakers_analyze-1.2.3.data" + dylib_name = SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name + with zipfile.ZipFile(path, "w") as wheel: + for member, content in ( + ( + f"{data_prefix}/scripts/{SPEAKERS_ANALYZE_SCRIPT_NAMES[0]}", + script, + ), + ( + f"{data_prefix}/{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/{dylib_name}", + dylib, + ), + ): + info = zipfile.ZipInfo(member) + info.create_system = 3 + info.external_attr = 0o755 << 16 + wheel.writestr(info, content) + return path + + def _facts(content: bytes) -> dict: return { "signed_binary_sha256": hashlib.sha256(content).hexdigest(), @@ -72,6 +107,15 @@ def _core_facts(content: bytes) -> dict: return {"members": {name: _facts(content) for name in CORE_SCRIPT_NAMES}} +def _speakers_analyze_facts(script: bytes, dylib: bytes) -> dict: + return { + "members": { + SPEAKERS_ANALYZE_SCRIPT_NAMES[0]: _facts(script), + SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name: _facts(dylib), + } + } + + def test_native_record_cli_and_makefile_use_package_module() -> None: root = Path(__file__).resolve().parents[1] result = subprocess.run( @@ -84,15 +128,20 @@ def test_native_record_cli_and_makefile_use_package_module() -> None: assert result.returncode == 0, result.stderr makefile = (root / "Makefile").read_text(encoding="utf-8") - assert makefile.count("python3 -m scripts.record_macos_native_wheel") == 2 + assert makefile.count("python3 -m scripts.record_macos_native_wheel") == 3 assert "python3 scripts/record_macos_native_wheel.py" not in makefile -def test_exactly_two_role_records_are_written_and_not_interchangeable( +def test_exactly_three_role_records_are_written_and_not_interchangeable( tmp_path: Path, ) -> None: root_wheel = _root_wheel(tmp_path, b"root") core_wheel = _core_wheel(tmp_path, b"core") + speakers_wheel = _speakers_analyze_wheel( + tmp_path, + script=b"speakers", + dylib=b"dylib", + ) root = native.build_macos_native_record( role="root", @@ -108,8 +157,23 @@ def test_exactly_two_role_records_are_written_and_not_interchangeable( source_commit=SOURCE_COMMIT, core_lock_sha256=CORE_LOCK, ) + speakers = native.build_macos_native_record( + role="speakers-analyze", + wheel_path=speakers_wheel, + signing_facts=_speakers_analyze_facts(b"speakers", b"dylib"), + source_commit=SOURCE_COMMIT, + core_lock_sha256=CORE_LOCK, + ) - assert {root["role"], core["role"]} == {"root", "core"} + assert {root["role"], core["role"], speakers["role"]} == { + "root", + "core", + "speakers-analyze", + } + assert set(speakers["members"]) == { + SPEAKERS_ANALYZE_SCRIPT_NAMES[0], + SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name, + } assert native.validate_macos_native_record( root, role="core", @@ -124,6 +188,13 @@ def test_exactly_two_role_records_are_written_and_not_interchangeable( source_commit=SOURCE_COMMIT, core_lock_sha256=CORE_LOCK, ) + assert native.validate_macos_native_record( + speakers, + role="root", + wheel_path=root_wheel, + source_commit=SOURCE_COMMIT, + core_lock_sha256=CORE_LOCK, + ) def test_record_rejects_member_hash_signing_and_notary_mismatches( diff --git a/tests/test_release_proof_host.py b/tests/test_release_proof_host.py index a1b299c69..d3909e8e4 100644 --- a/tests/test_release_proof_host.py +++ b/tests/test_release_proof_host.py @@ -53,11 +53,25 @@ def _write_metadata_wheel(path: Path) -> None: for name, content in members.items() ) members[record_name] = f"{record}\n{record_name},,".encode("utf-8") + if path.name.startswith("solstone_core_speakers_analyze-"): + version = path.name.removesuffix(".whl").split("-")[1] + script_name = smoke.SPEAKERS_ANALYZE_SCRIPT_NAME + script_path = ( + f"solstone_core_speakers_analyze-{version}.data/scripts/{script_name}" + ) + members[script_path] = f"#!/bin/sh\necho {script_name}\n".encode("utf-8") + record_name = f"solstone_core_speakers_analyze-{version}.dist-info/RECORD" + record = "\n".join( + f"{name},{record_hash(content)},{len(content)}" + for name, content in members.items() + ) + members[record_name] = f"{record}\n{record_name},,".encode("utf-8") for name, content in members.items(): info = zipfile.ZipInfo(name) info.external_attr = ( 0o755 << 16 - if Path(name).name in smoke.ROOT_LAUNCHER_NAMES + if Path(name).name + in (*smoke.ROOT_LAUNCHER_NAMES, smoke.SPEAKERS_ANALYZE_SCRIPT_NAME) else 0o644 << 16 ) wheel.writestr(info, content) @@ -70,6 +84,7 @@ def _candidate(tmp_path: Path) -> Path: if name.endswith(".whl") and ( name.startswith("solstone-") or name.startswith("solstone_core-") + or name.startswith("solstone_core_speakers_analyze-") or name.startswith("solstone_journal-") or name.startswith("solstone_journal_cuda-") ): @@ -134,6 +149,11 @@ def _observation( (env_root / "bin" / name).write_bytes(content) for name in smoke.CORE_SCRIPT_NAMES: (env_root / "bin" / name).write_bytes(b"core") + if target in smoke.SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS: + (env_root / "bin" / smoke.SPEAKERS_ANALYZE_SCRIPT_NAME).write_text( + smoke.SPEAKERS_ANALYZE_SCRIPT_NAME, + encoding="utf-8", + ) install_paths = smoke.target_install_paths_from_ledger( ledger, target=target, @@ -157,6 +177,25 @@ def _observation( ] if target == "macos-arm64": (env_root / "bin" / "parakeet-helper").write_bytes(b"helper") + smoke_results = { + name: smoke.CommandResult( + argv=(str(env_root / "bin" / name), "--version"), + exit_code=0, + stdout=f"{smoke.CORE_SMOKE_STDOUT[name]} {version}", + env=smoke.SCRUBBED_COMMAND_ENV, + ) + for name in smoke.INSTALL_SCRIPT_NAMES + } + if target in smoke.SPEAKERS_ANALYZE_REAL_INFERENCE_TARGETS: + smoke_results[smoke.SPEAKERS_ANALYZE_SCRIPT_NAME] = smoke.CommandResult( + argv=(str(env_root / "bin" / smoke.SPEAKERS_ANALYZE_SCRIPT_NAME),), + exit_code=0, + stdout=json.dumps( + {"schema": smoke.SPEAKERS_ANALYZE_RESPONSE_SCHEMA}, + separators=(",", ":"), + ), + env=smoke.SCRUBBED_COMMAND_ENV, + ) payload: dict[str, Any] = { "install": smoke.CommandResult( argv=( @@ -174,15 +213,7 @@ def _observation( ), "installed_distributions": smoke.expected_distribution_entries(install_paths), "installed_members": tuple(members), - "smoke": { - name: smoke.CommandResult( - argv=(str(env_root / "bin" / name), "--version"), - exit_code=0, - stdout=f"{smoke.CORE_SMOKE_STDOUT[name]} {version}", - env=smoke.SCRUBBED_COMMAND_ENV, - ) - for name in smoke.INSTALL_SCRIPT_NAMES - }, + "smoke": smoke_results, } if mutate is not None: mutate(payload) diff --git a/tests/test_release_publish.py b/tests/test_release_publish.py index c7a087949..6d44774a8 100644 --- a/tests/test_release_publish.py +++ b/tests/test_release_publish.py @@ -7,6 +7,7 @@ import hashlib import json import logging import subprocess +import sys from collections.abc import Callable, Mapping, Sequence from dataclasses import replace from pathlib import Path @@ -15,6 +16,7 @@ from typing import Any import pytest import scripts.check_rust_release_manifest as manifest +import scripts.check_wheel_contents as wheel_checker import scripts.release_publish as publisher from scripts.release_candidate_driver import ( CandidateReport, @@ -24,6 +26,13 @@ from scripts.release_candidate_driver import ( run_candidate as run_release_candidate, ) from scripts.transparency_core import failure +from tests.helpers.release_candidate_fixtures import ( + MACOS_ONNXRUNTIME, + SPEAKERS_ANALYZE_LICENSE_BYTES, + SPEAKERS_ANALYZE_RUNTIME_BYTES, + SPEAKERS_ANALYZE_THIRD_PARTY_NOTICE_BYTES, + write_core_unsupported_tombstone_record, +) from tests.helpers.release_candidate_fixtures import ( env as real_candidate_env, ) @@ -33,9 +42,6 @@ from tests.helpers.release_candidate_fixtures import ( from tests.helpers.release_candidate_fixtures import ( services as real_candidate_services, ) -from tests.helpers.release_candidate_fixtures import ( - write_core_unsupported_tombstone_record, -) SOURCE_COMMIT = "0123456789abcdef0123456789abcdef01234567" OTHER_COMMIT = "fedcba9876543210fedcba9876543210fedcba98" @@ -47,6 +53,48 @@ PROOF_TARGETS = ( ) +@pytest.fixture(autouse=True) +def _patch_speakers_analyze_fixture_hashes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + patched_targets = {} + for target, spec in tuple(wheel_checker.SPEAKERS_ANALYZE_TARGETS.items()): + runtime = ( + MACOS_ONNXRUNTIME + if target == "macos-arm64" + else SPEAKERS_ANALYZE_RUNTIME_BYTES + ) + notices = ( + replace( + spec.notices[0], + sha256=hashlib.sha256(SPEAKERS_ANALYZE_LICENSE_BYTES).hexdigest(), + ), + replace( + spec.notices[1], + sha256=hashlib.sha256( + SPEAKERS_ANALYZE_THIRD_PARTY_NOTICE_BYTES + ).hexdigest(), + ), + ) + patched_targets[target] = replace( + spec, + runtime_sha256=hashlib.sha256(runtime).hexdigest(), + notices=notices, + ) + for module in ( + wheel_checker, + sys.modules.get("check_wheel_contents"), + ): + if module is None: + continue + for target, spec in patched_targets.items(): + monkeypatch.setitem( + module.SPEAKERS_ANALYZE_TARGETS, + target, + spec, + ) + + def _sha(path: Path) -> tuple[str, int]: data = path.read_bytes() return hashlib.sha256(data).hexdigest(), len(data) diff --git a/tests/test_render_packaging.py b/tests/test_render_packaging.py index 3b10a4077..fcedd3d74 100644 --- a/tests/test_render_packaging.py +++ b/tests/test_render_packaging.py @@ -77,6 +77,7 @@ def test_live_lock_authorities_match_root_project_version() -> None: uv_workspace_names = { "solstone", "solstone-core", + "solstone-core-speakers-analyze", "solstone-journal", "solstone-journal-cuda", } @@ -159,6 +160,25 @@ def _fixture_root(tmp_path: Path, *, root_version: str = "1.2.3") -> Path: strip = true """, ) + _write( + tmp_path / "packages" / "solstone-core-speakers-analyze" / "pyproject.toml", + """ + [build-system] + requires = ["maturin==1.14.1"] + build-backend = "maturin" + + [project] + name = "solstone-core-speakers-analyze" + version = "0.0.1" + + [tool.maturin] + bindings = "bin" + manifest-path = "../../core/crates/solstone-core-speakers-analyze/Cargo.toml" + profile = "release" + strip = true + data = "wheel-data" + """, + ) _write( tmp_path / "scripts" @@ -290,6 +310,11 @@ def test_render_updates_python_leaves_and_cargo_lockstep(tmp_path: Path) -> None core_leaf = rendered[root / "packages" / "solstone-core" / "pyproject.toml"] assert 'version = "2.3.4"' in core_leaf assert "solstone[journal-host]==" not in core_leaf + speakers_analyze_leaf = rendered[ + root / "packages" / "solstone-core-speakers-analyze" / "pyproject.toml" + ] + assert 'version = "2.3.4"' in speakers_analyze_leaf + assert "solstone[journal-host]==" not in speakers_analyze_leaf root_pyproject = rendered[root / "pyproject.toml"] for marker in SOLSTONE_CORE_PLATFORM_MARKERS: assert f'"solstone-core==2.3.4; {marker}"' in root_pyproject @@ -318,6 +343,7 @@ def test_check_reports_synthetic_packaging_drift( assert "packaging metadata is stale" in out assert "drifted: pyproject.toml" in out assert "drifted: packages/solstone-core/pyproject.toml" in out + assert "drifted: packages/solstone-core-speakers-analyze/pyproject.toml" in out assert ( "drifted: scripts/solstone-core-unsupported-platform-tombstone/setup.py" in out ) diff --git a/tests/test_rust_policy_baseline.py b/tests/test_rust_policy_baseline.py index e2b65cb4b..02b9e4b5c 100644 --- a/tests/test_rust_policy_baseline.py +++ b/tests/test_rust_policy_baseline.py @@ -10,6 +10,7 @@ from typing import Any ROOT = Path(__file__).resolve().parents[1] EXPECTED_GRAPH_TARGETS = { "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", "aarch64-apple-darwin", diff --git a/tests/test_solstone_core_platforms.py b/tests/test_solstone_core_platforms.py index f6af7d4be..09d5c33d6 100644 --- a/tests/test_solstone_core_platforms.py +++ b/tests/test_solstone_core_platforms.py @@ -9,9 +9,12 @@ from pathlib import Path from packaging.markers import Marker +import solstone.think.probe as probe from solstone.think.probe import ( SOLSTONE_CORE_COVERED_PLATFORMS, SOLSTONE_CORE_PLATFORM_MARKERS, + SOLSTONE_CORE_SPEAKERS_ANALYZE_COVERED_PLATFORMS, + SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS, SOLSTONE_CORE_UNSUPPORTED_PLATFORM_MARKER, is_solstone_core_covered_platform, ) @@ -85,3 +88,20 @@ def test_core_pin_markers_match_probe_covered_platforms() -> None: "platform_machine": machine, } ) != is_solstone_core_covered_platform(system, machine) + + +def test_speakers_analyze_platform_tags_are_probe_declared_once() -> None: + assert SOLSTONE_CORE_SPEAKERS_ANALYZE_COVERED_PLATFORMS == ( + ("linux", "x86_64"), + ("linux", "aarch64"), + ("darwin", "arm64"), + ) + assert SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS == { + ("linux", "x86_64"): "manylinux_2_27_x86_64", + ("linux", "aarch64"): "manylinux_2_27_aarch64", + ("darwin", "arm64"): "macosx_14_0_arm64", + } + assert not hasattr(probe, "SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_MARKERS") + assert not hasattr( + probe, "SOLSTONE_CORE_SPEAKERS_ANALYZE_UNSUPPORTED_PLATFORM_MARKER" + ) diff --git a/tests/test_speakers_analyze_wheel_integration.py b/tests/test_speakers_analyze_wheel_integration.py new file mode 100644 index 000000000..e73f519b5 --- /dev/null +++ b/tests/test_speakers_analyze_wheel_integration.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +import json +import subprocess +import venv +from pathlib import Path + +import pytest + +import scripts.release_install_smoke as smoke + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.mark.integration +@pytest.mark.timeout(300) +def test_speakers_analyze_wheel_installs_and_runs_real_inference( + tmp_path: Path, +) -> None: + build = subprocess.run( + ["make", "wheel-speakers-analyze-linux-x86_64"], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + timeout=300, + ) + assert build.returncode == 0, build.stderr or build.stdout + wheels = sorted(ROOT.glob("dist/solstone_core_speakers_analyze-*.whl")) + assert wheels + wheel = wheels[-1] + + env_root = tmp_path / "venv" + venv.EnvBuilder(with_pip=True, symlinks=False).create(env_root) + python = env_root / "bin" / "python" + install = subprocess.run( + [ + str(python), + "-m", + "pip", + "install", + "--no-index", + "--no-deps", + str(wheel), + ], + capture_output=True, + text=True, + check=False, + timeout=120, + ) + assert install.returncode == 0, install.stderr or install.stdout + + executable = env_root / "bin" / "solstone-core-speakers-analyze" + run = subprocess.run( + [str(executable)], + input=smoke._speakers_analyze_request(env_root), + capture_output=True, + text=True, + check=False, + timeout=120, + ) + assert run.returncode == 0, run.stderr or run.stdout + response = json.loads(run.stdout) + assert response["schema"] == smoke.SPEAKERS_ANALYZE_RESPONSE_SCHEMA diff --git a/tests/test_stage_speakers_analyze_runtime.py b/tests/test_stage_speakers_analyze_runtime.py new file mode 100644 index 000000000..764f8b6d2 --- /dev/null +++ b/tests/test_stage_speakers_analyze_runtime.py @@ -0,0 +1,186 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +import hashlib +import json +import zipfile +from pathlib import Path + +import pytest + +import scripts.stage_speakers_analyze_runtime as stage + +RUNTIME_MEMBER = "onnxruntime/capi/libonnxruntime.so.1.25.0" +LICENSE_MEMBER = "onnxruntime/LICENSE" +THIRD_PARTY_MEMBER = "onnxruntime/ThirdPartyNotices.txt" +RUNTIME_BYTES = b"fixture onnxruntime\n" +LICENSE_BYTES = b"fixture license\n" +THIRD_PARTY_BYTES = b"fixture third party notices\n" + + +def _sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _write_wheel(path: Path, members: dict[str, bytes]) -> str: + path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(path, "w") as wheel: + for name, content in members.items(): + wheel.writestr(name, content) + return stage._sha256_file(path) + + +def _spec(tmp_path: Path, *, wheel_sha256: str) -> stage.TargetSpec: + return stage.TargetSpec( + key="fixture-linux", + wheel_url=f"https://example.invalid/{tmp_path.name}/onnxruntime-fixture.whl", + wheel_sha256=wheel_sha256, + runtime_member=RUNTIME_MEMBER, + runtime_sha256=_sha256(RUNTIME_BYTES), + runtime_staged_name="libonnxruntime.so.1", + link_names=("libonnxruntime.so.1", "libonnxruntime.so"), + notices=( + stage.NoticeSpec( + source_member=LICENSE_MEMBER, + staged_name="onnxruntime-LICENSE.txt", + sha256=_sha256(LICENSE_BYTES), + ), + stage.NoticeSpec( + source_member=THIRD_PARTY_MEMBER, + staged_name="onnxruntime-ThirdPartyNotices.txt", + sha256=_sha256(THIRD_PARTY_BYTES), + ), + ), + ) + + +def _stage( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + *, + members: dict[str, bytes] | None = None, + wheel_sha256: str | None = None, +) -> dict[str, object]: + cache_dir = tmp_path / "cache" + wheel_path = cache_dir / "onnxruntime-fixture.whl" + actual_wheel_sha = _write_wheel( + wheel_path, + members + or { + RUNTIME_MEMBER: RUNTIME_BYTES, + LICENSE_MEMBER: LICENSE_BYTES, + THIRD_PARTY_MEMBER: THIRD_PARTY_BYTES, + }, + ) + monkeypatch.setattr(stage, "_assert_lock_contains", lambda _spec: None) + spec = _spec(tmp_path, wheel_sha256=wheel_sha256 or actual_wheel_sha) + return stage.stage_runtime( + spec=spec, + package_dir=tmp_path / "package", + cache_dir=cache_dir, + link_root=tmp_path / "link", + receipt_path=tmp_path / "receipt.json", + offline=True, + ) + + +def test_stage_runtime_stages_minimal_library_notices_and_receipt( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + receipt = _stage(tmp_path, monkeypatch) + + runtime = ( + tmp_path + / "package" + / "wheel-data" + / stage.RUNTIME_INSTALL_DIR + / "libonnxruntime.so.1" + ) + license_notice = ( + tmp_path + / "package" + / "wheel-data" + / stage.NOTICE_INSTALL_DIR + / "onnxruntime-LICENSE.txt" + ) + third_party_notice = ( + tmp_path + / "package" + / "wheel-data" + / stage.NOTICE_INSTALL_DIR + / "onnxruntime-ThirdPartyNotices.txt" + ) + + assert runtime.read_bytes() == RUNTIME_BYTES + assert license_notice.read_bytes() == LICENSE_BYTES + assert third_party_notice.read_bytes() == THIRD_PARTY_BYTES + assert (tmp_path / "link" / "fixture-linux" / "libonnxruntime.so.1").exists() + assert receipt["runtime_library"]["sha256"] == _sha256(RUNTIME_BYTES) + recorded = json.loads((tmp_path / "receipt.json").read_text(encoding="utf-8")) + assert recorded == receipt + + +def test_stage_runtime_fails_loudly_on_whole_wheel_digest_mismatch( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + with pytest.raises(stage.StageError) as exc: + _stage(tmp_path, monkeypatch, wheel_sha256="0" * 64) + + assert "cached onnxruntime wheel digest mismatch" in str(exc.value) + assert ( + "expected: 0000000000000000000000000000000000000000000000000000000000000000" + in str(exc.value) + ) + assert "repair:" in str(exc.value) + + +def test_stage_runtime_fails_loudly_on_missing_expected_member( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + members = { + LICENSE_MEMBER: LICENSE_BYTES, + THIRD_PARTY_MEMBER: THIRD_PARTY_BYTES, + } + + with pytest.raises(stage.StageError) as exc: + _stage(tmp_path, monkeypatch, members=members) + + assert "onnxruntime wheel missing expected member" in str(exc.value) + assert f"expected: {RUNTIME_MEMBER}" in str(exc.value) + + +def test_stage_runtime_rejects_gpu_provider_members( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + members = { + RUNTIME_MEMBER: RUNTIME_BYTES, + LICENSE_MEMBER: LICENSE_BYTES, + THIRD_PARTY_MEMBER: THIRD_PARTY_BYTES, + "onnxruntime/capi/libonnxruntime_providers_cuda.so": b"cuda", + } + + with pytest.raises(stage.StageError) as exc: + _stage(tmp_path, monkeypatch, members=members) + + assert "forbidden GPU provider members" in str(exc.value) + assert "libonnxruntime_providers_cuda.so" in str(exc.value) + + +def test_stage_runtime_fails_loudly_on_notice_hash_drift( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + members = { + RUNTIME_MEMBER: RUNTIME_BYTES, + LICENSE_MEMBER: b"changed license\n", + THIRD_PARTY_MEMBER: THIRD_PARTY_BYTES, + } + + with pytest.raises(stage.StageError) as exc: + _stage(tmp_path, monkeypatch, members=members) + + assert "extracted onnxruntime notice digest mismatch" in str(exc.value) + assert f"expected: {LICENSE_MEMBER} sha256 {_sha256(LICENSE_BYTES)}" in str( + exc.value + )