| |
|
|
| from __future__ import annotations |
|
|
| import re |
| from dataclasses import dataclass |
| from pathlib import Path |
|
|
|
|
| SHA256_RE = re.compile(r"[0-9a-f]{64}") |
| HTTP_FILE_BLOCK_RE = re.compile(r"(?ms)^http_file\(\n.*?^\)\n?") |
| HTTP_FILE_VERSION_RE = re.compile(r"^rusty_v8_([0-9]+)_([0-9]+)_([0-9]+)_") |
|
|
|
|
| class RustyV8ChecksumError(ValueError): |
| pass |
|
|
|
|
| @dataclass(frozen=True) |
| class RustyV8HttpFile: |
| start: int |
| end: int |
| block: str |
| name: str |
| downloaded_file_path: str |
| sha256: str | None |
|
|
|
|
| def parse_checksum_manifest(path: Path) -> dict[str, str]: |
| try: |
| lines = path.read_text(encoding="utf-8").splitlines() |
| except FileNotFoundError as exc: |
| raise RustyV8ChecksumError(f"missing checksum manifest: {path}") from exc |
|
|
| checksums: dict[str, str] = {} |
| for line_number, line in enumerate(lines, 1): |
| if not line.strip(): |
| continue |
| parts = line.split() |
| if len(parts) != 2: |
| raise RustyV8ChecksumError( |
| f"{path}:{line_number}: expected '<sha256> <filename>'" |
| ) |
| checksum, filename = parts |
| if not SHA256_RE.fullmatch(checksum): |
| raise RustyV8ChecksumError( |
| f"{path}:{line_number}: invalid SHA-256 digest for {filename}" |
| ) |
| if not filename or filename in {".", ".."} or "/" in filename: |
| raise RustyV8ChecksumError( |
| f"{path}:{line_number}: expected a bare artifact filename" |
| ) |
| if filename in checksums: |
| raise RustyV8ChecksumError( |
| f"{path}:{line_number}: duplicate checksum for {filename}" |
| ) |
| checksums[filename] = checksum |
|
|
| if not checksums: |
| raise RustyV8ChecksumError(f"empty checksum manifest: {path}") |
| return checksums |
|
|
|
|
| def string_field(block: str, field: str) -> str | None: |
| |
| match = re.search(rf'^\s*{re.escape(field)}\s*=\s*"([^"]+)",\s*$', block, re.M) |
| if match: |
| return match.group(1) |
| return None |
|
|
|
|
| def rusty_v8_http_files(module_bazel: str, version: str) -> list[RustyV8HttpFile]: |
| version_slug = version.replace(".", "_") |
| name_prefix = f"rusty_v8_{version_slug}_" |
| entries = [] |
| for match in HTTP_FILE_BLOCK_RE.finditer(module_bazel): |
| block = match.group(0) |
| name = string_field(block, "name") |
| if not name or not name.startswith(name_prefix): |
| continue |
| downloaded_file_path = string_field(block, "downloaded_file_path") |
| if not downloaded_file_path: |
| raise RustyV8ChecksumError( |
| f"MODULE.bazel {name} is missing downloaded_file_path" |
| ) |
| entries.append( |
| RustyV8HttpFile( |
| start=match.start(), |
| end=match.end(), |
| block=block, |
| name=name, |
| downloaded_file_path=downloaded_file_path, |
| sha256=string_field(block, "sha256"), |
| ) |
| ) |
| return entries |
|
|
|
|
| def rusty_v8_http_file_versions(module_bazel: str) -> list[str]: |
| versions = set() |
| for match in HTTP_FILE_BLOCK_RE.finditer(module_bazel): |
| name = string_field(match.group(0), "name") |
| if not name: |
| continue |
| version_match = HTTP_FILE_VERSION_RE.match(name) |
| if version_match: |
| versions.add(".".join(version_match.groups())) |
| return sorted(versions) |
|
|
|
|
| def module_entry_set_errors( |
| entries: list[RustyV8HttpFile], |
| checksums: dict[str, str], |
| version: str, |
| ) -> list[str]: |
| errors = [] |
| if not entries: |
| errors.append(f"MODULE.bazel has no rusty_v8 http_file entries for {version}") |
| return errors |
|
|
| module_files: dict[str, RustyV8HttpFile] = {} |
| duplicate_files = set() |
| for entry in entries: |
| if entry.downloaded_file_path in module_files: |
| duplicate_files.add(entry.downloaded_file_path) |
| module_files[entry.downloaded_file_path] = entry |
|
|
| for filename in sorted(duplicate_files): |
| errors.append(f"MODULE.bazel has duplicate http_file entries for {filename}") |
|
|
| for filename in sorted(set(module_files) - set(checksums)): |
| entry = module_files[filename] |
| errors.append(f"MODULE.bazel {entry.name} has no checksum in the manifest") |
|
|
| for filename in sorted(set(checksums) - set(module_files)): |
| errors.append(f"manifest has {filename}, but MODULE.bazel has no http_file") |
|
|
| return errors |
|
|
|
|
| def module_checksum_errors( |
| entries: list[RustyV8HttpFile], |
| checksums: dict[str, str], |
| ) -> list[str]: |
| errors = [] |
| for entry in entries: |
| expected = checksums.get(entry.downloaded_file_path) |
| if expected is None: |
| continue |
| if entry.sha256 is None: |
| errors.append(f"MODULE.bazel {entry.name} is missing sha256") |
| elif entry.sha256 != expected: |
| errors.append( |
| f"MODULE.bazel {entry.name} has sha256 {entry.sha256}, " |
| f"expected {expected}" |
| ) |
| return errors |
|
|
|
|
| def raise_checksum_errors(message: str, errors: list[str]) -> None: |
| if errors: |
| formatted_errors = "\n".join(f"- {error}" for error in errors) |
| raise RustyV8ChecksumError(f"{message}:\n{formatted_errors}") |
|
|
|
|
| def check_module_bazel_text( |
| module_bazel: str, |
| checksums: dict[str, str], |
| version: str, |
| ) -> None: |
| entries = rusty_v8_http_files(module_bazel, version) |
| errors = [ |
| *module_entry_set_errors(entries, checksums, version), |
| *module_checksum_errors(entries, checksums), |
| ] |
| raise_checksum_errors("rusty_v8 MODULE.bazel checksum drift", errors) |
|
|
|
|
| def block_with_sha256(block: str, checksum: str) -> str: |
| sha256_line_re = re.compile(r'(?m)^(\s*)sha256\s*=\s*"[0-9a-f]+",\s*$') |
| if sha256_line_re.search(block): |
| return sha256_line_re.sub( |
| lambda match: f'{match.group(1)}sha256 = "{checksum}",', |
| block, |
| count=1, |
| ) |
|
|
| downloaded_file_path_match = re.search( |
| r'(?m)^(\s*)downloaded_file_path\s*=\s*"[^"]+",\n', |
| block, |
| ) |
| if not downloaded_file_path_match: |
| raise RustyV8ChecksumError("http_file block is missing downloaded_file_path") |
| insert_at = downloaded_file_path_match.end() |
| indent = downloaded_file_path_match.group(1) |
| return f'{block[:insert_at]}{indent}sha256 = "{checksum}",\n{block[insert_at:]}' |
|
|
|
|
| def update_module_bazel_text( |
| module_bazel: str, |
| checksums: dict[str, str], |
| version: str, |
| ) -> str: |
| entries = rusty_v8_http_files(module_bazel, version) |
| errors = module_entry_set_errors(entries, checksums, version) |
| raise_checksum_errors("cannot update rusty_v8 MODULE.bazel checksums", errors) |
|
|
| updated = [] |
| previous_end = 0 |
| for entry in entries: |
| updated.append(module_bazel[previous_end : entry.start]) |
| updated.append( |
| block_with_sha256(entry.block, checksums[entry.downloaded_file_path]) |
| ) |
| previous_end = entry.end |
| updated.append(module_bazel[previous_end:]) |
| return "".join(updated) |
|
|
|
|
| def check_module_bazel( |
| module_bazel_path: Path, |
| manifest_path: Path, |
| version: str, |
| ) -> None: |
| checksums = parse_checksum_manifest(manifest_path) |
| module_bazel = module_bazel_path.read_text(encoding="utf-8") |
| check_module_bazel_text(module_bazel, checksums, version) |
| print(f"{module_bazel_path} rusty_v8 {version} checksums match {manifest_path}") |
|
|
|
|
| def update_module_bazel( |
| module_bazel_path: Path, |
| manifest_path: Path, |
| version: str, |
| ) -> None: |
| checksums = parse_checksum_manifest(manifest_path) |
| module_bazel = module_bazel_path.read_text(encoding="utf-8") |
| updated_module_bazel = update_module_bazel_text(module_bazel, checksums, version) |
| if updated_module_bazel == module_bazel: |
| print(f"{module_bazel_path} rusty_v8 {version} checksums are already current") |
| return |
| module_bazel_path.write_text(updated_module_bazel, encoding="utf-8") |
| print(f"updated {module_bazel_path} rusty_v8 {version} checksums") |
|
|