feeds: Vendor repository indexes

Keep the native APK indexes alongside the expanded package
metadata so image builds do not fetch mutable repository state.

Generate pins atomically and retry transient generation failures.
This commit is contained in:
2026-08-23 23:03:14 +01:00
parent 8248f82ebb
commit 53720becf1
34 changed files with 42526 additions and 23114 deletions
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
import base64
import hashlib
import os
from pathlib import Path
import re
import subprocess
import sys
URL_RE = re.compile(r'^(?P<indent>\s*)url = "(?P<url>https://.*/packages\.adb)";$')
HASH_RE = re.compile(r'^(?P<indent>\s*)hash = ".*";$')
NAME_RE = re.compile(r'^\s*name = "(?P<name>[^"]+)";$')
def vendor_block(lines, start, end, nix_file, indexes_dir):
url_match = None
hash_index = None
name = None
for index in range(start, end):
stripped = lines[index].rstrip("\n")
if match := URL_RE.match(stripped):
url_match = (index, match)
if HASH_RE.match(stripped):
hash_index = index
if match := NAME_RE.match(stripped):
name = match.group("name")
if url_match is None:
return False
if hash_index is None or name is None:
raise RuntimeError(f"incomplete sourceInfo in {nix_file}")
url_index, match = url_match
destination = indexes_dir / name
temporary = destination.with_suffix(destination.suffix + ".tmp")
subprocess.run(
["curl", "--fail", "--location", "--silent", "--show-error",
"--output", temporary, match.group("url")],
check=True,
)
temporary.replace(destination)
digest = base64.b64encode(hashlib.sha256(destination.read_bytes()).digest()).decode()
hash_indent = HASH_RE.match(lines[hash_index].rstrip("\n")).group("indent")
relative = os.path.relpath(destination, nix_file.parent)
if not relative.startswith("."):
relative = f"./{relative}"
lines[hash_index] = f'{hash_indent}hash = "sha256-{digest}";\n'
lines[url_index] = f'{match.group("indent")}url = "file://${{{relative}}}";\n'
return True
def vendor_file(nix_file, indexes_dir):
lines = nix_file.read_text().splitlines(keepends=True)
block_start = None
changed = 0
for index, line in enumerate(lines):
if line.strip() == "sourceInfo = {":
block_start = index + 1
elif block_start is not None and line.strip() == "};":
changed += vendor_block(lines, block_start, index, nix_file, indexes_dir)
block_start = None
if changed:
nix_file.write_text("".join(lines))
return changed
def main():
if len(sys.argv) != 2:
raise SystemExit(f"usage: {sys.argv[0]} CACHE-DIRECTORY")
cache_dir = Path(sys.argv[1]).resolve()
if not (cache_dir / "default.nix").is_file():
raise SystemExit(f"not an OpenWrt cache directory: {cache_dir}")
indexes_dir = cache_dir / "indexes"
indexes_dir.mkdir(exist_ok=True)
changed = 0
for nix_file in sorted(cache_dir.rglob("*.nix")):
changed += vendor_file(nix_file, indexes_dir)
print(f"Vendored {changed} repository indexes in {cache_dir}", file=sys.stderr)
if __name__ == "__main__":
main()