Termux removes old package files, so a pinned URL and hash do not keep build inputs available. Preserve the exact bytes without changing pins. Archive every PM HTTP artifact and the Termux runtime inputs by SHA256. CI reads R2 first. Only a missing object permits an upstream download, hash verification, immutable upload, and verified readback. Seed the actual toolchain and payload stores before their consumers run. Use the public archive as a pinned fallback in PM, bootstrap installers, and Nix fetchers. Keep network retries bounded and report attempted URLs. Keep publication credentials in protected CI jobs, not installed clients. Verification: - 283 targeted tests passed; five POSIX tests skipped on Windows. - All 87 preserved Termux packages passed local archive miss/hit checks. - Native ARM64 ripgrep installed through the mirror and ran successfully. - Wheel import, workflow lint, Python lint, shell syntax, and pins passed. Live R2 publication, POSIX tests, and Nix builds remain for native CI. The real-byte archive checks used loopback HTTP, not the live bucket.
211 lines
7.3 KiB
Python
211 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the bootstrap pin fragments inside the installers.
|
|
|
|
The installers bootstrap uv (and, on Windows, git) BEFORE any checkout
|
|
exists, so they cannot read pm/lock.json at run time: they are fetched
|
|
standalone (`curl | sh`, `irm | iex`) and there is no JSON parser they can
|
|
rely on that early (no jq guarantee; python is what uv installs on Windows).
|
|
Instead, this script derives a plain-data fragment from pm/lock.json — the
|
|
SAME authority the pm package manager uses for every managed tool — and
|
|
splices it between markers in each installer. The bytes are stored, the
|
|
truth is derived, and the drift test (tests/test_bootstrap_pins_fragment.py)
|
|
fails when they disagree.
|
|
|
|
Run after bumping a bootstrapped tool in pm/lock.json:
|
|
|
|
python3 scripts/gen-bootstrap-pins.py # rewrite fragments
|
|
python3 scripts/gen-bootstrap-pins.py --check # exit 1 on drift
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
PINS_PATH = REPO_ROOT / "pm" / "lock.json"
|
|
|
|
# Standalone installers cannot read the shared mirror layout before checkout.
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
from pm.artifact_mirror import mirror_url # noqa: E402
|
|
|
|
BEGIN_MARK = "# --- BEGIN GENERATED: bootstrap pins (scripts/gen-bootstrap-pins.py) ---"
|
|
END_MARK = "# --- END GENERATED: bootstrap pins ---"
|
|
|
|
_POSIX_TARGETS = ("linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64")
|
|
_WINDOWS_TARGETS = ("win32-x64", "win32-arm64")
|
|
|
|
|
|
def _load_lock() -> dict:
|
|
return json.loads(PINS_PATH.read_text(encoding="utf-8-sig"))
|
|
|
|
|
|
def _load_uv_pin() -> dict:
|
|
entry = _load_lock()["packages"]["uv"]
|
|
for target in _POSIX_TARGETS + _WINDOWS_TARGETS:
|
|
_validate_artifact("uv", entry, target)
|
|
return entry
|
|
|
|
|
|
def _load_git_pin() -> dict:
|
|
# Windows targets only. install.ps1 bootstraps the pinned git-for-windows
|
|
# artifact because Windows needs it to clone. macOS and Linux use the
|
|
# machine's git, so the pin table declares those targets as reasoned gaps
|
|
# and there is nothing for the sh installers to stage.
|
|
entry = _load_lock()["packages"]["git"]
|
|
for target in _WINDOWS_TARGETS:
|
|
_validate_artifact("git", entry, target)
|
|
return entry
|
|
|
|
|
|
def _validate_artifact(tool: str, entry: dict, target: str) -> None:
|
|
spec = entry["artifacts"][target] # KeyError on a missing target is the point
|
|
if spec.get("missing"):
|
|
# The table says upstream ships nothing here, but this generator was
|
|
# told to emit that target. One of the two is wrong, and a KeyError
|
|
# further down would not say which.
|
|
raise ValueError(
|
|
f"{tool} pin for {target}: the table declares a gap "
|
|
f"({spec['missing']}), so there is nothing to generate"
|
|
)
|
|
if not spec["url"].startswith("https://"):
|
|
raise ValueError(f"{tool} pin for {target}: url must be https")
|
|
if len(spec["sha256"]) != 64:
|
|
raise ValueError(f"{tool} pin for {target}: sha256 must be 64 hex chars")
|
|
|
|
|
|
def _sh_fragment(uv: dict) -> str:
|
|
lines = [
|
|
BEGIN_MARK,
|
|
"# Derived from pm/lock.json. DO NOT EDIT BY HAND:",
|
|
"# run scripts/gen-bootstrap-pins.py after a pin bump.",
|
|
f'UV_PIN_VERSION="{uv["version"]}"',
|
|
"",
|
|
"# Sets UV_PIN_URL + UV_PIN_SHA256 for a <os>-<arch> target key.",
|
|
"uv_bootstrap_pin() {",
|
|
' case "$1" in',
|
|
]
|
|
for target in _POSIX_TARGETS:
|
|
entry = uv["artifacts"][target]
|
|
lines += [
|
|
f" {target})",
|
|
f' UV_PIN_URL="{entry["url"]}"',
|
|
f' UV_PIN_MIRROR="{mirror_url(entry["sha256"])}"',
|
|
f' UV_PIN_SHA256="{entry["sha256"]}"',
|
|
" ;;",
|
|
]
|
|
lines += [
|
|
" *)",
|
|
' UV_PIN_URL=""',
|
|
' UV_PIN_SHA256=""',
|
|
" return 1",
|
|
" ;;",
|
|
" esac",
|
|
"}",
|
|
END_MARK,
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _ps1_fragment(uv: dict, git: dict) -> str:
|
|
lines = [
|
|
BEGIN_MARK,
|
|
"# Derived from pm/lock.json. DO NOT EDIT BY HAND:",
|
|
"# run scripts/gen-bootstrap-pins.py after a pin bump.",
|
|
f'$script:UvPinVersion = "{uv["version"]}"',
|
|
"$script:UvPinFiles = @{",
|
|
]
|
|
for target in _WINDOWS_TARGETS:
|
|
entry = uv["artifacts"][target]
|
|
lines += [
|
|
f' "{target}" = @{{',
|
|
f' Url = "{entry["url"]}"',
|
|
f' MirrorUrl = "{mirror_url(entry["sha256"])}"',
|
|
f' Sha256 = "{entry["sha256"]}"',
|
|
" }",
|
|
]
|
|
lines += [
|
|
"}",
|
|
"",
|
|
f'$script:GitPinVersion = "{git["version"]}"',
|
|
"$script:GitPinFiles = @{",
|
|
]
|
|
for target in _WINDOWS_TARGETS:
|
|
entry = git["artifacts"][target]
|
|
lines += [
|
|
f' "{target}" = @{{',
|
|
f' Url = "{entry["url"]}"',
|
|
f' MirrorUrl = "{mirror_url(entry["sha256"])}"',
|
|
f' Sha256 = "{entry["sha256"]}"',
|
|
" }",
|
|
]
|
|
lines += [
|
|
"}",
|
|
END_MARK,
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _splice(path: Path, fragment: str, check: bool) -> bool:
|
|
"""Replace the marked block in *path*. Returns True when up to date."""
|
|
raw = path.read_bytes()
|
|
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
|
text = raw.decode("utf-8-sig")
|
|
begin = text.index(BEGIN_MARK) # ValueError on missing marker is the point
|
|
end = text.index(END_MARK) + len(END_MARK)
|
|
# Match the file's line endings (install.ps1 is CRLF) so the generated
|
|
# block does not leave the script with mixed newlines.
|
|
newline = "\r\n" if "\r\n" in text else "\n"
|
|
fragment = fragment.replace("\n", newline)
|
|
updated = text[:begin] + fragment + text[end:]
|
|
if updated == text:
|
|
return True
|
|
if not check:
|
|
path.write_bytes((b"\xef\xbb\xbf" if had_bom else b"") + updated.encode("utf-8"))
|
|
return False
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--check",
|
|
action="store_true",
|
|
help="exit 1 when the committed fragments drift from pm/lock.json",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
uv = _load_uv_pin()
|
|
git = _load_git_pin()
|
|
results = {
|
|
"scripts/install.sh": _splice(
|
|
REPO_ROOT / "scripts" / "install.sh", _sh_fragment(uv), args.check
|
|
),
|
|
"scripts/install.ps1": _splice(
|
|
REPO_ROOT / "scripts" / "install.ps1", _ps1_fragment(uv, git), args.check
|
|
),
|
|
# setup-hermes.sh deliberately holds NO fragment: it runs after a
|
|
# checkout exists, so it reads the pin straight from pm/lock.json
|
|
# at run time (the pm.sh-derived bootstrap). The fragment exists
|
|
# only for the curl|sh / irm|iex installers that bootstrap BEFORE
|
|
# any checkout exists.
|
|
}
|
|
stale = [name for name, fresh in results.items() if not fresh]
|
|
if args.check and stale:
|
|
print(
|
|
"bootstrap pin fragments drifted from pm/lock.json in: "
|
|
f"{', '.join(stale)}\nrun: python3 scripts/gen-bootstrap-pins.py",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
for name in stale:
|
|
print(f"updated {name}")
|
|
if not stale:
|
|
print("fragments up to date")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|