Files
aipackage/autoinstall/offline/resolve-apt-lock.py
T

107 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Resolve an Ubuntu APT dependency closure into candidate payload-lock objects."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
from pathlib import Path
def filtered_lines(path: Path) -> list[str]:
return [
line.strip() for line in path.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
]
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def deb_field(path: Path, field: str) -> str:
result = subprocess.run(
["dpkg-deb", "-f", str(path), field], check=True, capture_output=True, text=True
)
return result.stdout.strip()
def resolve(packages: list[str], work: Path) -> list[dict]:
archives = work / "archives"
archives.mkdir(parents=True, exist_ok=True)
(archives / "partial").mkdir(exist_ok=True)
status = work / "empty-dpkg-status"
status.write_text("", encoding="utf-8")
command = [
"apt-get", "--download-only", "--yes", "--no-install-recommends",
"-o", "Debug::NoLocking=1",
"-o", f"Dir::State::status={status}",
"-o", f"Dir::Cache::archives={archives}",
"-o", "APT::Get::AllowUnauthenticated=false",
"install", *packages,
]
subprocess.run(command, check=True)
debs = sorted(archives.glob("*.deb"))
if not debs:
raise RuntimeError("APT resolution produced no .deb files")
objects = []
for path in debs:
package = deb_field(path, "Package")
version = deb_field(path, "Version")
architecture = deb_field(path, "Architecture")
if architecture not in {"amd64", "all"}:
raise RuntimeError(f"unexpected architecture {architecture} for {path.name}")
digest = sha256_file(path)
objects.append({
"id": f"apt-{package}",
"version": version,
"architecture": architecture,
"source": f"apt://ubuntu-22.04/{package}/{version}/{path.name}",
"target": f"payload/apt/{path.name}",
"sha256": digest,
"bytes": path.stat().st_size,
"bytes_basis": "measured",
"license": "REVIEW-REQUIRED",
"requested_directly": package in packages,
})
return objects
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--lock", type=Path, required=True)
parser.add_argument("--packages", type=Path, required=True)
parser.add_argument("--work", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
lock = json.loads(args.lock.read_text(encoding="utf-8"))
packages = filtered_lines(args.packages)
objects = resolve(packages, args.work)
lock["objects"]["apt"] = objects
lock["unresolved"] = [x for x in lock["unresolved"] if x != "exact APT dependency closure"]
lock["unresolved"].extend([
"APT repository snapshot/index digests and origin approval",
"APT package license mapping and notice extraction",
"approved R570/CUDA 12.8.1/cuDNN/NVIDIA Container Toolkit package closure"
])
lock["unresolved"] = list(dict.fromkeys(lock["unresolved"]))
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(lock, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"APT requested packages: {len(packages)}")
print(f"APT resolved .deb objects: {len(objects)}")
print(f"APT measured bytes: {sum(item['bytes'] for item in objects)}")
print(f"Wrote candidate lock: {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())