personal memory agent
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())