From 8668c12f7dfef0261084a04c6a5b0da75f8c5399 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 17:49:58 +0000 Subject: [PATCH] Fix custom package checksum verification for Mac and Linux arm64 Three root causes were causing failures on non-x86_64-Linux platforms: 1. Single SHA256 per package: the pinned fallback checksum in formatted_packages.py was a single value computed for linux-x86_64 only. Downloads on linux-aarch64, macos-x86_64, and macos-aarch64 produced different binaries with different digests, so verification always failed on those platforms when the fetch_latest resolver was unavailable. Fix: replace the single `sha256` field with a `sha256_map` dict keyed by "{os}-{arch}" (e.g. "linux-aarch64", "macos-arm64"). Added the correct pinned digests for all four supported platforms for both Go 1.26.3 and Firecracker 1.15.1, fetched from official sources. 2. Case-sensitive SHA256 comparison: _sha256_of() always returns lowercase hex, but checksums returned by external APIs could be uppercase. _verify() compared them without normalising case, causing false mismatches. Fix: new resolved_sha256 property always returns a lowercased digest; _resolve_latest() also lowercases the dynamically-fetched sha before storing it. 3. Firecracker not skipped on macOS: _resolve_latest_firecracker() returns None on macOS (firecracker is Linux-only), causing a fallback to the pinned linux binary URL with a linux-only checksum map entry. The download and verification would both fail misleadingly. Fix: explicit early-continue guard in install_custom_packages() when name == "firecracker" and IS_MACOS. --- bootstrap_environment.py | 35 +++++++++++++++++++++++++++++------ formatted_packages.py | 12 ++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/bootstrap_environment.py b/bootstrap_environment.py index ebbba5d..b82c11a 100755 --- a/bootstrap_environment.py +++ b/bootstrap_environment.py @@ -47,7 +47,7 @@ import urllib.error import urllib.request from dataclasses import dataclass from pathlib import Path -from typing import Optional +from typing import Optional, Union import formatted_packages @@ -890,7 +890,8 @@ class CustomPackage: name: str version: Optional[str] = None # pinned fallback version url_template: Optional[str] = None # uses {version}, {arch}, {arch_go} - sha256: Optional[str] = None # hex digest of the pinned archive + sha256: Optional[str] = None # single-arch hex digest (set by _resolve_latest) + sha256_map: Optional[dict] = None # per-platform pinned digests: {"os-arch": hex} sha256_url_template: Optional[str] = None # template for a .minisig URL minisign_key: Optional[str] = None # base64 public key for minisign verification fetch_latest: Optional[str] = None # latest-version resolver hint @@ -907,6 +908,22 @@ class CustomPackage: if self.sha256_url_template else None ) + @property + def resolved_sha256(self) -> Optional[str]: + """Return the SHA256 hex for the current OS+arch, lowercased. + + sha256 (set dynamically by _resolve_latest) takes priority over sha256_map + so that a freshly fetched checksum always wins over the pinned fallback. + """ + if self.sha256: + return self.sha256.lower() + if self.sha256_map: + key = f"{OS}-{ARCH}" + val = self.sha256_map.get(key) + if val: + return val.lower() + return None + @property def display_name(self) -> str: return f"{self.name}-{self.version}" if self.version else self.name @@ -974,10 +991,11 @@ def _sha256_of(path: Path) -> str: def _verify(archive: Path, pkg: CustomPackage) -> bool: """Returns True if verification passed (or nothing to verify), False on failure.""" - if pkg.sha256: + expected = pkg.resolved_sha256 + if expected: actual = _sha256_of(archive) - if actual != pkg.sha256: - err(f"SHA256 mismatch for {pkg.name}: expected {pkg.sha256}, got {actual}") + if actual != expected: + err(f"SHA256 mismatch for {pkg.name}: expected {expected}, got {actual}") return False print(" SHA256 OK") elif pkg.sha256_url: @@ -1469,7 +1487,7 @@ def _resolve_latest(pkg: CustomPackage) -> None: return print(f" Latest is {latest_version} (pinned was {pkg.version}); using latest.") pkg.version = latest_version - pkg.sha256 = latest_sha + pkg.sha256 = latest_sha.lower() pkg.sha256_url_template = None # prefer the freshly resolved sha256 @@ -1483,6 +1501,11 @@ def install_custom_packages(to_install: list[CustomPackage]) -> None: if check_path is None and name_lower != "pip": warn(f"{pkg.name}: no known install path — script will not detect future installs") + # Packages that are OS-specific + if name_lower == "firecracker" and IS_MACOS: + warn(f"{pkg.name}: Linux-only — skipping on macOS") + continue + # Handlers that manage their own download/install if name_lower == "nvm": _install_nvm() diff --git a/formatted_packages.py b/formatted_packages.py index 252d1d0..dfa9636 100644 --- a/formatted_packages.py +++ b/formatted_packages.py @@ -94,7 +94,12 @@ CUSTOM_PACKAGES: list[dict] = [ "name": "go", "version": "1.26.3", "url_template": "https://go.dev/dl/go{version}.{os_go}-{arch_go}.tar.gz", - "sha256": "2b2cfc7148493da5e73981bffbf3353af381d5f93e789c82c79aff64962eb556", + "sha256_map": { + "linux-x86_64": "2b2cfc7148493da5e73981bffbf3353af381d5f93e789c82c79aff64962eb556", + "linux-aarch64": "9d89a3ea57d141c2b22d70083f2c8459ba3890f2d9e818e7e933b75614936565", + "macos-x86_64": "278d580b32e299fe4a9c990fcf2d02acfe538c7e551a6ee18f9c7164573d2c63", + "macos-aarch64": "875cf54a15311eee2c99b9dd67c68c4a49351d489ab622bf2cfd28c8f2078d3c", + }, "fetch_latest": "go", }, {"name": "neovim"}, @@ -105,7 +110,10 @@ CUSTOM_PACKAGES: list[dict] = [ "https://github.com/firecracker-microvm/firecracker/releases/download/" "v{version}/firecracker-v{version}-{arch}.tgz" ), - "sha256": "d4a32ab2322d887ca1bc4a4e7afa9cc35393e6362dfc2b3becb389d362e4275a", + "sha256_map": { + "linux-x86_64": "d4a32ab2322d887ca1bc4a4e7afa9cc35393e6362dfc2b3becb389d362e4275a", + "linux-aarch64": "00654ac1e702a22744121ea9f10a4f792ebd7c3a744cba587dfac9fcb79b41a5", + }, "fetch_latest": "firecracker", }, { -- 2.47.3