#!/usr/bin/env python3 """Disposable local Ubuntu validation VM helper; no host package installation.""" import os import urllib.request, hashlib, json, pathlib, time, subprocess, concurrent.futures ROOT=pathlib.Path(os.environ.get('DOCVIEW_VM_WORK', 'build-ubuntu-vm')).resolve() D=ROOT/'downloads' for sub in ['downloads','metadata','logs','tools','private']: (ROOT/sub).mkdir(parents=True,exist_ok=True) os.chmod(ROOT/'private',0o700) entries=[ ('SHA256SUMS','https://cloud-images.ubuntu.com/noble/20260911/SHA256SUMS',100000), ('SHA256SUMS.gpg','https://cloud-images.ubuntu.com/noble/20260911/SHA256SUMS.gpg',100000), ('ubuntu-cloud-key.asc','https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xD2EB44626FDDC30B513D5BB71A5D6C4C7DB87C81',100000), ('noble-server-cloudimg-amd64.img','https://cloud-images.ubuntu.com/noble/20260911/noble-server-cloudimg-amd64.img',700*1024*1024), ('noble-server-cloudimg-amd64.manifest','https://cloud-images.ubuntu.com/noble/20260911/noble-server-cloudimg-amd64.manifest',100000), ('qemu-img-11.1.1-2-x86_64.pkg.tar.zst','https://geo.mirror.pkgbuild.com/extra/os/x86_64/qemu-img-11.1.1-2-x86_64.pkg.tar.zst',5*1024*1024), ('qemu-img-11.1.1-2-x86_64.pkg.tar.zst.sig','https://geo.mirror.pkgbuild.com/extra/os/x86_64/qemu-img-11.1.1-2-x86_64.pkg.tar.zst.sig',100000), ('qt-base-Updates.xml','https://download.qt.io/online/qtsdkrepository/linux_x64/desktop/qt6_6112/qt6_6112/Updates.xml',1000000), ('qt-webengine-Updates.xml','https://download.qt.io/online/qtsdkrepository/linux_x64/extensions/qtwebengine/6112/x86_64/Updates.xml',1000000), ('pycdlib.json','https://pypi.org/pypi/pycdlib/1.14.0/json',1000000)] def fetch(e): name,url,limit=e;p=D/name;start=time.time();h=hashlib.sha256();n=0 if p.exists(): with p.open('rb') as f: for b in iter(lambda:f.read(1024*1024),b''):h.update(b);n+=len(b) return dict(name=name,url=url,size=n,sha256=h.hexdigest(),reused=True) try: with urllib.request.urlopen(url,timeout=60) as r,p.with_suffix(p.suffix+'.part').open('wb') as f: final=r.url while True: b=r.read(1024*1024) if not b:break n+=len(b) if n>limit:raise RuntimeError('download size bound exceeded') f.write(b);h.update(b) p.with_suffix(p.suffix+'.part').rename(p) print(name,n,'downloaded',flush=True) return dict(name=name,url=url,finalUrl=final,size=n,sha256=h.hexdigest(),seconds=round(time.time()-start,3)) except Exception as ex: print(name,'FAILED',str(ex),flush=True);return dict(name=name,url=url,error=str(ex)) with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool: records=list(pool.map(fetch,entries)) (ROOT/'metadata/downloads.json').write_text(json.dumps(records,indent=2)+'\n') if any('error' in e for e in records):raise SystemExit(1)