This commit is contained in:
Mateusz Gruszczyński committed 2026-11-08 11:47:01 +01:00
1 parent 0cde2c8a18
commit 3d3e0ea5b8
12 files changed
+474 -7

No files matched your search

Executable → Regular
+108 -1
View File
@@ -14,6 +14,7 @@ import json
import os
from pathlib import Path
import re
import shutil
import sys
import tempfile
import time
@@ -25,6 +26,7 @@ import zlib
VERSION = "5.33.1"
DOWNLOAD_URL = f"https://github.com/swagger-api/swagger-ui/archive/refs/tags/v{VERSION}.zip"
PREFIX = f"swagger-ui-{VERSION}/"
VENDORED = Path(__file__).resolve().parents[2] / "third_party" / f"swagger-ui-{VERSION}"
MAX_ARCHIVE = 64 * 1024 * 1024
ASSETS = {
"swagger-ui-bundle.js": ("dist/swagger-ui-bundle.js", 16 * 1024 * 1024),
@@ -82,11 +84,29 @@ class HttpsRedirects(HTTPRedirectHandler):
def download(destination: Path) -> None:
"""Download with a few retries; github.com occasionally times out or returns a gateway error."""
last: Exception | None = None
for attempt in range(4):
try:
_download_once(destination)
return
except AssetError:
raise
except (OSError, ValueError) as error: # URLError/HTTPError/timeouts are OSError subclasses
last = error
if attempt < 3:
delay = (3, 6, 12)[attempt]
print(f"Swagger download failed ({error}); retrying in {delay}s ({attempt + 1}/3).", file=sys.stderr)
time.sleep(delay)
raise AssetError(f"Swagger download failed after 4 attempts: {last}")
def _download_once(destination: Path) -> None:
"""TLS verification uses Python's default trust store; never executes a shell."""
started = time.monotonic()
opener = build_opener(HttpsRedirects())
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build"})
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
with opener.open(request, timeout=60) as response, destination.open("wb") as output:
length = response.headers.get("Content-Length")
if length and int(length) > MAX_ARCHIVE:
raise AssetError("Swagger archive exceeds the 64 MiB limit.")
@@ -162,6 +182,75 @@ def atomic_write(path: Path, data: bytes) -> None:
temporary.unlink(missing_ok=True)
def cached_archives(out_dir: Path, cache: Path) -> list[Path]:
"""Previously downloaded copies of the pinned archive.
Cargo gives every package version its own OUT_DIR, so a version bump would download the same
pinned archive again. Reuse an archive from the shared cache or from another build directory,
after the usual validation below.
"""
candidates: list[Path] = []
override = os.environ.get("SWAGGER_UI_CACHE_DIR", "").strip()
if override:
candidates.append(Path(override) / cache.name)
parents = out_dir.parents
if len(parents) > 3 and parents[1].name == "build":
candidates.append(parents[3] / "swagger-ui-cache" / cache.name)
try:
siblings = sorted(parents[1].glob("*/out/" + cache.name), key=lambda item: item.stat().st_mtime, reverse=True)
except OSError:
siblings = []
candidates.extend(siblings)
return [item for item in dict.fromkeys(candidates) if item != cache and item.is_file()]
def remember_archive(out_dir: Path, cache: Path) -> None:
"""Best-effort copy of a validated archive into the shared cache directory."""
parents = out_dir.parents
if len(parents) > 3 and parents[1].name == "build":
shared = parents[3] / "swagger-ui-cache"
try:
shared.mkdir(parents=True, exist_ok=True)
if not (shared / cache.name).exists():
shutil.copyfile(cache, shared / cache.name)
except OSError:
pass
def read_vendored() -> tuple[dict[str, bytes], str, Path] | None:
"""Assets vendored in third_party/ (official swagger-ui-dist files); verified against SHA256SUMS.
Returns None when the directory is absent so the pinned download still works for older trees.
A present but inconsistent directory is an error, never a silent fallback.
"""
manifest = VENDORED / "SHA256SUMS"
if not manifest.is_file():
return None
expected: dict[str, str] = {}
for line in manifest.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
digest, _, name = line.partition(" ")
if not re.fullmatch(r"[0-9a-f]{64}", digest) or not name or "/" in name or name.startswith("."):
raise AssetError("Vendored Swagger SHA256SUMS is malformed.")
expected[name] = digest
files: dict[str, bytes] = {}
for name in ("swagger-ui-bundle.js", "swagger-ui.css", "LICENSE", "NOTICE", "swagger-ui-bundle.js.LICENSE.txt"):
path = VENDORED / name
if name not in expected or not path.is_file() or path.is_symlink():
raise AssetError(f"Vendored Swagger UI is incomplete: {name} is missing or not listed in SHA256SUMS.")
data = path.read_bytes()
if not 0 < len(data) <= 16 * 1024 * 1024 or hashlib.sha256(data).hexdigest() != expected[name]:
raise AssetError(f"Vendored Swagger UI file failed its SHA-256 check: {name}.")
data.decode("utf-8")
files[name] = data
package = {name: files[name] for name in ASSETS}
licenses = [label.encode() + b"\n" + b"-" * 40 + b"\n" + files[name] for label, name in (("LICENSE", "LICENSE"), ("NOTICE", "NOTICE"), ("dist/swagger-ui-bundle.js.LICENSE.txt", "swagger-ui-bundle.js.LICENSE.txt"))]
assets = {"swagger-ui-bundle.js": package["swagger-ui-bundle.js"], "swagger-ui.css": package["swagger-ui.css"],
"licenses.txt": b"Swagger UI " + VERSION.encode() + b"\n\n" + b"\n\n".join(licenses)}
return assets, hashlib.sha256(manifest.read_bytes()).hexdigest(), manifest
def prepare(out_dir: Path, source: str | Path, checksum: str | None = None,
*, offline: bool = False) -> list[Path]:
out_dir.mkdir(parents=True, exist_ok=True)
@@ -169,10 +258,27 @@ def prepare(out_dir: Path, source: str | Path, checksum: str | None = None,
if isinstance(source, Path):
archive = source
assets, digest = read_assets(archive, checksum)
elif source == DOWNLOAD_URL and checksum is None and (vendored := read_vendored()) is not None:
assets, digest, archive = vendored
print(f"Using the vendored Swagger UI {VERSION} from {VENDORED}.", file=sys.stderr)
elif source == DOWNLOAD_URL:
archive = cache
reused = None
if cache.exists():
assets, digest = read_assets(cache, checksum)
else:
for candidate in cached_archives(out_dir, cache):
try:
reused = (read_assets(candidate, checksum), candidate)
break
except (OSError, ValueError, KeyError, RuntimeError, zipfile.BadZipFile, zlib.error):
continue
if reused is not None:
(assets, digest), candidate = reused
shutil.copyfile(candidate, cache)
print(f"Reusing the cached Swagger UI archive from {candidate}.", file=sys.stderr)
elif cache.exists():
pass
else:
if offline:
raise AssetError("Swagger assets are not cached and CARGO_NET_OFFLINE is set. " + HELP)
@@ -185,6 +291,7 @@ def prepare(out_dir: Path, source: str | Path, checksum: str | None = None,
os.replace(temporary, cache) # Cache only an already-validated archive.
finally:
temporary.unlink(missing_ok=True)
remember_archive(out_dir, cache)
else:
raise AssetError("Unrecognized Swagger source.")
destination = out_dir / "swagger-ui"