107 lines
3.8 KiB
Python
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())
|