personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / scripts / check_release_preflight.py
26 kB 838 lines
1#!/usr/bin/env python3 2# SPDX-License-Identifier: AGPL-3.0-only 3# Copyright (c) 2026 sol pbc 4 5"""Release environment checks for the Rust toolchain and wheel rail.""" 6 7from __future__ import annotations 8 9import argparse 10import os 11import shutil 12import subprocess 13import sys 14import tomllib 15from dataclasses import dataclass 16from pathlib import Path 17from typing import Callable, Literal, Mapping, Sequence 18 19ROOT = Path(__file__).resolve().parent.parent 20_SCRIPTS_DIR = Path(__file__).resolve().parent 21for _path in (str(ROOT), str(_SCRIPTS_DIR)): 22 if _path not in sys.path: 23 sys.path.insert(0, _path) 24 25from scripts.release_tool_pins import ( # noqa: E402 26 CARGO_DENY_PIN, 27 CARGO_DENY_VERSION, 28 CARGO_VERSION_PIN, 29 MACOS_CODESIGN_PATH, 30 MACOS_CODESIGN_PUBLIC_PIN, 31 MACOS_NOTARYTOOL_PIN, 32 MACOS_SIGNING_MODE, 33 MACOS_SWIFT_PIN, 34 MACOS_XCODE_BUILD, 35 MACOS_XCODE_PIN, 36 MACOS_XCODE_VERSION, 37 MATURIN_PIN, 38 PYTHON_MACOS_VERSION, 39 PYTHON_SOURCE_LINUX_VERSION, 40 RUSTC_VERSION_BANNER, 41 UV_PIN, 42 ZIG_PIN, 43 ZIG_VERSION, 44 tool_value_matches_pin, 45) 46 47TOOLCHAIN_FILE = "rust-toolchain.toml" 48COMPONENT_BINARIES = { 49 "rustfmt": "rustfmt", 50 "clippy": "cargo-clippy", 51} 52LaneName = Literal[ 53 "source", 54 "linux-x86_64-musl", 55 "linux-aarch64-musl", 56 "macos-arm64", 57] 58LANE_TOOL_KEYS: dict[LaneName, tuple[str, ...]] = { 59 "source": ("python", "rustc", "cargo", "uv", "maturin", "cargo-deny"), 60 "linux-x86_64-musl": ( 61 "python", 62 "rustc", 63 "cargo", 64 "uv", 65 "maturin", 66 "cargo-deny", 67 "zig", 68 ), 69 "linux-aarch64-musl": ( 70 "python", 71 "rustc", 72 "cargo", 73 "uv", 74 "maturin", 75 "cargo-deny", 76 "zig", 77 ), 78 "macos-arm64": ( 79 "python", 80 "rustc", 81 "cargo", 82 "uv", 83 "maturin", 84 "cargo-deny", 85 "xcode", 86 "swift", 87 "codesign", 88 "notarytool", 89 "signing_mode", 90 ), 91} 92PRESIGN_LANE_TOOL_KEYS: dict[LaneName, tuple[str, ...]] = { 93 **{lane: keys for lane, keys in LANE_TOOL_KEYS.items() if lane != "macos-arm64"}, 94 "macos-arm64": tuple( 95 key for key in LANE_TOOL_KEYS["macos-arm64"] if key != "signing_mode" 96 ), 97} 98 99 100@dataclass(frozen=True) 101class ToolchainSpec: 102 channel: str 103 components: tuple[str, ...] 104 targets: tuple[str, ...] 105 profile: str | None 106 107 108@dataclass(frozen=True) 109class Failure: 110 error: str 111 expected: str 112 actual: str 113 repair: str 114 115 116Runner = Callable[..., subprocess.CompletedProcess[str]] 117 118 119def _format_failures(failures: Sequence[Failure]) -> None: 120 for failure in failures: 121 print(f"ERROR: {failure.error}", file=sys.stderr) 122 print(f" expected: {failure.expected}", file=sys.stderr) 123 print(f" actual: {failure.actual}", file=sys.stderr) 124 print(f" repair command: {failure.repair}", file=sys.stderr) 125 126 127def load_toolchain_spec(root: Path) -> ToolchainSpec: 128 path = root / TOOLCHAIN_FILE 129 data = tomllib.loads(path.read_text(encoding="utf-8")) 130 toolchain = data.get("toolchain", {}) 131 channel = toolchain.get("channel") 132 components = toolchain.get("components", []) 133 targets = toolchain.get("targets", []) 134 profile = toolchain.get("profile") 135 if not isinstance(channel, str) or not channel: 136 raise ValueError(f"{TOOLCHAIN_FILE} must set [toolchain].channel") 137 if not isinstance(components, list) or not all( 138 isinstance(item, str) for item in components 139 ): 140 raise ValueError(f"{TOOLCHAIN_FILE} [toolchain].components must be strings") 141 if not isinstance(targets, list) or not all( 142 isinstance(item, str) for item in targets 143 ): 144 raise ValueError(f"{TOOLCHAIN_FILE} [toolchain].targets must be strings") 145 if profile is not None and not isinstance(profile, str): 146 raise ValueError(f"{TOOLCHAIN_FILE} [toolchain].profile must be a string") 147 return ToolchainSpec( 148 channel=channel, 149 components=tuple(components), 150 targets=tuple(targets), 151 profile=profile, 152 ) 153 154 155def check_rustup_override( 156 expected: str, 157 env: Mapping[str, str], 158) -> list[Failure]: 159 actual = env.get("RUSTUP_TOOLCHAIN") 160 if actual is None or actual == expected: 161 return [] 162 return [ 163 Failure( 164 error="RUSTUP_TOOLCHAIN overrides the pinned release toolchain", 165 expected=f"RUSTUP_TOOLCHAIN unset or {expected}", 166 actual=actual, 167 repair=f"unset RUSTUP_TOOLCHAIN || export RUSTUP_TOOLCHAIN={expected}", 168 ) 169 ] 170 171 172def rustup_home(env: Mapping[str, str]) -> Path: 173 value = env.get("RUSTUP_HOME") 174 if value: 175 return Path(value).expanduser() 176 return Path.home() / ".rustup" 177 178 179def find_installed_toolchain(expected: str, rustup_home_path: Path) -> Path | None: 180 toolchains = rustup_home_path / "toolchains" 181 if not toolchains.is_dir(): 182 return None 183 candidates = sorted( 184 path 185 for path in toolchains.iterdir() 186 if path.is_dir() 187 and (path.name == expected or path.name.startswith(f"{expected}-")) 188 and (path / "bin" / "rustc").is_file() 189 ) 190 return candidates[0] if candidates else None 191 192 193def check_toolchain_installed( 194 expected: str, 195 rustup_home_path: Path, 196) -> tuple[Path | None, list[Failure]]: 197 toolchain_dir = find_installed_toolchain(expected, rustup_home_path) 198 if toolchain_dir is not None: 199 return toolchain_dir, [] 200 return ( 201 None, 202 [ 203 Failure( 204 error="required Rust toolchain is not installed", 205 expected=f"installed rustup toolchain {expected}", 206 actual=f"no {expected}-* directory with bin/rustc under {rustup_home_path / 'toolchains'}", 207 repair=f"rustup toolchain install {expected}", 208 ) 209 ], 210 ) 211 212 213def check_rustc_version( 214 toolchain_dir: Path, 215 expected: str, 216 *, 217 runner: Runner = subprocess.run, 218) -> list[Failure]: 219 rustc = toolchain_dir / "bin" / "rustc" 220 result = runner( 221 [str(rustc), "--version"], 222 capture_output=True, 223 text=True, 224 check=False, 225 ) 226 actual = ( 227 result.stdout.strip() or result.stderr.strip() or f"exit {result.returncode}" 228 ) 229 if result.returncode != 0 or not actual.startswith(f"rustc {expected} "): 230 return [ 231 Failure( 232 error="rustc version does not match the pinned release toolchain", 233 expected=f"rustc {expected}", 234 actual=actual, 235 repair=f"rustup toolchain install {expected}", 236 ) 237 ] 238 return [] 239 240 241def check_toolchain_components( 242 toolchain_dir: Path, 243 channel: str, 244 components: Sequence[str], 245) -> list[Failure]: 246 failures: list[Failure] = [] 247 for component in components: 248 binary = COMPONENT_BINARIES.get(component) 249 if binary is None: 250 failures.append( 251 Failure( 252 error="release toolchain component has no filesystem check", 253 expected=f"known component in {sorted(COMPONENT_BINARIES)}", 254 actual=component, 255 repair=f"rustup component add --toolchain {channel} {component}", 256 ) 257 ) 258 continue 259 path = toolchain_dir / "bin" / binary 260 if path.is_file(): 261 continue 262 failures.append( 263 Failure( 264 error="release toolchain component is not installed", 265 expected=f"{component} component marker {path}", 266 actual="missing", 267 repair=f"rustup component add --toolchain {channel} {component}", 268 ) 269 ) 270 return failures 271 272 273def check_toolchain_targets( 274 toolchain_dir: Path, 275 channel: str, 276 targets: Sequence[str], 277) -> list[Failure]: 278 failures: list[Failure] = [] 279 for target in targets: 280 path = toolchain_dir / "lib" / "rustlib" / target / "lib" 281 if path.is_dir(): 282 continue 283 failures.append( 284 Failure( 285 error="release toolchain target is not installed", 286 expected=f"target stdlib directory {path}", 287 actual="missing", 288 repair=f"rustup target add --toolchain {channel} {target}", 289 ) 290 ) 291 return failures 292 293 294def check_zig( 295 expected: str = ZIG_VERSION, 296 *, 297 which: Callable[[str], str | None] = shutil.which, 298 runner: Runner = subprocess.run, 299) -> list[Failure]: 300 zig = which("zig") 301 if zig is None: 302 return [ 303 Failure( 304 error="zig is not on PATH", 305 expected=f"zig {expected}", 306 actual="not found", 307 repair=f"python3 -m pip install --user ziglang=={expected}", 308 ) 309 ] 310 result = runner( 311 [zig, "version"], 312 capture_output=True, 313 text=True, 314 check=False, 315 ) 316 actual = ( 317 result.stdout.strip() or result.stderr.strip() or f"exit {result.returncode}" 318 ) 319 if result.returncode != 0 or actual != expected: 320 return [ 321 Failure( 322 error="zig version does not match the supported wheel rail", 323 expected=expected, 324 actual=actual, 325 repair=f"python3 -m pip install --user ziglang=={expected}", 326 ) 327 ] 328 return [] 329 330 331def check_cargo_deny( 332 expected: str = CARGO_DENY_VERSION, 333 *, 334 which: Callable[[str], str | None] = shutil.which, 335 runner: Runner = subprocess.run, 336) -> list[Failure]: 337 cargo_deny = which("cargo-deny") 338 repair = f"cargo install cargo-deny@{expected} --locked --force" 339 if cargo_deny is None: 340 return [ 341 Failure( 342 error="cargo-deny is not on PATH", 343 expected=expected, 344 actual="not found", 345 repair=repair, 346 ) 347 ] 348 result = runner( 349 [cargo_deny, "--version"], 350 capture_output=True, 351 text=True, 352 check=False, 353 ) 354 actual = ( 355 result.stdout.strip() or result.stderr.strip() or f"exit {result.returncode}" 356 ) 357 parts = actual.split() 358 if ( 359 result.returncode != 0 360 or len(parts) < 2 361 or parts[0] != "cargo-deny" 362 or parts[1] != expected 363 ): 364 return [ 365 Failure( 366 error="cargo-deny version does not match the Rust dependency policy baseline", 367 expected=expected, 368 actual=actual, 369 repair=repair, 370 ) 371 ] 372 return [] 373 374 375def _expected_lane_tool_evidence( 376 lane: LaneName, 377 *, 378 keys_by_lane: Mapping[LaneName, tuple[str, ...]], 379) -> dict[str, str]: 380 if lane not in keys_by_lane: 381 raise ValueError(f"unknown release lane: {lane}") 382 common = { 383 "python": PYTHON_MACOS_VERSION 384 if lane == "macos-arm64" 385 else PYTHON_SOURCE_LINUX_VERSION, 386 "rustc": RUSTC_VERSION_BANNER, 387 "cargo": CARGO_VERSION_PIN, 388 "uv": UV_PIN, 389 "maturin": MATURIN_PIN, 390 "cargo-deny": CARGO_DENY_PIN, 391 } 392 if lane in {"linux-x86_64-musl", "linux-aarch64-musl"}: 393 common["zig"] = ZIG_PIN 394 if lane == "macos-arm64": 395 common.update( 396 { 397 "xcode": MACOS_XCODE_PIN, 398 "swift": MACOS_SWIFT_PIN, 399 "codesign": MACOS_CODESIGN_PUBLIC_PIN, 400 "notarytool": MACOS_NOTARYTOOL_PIN, 401 "signing_mode": MACOS_SIGNING_MODE, 402 } 403 ) 404 return {key: common[key] for key in keys_by_lane[lane]} 405 406 407def expected_lane_tool_evidence(lane: LaneName) -> dict[str, str]: 408 return _expected_lane_tool_evidence(lane, keys_by_lane=LANE_TOOL_KEYS) 409 410 411def expected_presign_lane_tool_evidence(lane: LaneName) -> dict[str, str]: 412 return _expected_lane_tool_evidence(lane, keys_by_lane=PRESIGN_LANE_TOOL_KEYS) 413 414 415def check_lane_tool_evidence( 416 lane: LaneName, 417 evidence: Mapping[str, str], 418) -> list[Failure]: 419 expected = expected_lane_tool_evidence(lane) 420 failures: list[Failure] = [] 421 if set(evidence) != set(expected): 422 failures.append( 423 Failure( 424 error="release lane tool evidence keys do not match lane", 425 expected=", ".join(expected), 426 actual=", ".join(sorted(evidence)) or "<empty>", 427 repair=f"python3 scripts/check_release_preflight.py lane-tools --lane {lane}", 428 ) 429 ) 430 for key, expected_value in expected.items(): 431 actual = evidence.get(key) 432 if not tool_value_matches_pin(key, expected_value, actual): 433 failures.append( 434 Failure( 435 error=f"release lane tool {key} is not pinned", 436 expected=expected_value, 437 actual=str(actual), 438 repair=f"python3 scripts/check_release_preflight.py lane-tools --lane {lane}", 439 ) 440 ) 441 return failures 442 443 444def check_presign_lane_tool_evidence( 445 lane: LaneName, 446 evidence: Mapping[str, str], 447) -> list[Failure]: 448 expected = expected_presign_lane_tool_evidence(lane) 449 failures: list[Failure] = [] 450 if set(evidence) != set(expected): 451 failures.append( 452 Failure( 453 error="pre-sign lane tool evidence keys do not match lane", 454 expected=", ".join(expected), 455 actual=", ".join(sorted(evidence)) or "<empty>", 456 repair=f"python3 scripts/check_release_preflight.py lane-tools --lane {lane}", 457 ) 458 ) 459 for key, expected_value in expected.items(): 460 actual = evidence.get(key) 461 if not tool_value_matches_pin(key, expected_value, actual): 462 failures.append( 463 Failure( 464 error=f"pre-sign lane tool {key} is not pinned", 465 expected=expected_value, 466 actual=str(actual), 467 repair=f"python3 scripts/check_release_preflight.py lane-tools --lane {lane}", 468 ) 469 ) 470 return failures 471 472 473def finalize_macos_tool_evidence( 474 preflight_evidence: Mapping[str, str], 475 native_records: Sequence[Mapping[str, object]], 476) -> tuple[dict[str, str] | None, list[Failure]]: 477 failures = check_presign_lane_tool_evidence("macos-arm64", preflight_evidence) 478 roles = { 479 str(record.get("role")): record 480 for record in native_records 481 if isinstance(record, Mapping) 482 } 483 if set(roles) != {"root", "core", "speakers-analyze"}: 484 failures.append( 485 Failure( 486 error="macOS signed tool finalizer requires all native records", 487 expected="root, core, and speakers-analyze native records", 488 actual=", ".join(sorted(roles)) or "<empty>", 489 repair="bash scripts/release.sh --candidate", 490 ) 491 ) 492 for role, record in sorted(roles.items()): 493 signing = record.get("signing") 494 if record.get("signing_mode") != MACOS_SIGNING_MODE: 495 failures.append( 496 Failure( 497 error="macOS native record signing_mode is not final", 498 expected=MACOS_SIGNING_MODE, 499 actual=repr(record.get("signing_mode")), 500 repair="bash scripts/release.sh --candidate", 501 ) 502 ) 503 if not isinstance(signing, Mapping) or any( 504 signing.get(key) is not True 505 for key in ( 506 "signer_pinned", 507 "team_pinned", 508 "hardened_runtime", 509 "trusted_timestamp", 510 ) 511 ): 512 failures.append( 513 Failure( 514 error="macOS native record signing verification is incomplete", 515 expected=f"{role} signed and timestamped native record", 516 actual=repr(signing), 517 repair="bash scripts/release.sh --candidate", 518 ) 519 ) 520 if record.get("notarization_status") != "accepted": 521 failures.append( 522 Failure( 523 error="macOS native record notarization is not accepted", 524 expected="accepted", 525 actual=repr(record.get("notarization_status")), 526 repair="bash scripts/release.sh --candidate", 527 ) 528 ) 529 if failures: 530 return None, failures 531 final = dict(preflight_evidence) 532 final["signing_mode"] = MACOS_SIGNING_MODE 533 final_failures = check_lane_tool_evidence("macos-arm64", final) 534 if final_failures: 535 return None, final_failures 536 return final, [] 537 538 539def _tool_output( 540 name: str, 541 args: Sequence[str], 542 *, 543 which: Callable[[str], str | None], 544 runner: Runner, 545) -> str: 546 path = which(name) 547 if path is None: 548 return "not found" 549 result = runner( 550 [path, *args], 551 capture_output=True, 552 text=True, 553 check=False, 554 ) 555 return result.stdout.strip() or result.stderr.strip() or f"exit {result.returncode}" 556 557 558def _first_line(value: str) -> str: 559 return value.splitlines()[0].strip() if value.splitlines() else value.strip() 560 561 562def _xcode_evidence(output: str) -> str: 563 lines = [line.strip() for line in output.splitlines() if line.strip()] 564 if ( 565 f"Xcode {MACOS_XCODE_VERSION}" in lines 566 and f"Build version {MACOS_XCODE_BUILD}" in lines 567 ): 568 return MACOS_XCODE_PIN 569 return "; ".join(lines) or "not found" 570 571 572def collect_lane_tool_evidence( 573 lane: LaneName, 574 *, 575 which: Callable[[str], str | None] = shutil.which, 576 runner: Runner = subprocess.run, 577 python_executable: str = sys.executable, 578) -> dict[str, str]: 579 if lane not in LANE_TOOL_KEYS: 580 raise ValueError(f"unknown release lane: {lane}") 581 python_result = runner( 582 [python_executable, "--version"], 583 capture_output=True, 584 text=True, 585 check=False, 586 ) 587 python_output = ( 588 python_result.stdout.strip() 589 or python_result.stderr.strip() 590 or f"exit {python_result.returncode}" 591 ) 592 evidence = { 593 "python": python_output.removeprefix("Python ").strip(), 594 "rustc": _tool_output("rustc", ["--version"], which=which, runner=runner), 595 "cargo": _tool_output("cargo", ["--version"], which=which, runner=runner), 596 "uv": _tool_output("uv", ["--version"], which=which, runner=runner), 597 "maturin": _tool_output("maturin", ["--version"], which=which, runner=runner), 598 "cargo-deny": _tool_output( 599 "cargo-deny", ["--version"], which=which, runner=runner 600 ), 601 } 602 if lane in {"linux-x86_64-musl", "linux-aarch64-musl"}: 603 zig = _tool_output("zig", ["version"], which=which, runner=runner) 604 evidence["zig"] = f"zig {zig}" if zig != "not found" else zig 605 if lane == "macos-arm64": 606 evidence["xcode"] = _xcode_evidence( 607 _tool_output("xcodebuild", ["-version"], which=which, runner=runner) 608 ) 609 evidence["swift"] = _first_line( 610 _tool_output("swift", ["--version"], which=which, runner=runner) 611 ) 612 codesign_path = which("codesign") 613 evidence["codesign"] = ( 614 MACOS_CODESIGN_PUBLIC_PIN 615 if codesign_path == MACOS_CODESIGN_PATH 616 else codesign_path or "not found" 617 ) 618 evidence["notarytool"] = _tool_output( 619 "xcrun", ["notarytool", "--version"], which=which, runner=runner 620 ) 621 return {key: evidence[key] for key in LANE_TOOL_KEYS[lane] if key in evidence} 622 623 624def check_lane_tools( 625 lane: LaneName, 626 *, 627 which: Callable[[str], str | None] = shutil.which, 628 runner: Runner = subprocess.run, 629 python_executable: str = sys.executable, 630) -> list[Failure]: 631 return check_presign_lane_tool_evidence( 632 lane, 633 collect_lane_tool_evidence( 634 lane, 635 which=which, 636 runner=runner, 637 python_executable=python_executable, 638 ), 639 ) 640 641 642def check_local_clean_status(status_output: str) -> list[Failure]: 643 paths = [line for line in status_output.splitlines() if line.strip()] 644 if not paths: 645 return [] 646 return [ 647 Failure( 648 error="working tree is dirty", 649 expected="no tracked or untracked changes", 650 actual="; ".join(paths), 651 repair="git status --short --untracked-files=normal", 652 ) 653 ] 654 655 656def local_status(root: Path, *, runner: Runner = subprocess.run) -> str: 657 result = runner( 658 ["git", "status", "--porcelain", "--untracked-files=normal"], 659 cwd=root, 660 capture_output=True, 661 text=True, 662 check=False, 663 ) 664 if result.returncode != 0: 665 raise RuntimeError(result.stderr.strip() or "git status failed") 666 return result.stdout 667 668 669def check_remote_state( 670 expected_ref: str, 671 actual_ref: str, 672 status_output: str, 673 *, 674 label: str, 675) -> list[Failure]: 676 failures: list[Failure] = [] 677 actual_ref = actual_ref.strip() 678 if actual_ref != expected_ref: 679 failures.append( 680 Failure( 681 error=f"{label} is not on the release ref", 682 expected=expected_ref, 683 actual=actual_ref or "<empty>", 684 repair="python3 scripts/check_release_preflight.py remote-state --help", 685 ) 686 ) 687 for failure in check_local_clean_status(status_output): 688 failures.append( 689 Failure( 690 error=f"{label} working tree is dirty", 691 expected=failure.expected, 692 actual=failure.actual, 693 repair="git status --short --untracked-files=normal", 694 ) 695 ) 696 return failures 697 698 699def check_pinned_toolchain(root: Path, env: Mapping[str, str]) -> list[Failure]: 700 try: 701 spec = load_toolchain_spec(root) 702 except (OSError, tomllib.TOMLDecodeError, ValueError) as exc: 703 return [ 704 Failure( 705 error="release toolchain file is invalid", 706 expected=f"valid {TOOLCHAIN_FILE} with [toolchain].channel", 707 actual=str(exc), 708 repair="$EDITOR rust-toolchain.toml", 709 ) 710 ] 711 failures = check_rustup_override(spec.channel, env) 712 toolchain_dir, install_failures = check_toolchain_installed( 713 spec.channel, 714 rustup_home(env), 715 ) 716 failures.extend(install_failures) 717 if toolchain_dir is not None: 718 failures.extend(check_rustc_version(toolchain_dir, spec.channel)) 719 failures.extend( 720 check_toolchain_components(toolchain_dir, spec.channel, spec.components) 721 ) 722 failures.extend( 723 check_toolchain_targets(toolchain_dir, spec.channel, spec.targets) 724 ) 725 return failures 726 727 728def check_named_toolchain(toolchain: str, env: Mapping[str, str]) -> list[Failure]: 729 toolchain_dir, failures = check_toolchain_installed(toolchain, rustup_home(env)) 730 if toolchain_dir is not None: 731 failures.extend(check_rustc_version(toolchain_dir, toolchain)) 732 return failures 733 734 735def _cmd_local(args: argparse.Namespace) -> int: 736 root = args.root.resolve() 737 failures = check_pinned_toolchain(root, os.environ) 738 failures.extend(check_zig()) 739 if args.require_clean: 740 try: 741 status = local_status(root) 742 except RuntimeError as exc: 743 failures.append( 744 Failure( 745 error="could not inspect local working tree", 746 expected="git status succeeds", 747 actual=str(exc), 748 repair="git status --short --untracked-files=normal", 749 ) 750 ) 751 else: 752 failures.extend(check_local_clean_status(status)) 753 if failures: 754 _format_failures(failures) 755 return 1 756 print("release local preflight ok") 757 return 0 758 759 760def _cmd_msrv(args: argparse.Namespace) -> int: 761 failures = check_named_toolchain(args.toolchain, os.environ) 762 if failures: 763 _format_failures(failures) 764 return 1 765 print(f"rust MSRV toolchain {args.toolchain} ok") 766 return 0 767 768 769def _cmd_cargo_deny(_args: argparse.Namespace) -> int: 770 failures = check_cargo_deny() 771 if failures: 772 _format_failures(failures) 773 return 1 774 print(f"cargo-deny {CARGO_DENY_VERSION} ok") 775 return 0 776 777 778def _cmd_lane_tools(args: argparse.Namespace) -> int: 779 failures = check_lane_tools(args.lane) 780 if failures: 781 _format_failures(failures) 782 return 1 783 print(f"{args.lane} preflight-valid non-release tool evidence ok") 784 return 0 785 786 787def _cmd_remote_state(args: argparse.Namespace) -> int: 788 status = args.status_file.read_text(encoding="utf-8") 789 failures = check_remote_state( 790 args.expected_ref, 791 args.actual_ref, 792 status, 793 label=args.label, 794 ) 795 if failures: 796 _format_failures(failures) 797 return 1 798 print(f"{args.label} release state ok") 799 return 0 800 801 802def main(argv: list[str] | None = None) -> int: 803 parser = argparse.ArgumentParser() 804 subparsers = parser.add_subparsers(dest="command", required=True) 805 806 local = subparsers.add_parser("local") 807 local.add_argument("--root", type=Path, default=Path(".")) 808 local.add_argument("--require-clean", action="store_true") 809 local.set_defaults(func=_cmd_local) 810 811 msrv = subparsers.add_parser("msrv") 812 msrv.add_argument("--toolchain", required=True) 813 msrv.set_defaults(func=_cmd_msrv) 814 815 cargo_deny = subparsers.add_parser("cargo-deny") 816 cargo_deny.set_defaults(func=_cmd_cargo_deny) 817 818 lane_tools = subparsers.add_parser("lane-tools") 819 lane_tools.add_argument( 820 "--lane", 821 choices=tuple(LANE_TOOL_KEYS), 822 required=True, 823 ) 824 lane_tools.set_defaults(func=_cmd_lane_tools) 825 826 remote = subparsers.add_parser("remote-state") 827 remote.add_argument("--label", required=True) 828 remote.add_argument("--expected-ref", required=True) 829 remote.add_argument("--actual-ref", required=True) 830 remote.add_argument("--status-file", type=Path, required=True) 831 remote.set_defaults(func=_cmd_remote_state) 832 833 args = parser.parse_args(argv) 834 return args.func(args) 835 836 837if __name__ == "__main__": 838 raise SystemExit(main())