Track Debian QEMU revisions through verified immutable snapshots

This commit is contained in:
Scott Jones committed 2026-09-23 23:26:34 -04:00
1 parent fe0cf953d7
commit aa64ec9a4f
8 files changed
+272 -15

No files matched your search

@@ -1,4 +1,3 @@
{
"source": "local",
"sync": false
"source": "local"
}
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Track Debian 13 ARM64 QEMU revisions, verifying an immutable snapshot first."""
import functools
import hashlib
import json
import lzma
from pathlib import Path
import re
import subprocess
import sys
import urllib.parse
import urllib.request
FEEDS = [
'https://deb.debian.org/debian/dists/trixie/main/binary-arm64/Packages.xz',
'https://deb.debian.org/debian/dists/trixie-updates/main/binary-arm64/Packages.xz',
'https://security.debian.org/debian-security/dists/trixie-security/main/binary-arm64/Packages.xz',
]
def fetch(url):
with urllib.request.urlopen(url, timeout=120) as response:
if not response.url.startswith('https://'):
raise ValueError('Insecure redirect')
return response.read()
def parts(version):
match = re.fullmatch(r'(?:(\d+):)?([0-9][A-Za-z0-9.+~]*?)-([A-Za-z0-9.+~]+)', version)
if not match:
raise ValueError('Unsupported Debian version: ' + version)
return int(match[1] or '0'), match[2], match[3]
def segment_cmp(a, b):
# Debian policy: tilde precedes everything, then end/digits, letters,
# then other characters; digit runs are compared numerically.
def order(c):
if c == '~': return -1
if not c or c.isdigit(): return 0
return ord(c) if c.isalpha() else ord(c) + 256
while a or b:
while (a and not a[0].isdigit()) or (b and not b[0].isdigit()):
x, y = order(a[:1]), order(b[:1])
if x != y: return (x > y) - (x < y)
a, b = a[1:], b[1:]
x = re.match(r'\d*', a)[0]
y = re.match(r'\d*', b)[0]
nx, ny = int(x or '0'), int(y or '0')
if nx != ny: return (nx > ny) - (nx < ny)
a, b = a[len(x):], b[len(y):]
return 0
def compare(a, b):
ea, ua, ra = parts(a)
eb, ub, rb = parts(b)
return (ea > eb) - (ea < eb) or segment_cmp(ua, ub) or segment_cmp(ra, rb)
def package_version(version):
epoch, upstream, revision = parts(version)
# Stable releases only. Fail visibly on prerelease/repack conventions that
# need a reviewed Arch ordering rather than silently misordering them.
if '~' in version:
raise ValueError('Debian prerelease needs a reviewed Arch version mapping')
return f'{epoch}.{upstream}.{revision}'
def records(data):
found = []
for stanza in re.split(r'\n\s*\n', lzma.decompress(data).decode()):
fields = dict(re.findall(r'^([A-Za-z0-9-]+): (.*)$', stanza, re.M))
if fields.get('Package') != 'qemu-user': continue
if fields.get('Architecture') != 'arm64': raise ValueError('Wrong architecture')
parts(fields['Version'])
if not re.fullmatch(r'[0-9a-f]{64}', fields.get('SHA256', '')):
raise ValueError('Missing/malformed SHA256')
if not re.fullmatch(r'[1-9][0-9]*', fields.get('Size', '')):
raise ValueError('Missing/malformed package size')
found.append(fields)
return found
def discover(current, current_pkgver, current_hash, get=fetch):
candidates = [row for url in FEEDS for row in records(get(url))]
if not candidates: raise ValueError('No ARM64 qemu-user package in Debian feeds')
selected = max(candidates, key=functools.cmp_to_key(lambda a,b: compare(a['Version'], b['Version'])))
version, checksum = selected['Version'], selected['SHA256']
if any(row['SHA256'] != checksum for row in candidates if row['Version'] == version):
raise ValueError('Debian feeds disagree on the selected checksum')
ordering = compare(version, current)
if ordering < 0: raise ValueError('Debian feeds are older than the pinned recipe')
if ordering == 0:
if checksum != current_hash: raise ValueError('Checksum changed for the pinned Debian revision')
return {}
pkgver = package_version(version)
if int(subprocess.check_output(['vercmp', pkgver, current_pkgver], text=True)) <= 0:
raise ValueError('New Debian revision does not advance Arch version; review mapping')
api = 'https://snapshot.debian.org/mr/binary/qemu-user/' + urllib.parse.quote(version, safe='') + '/binfiles'
document = json.loads(get(api))
if document.get('binary') != 'qemu-user' or document.get('binary_version') != version:
raise ValueError('Snapshot revision differs')
hashes = {row['hash'] for row in document['result'] if row['architecture'] == 'arm64'}
if len(hashes) != 1 or not re.fullmatch(r'[0-9a-f]{40}', next(iter(hashes), '')):
raise ValueError('Missing/ambiguous ARM64 snapshot')
snapshot = hashes.pop()
archive = get('https://snapshot.debian.org/file/' + snapshot)
if len(archive) != int(selected['Size']) or hashlib.sha256(archive).hexdigest() != checksum or hashlib.sha1(archive).hexdigest() != snapshot:
raise ValueError('Snapshot bytes differ from Debian package metadata')
return {'pkgver': pkgver, 'variables': {'_debver': version, '_snapshot': snapshot},
'sha256sums': {'aarch64': [checksum]}}
def main():
recipe = Path('PKGBUILD').read_text()
def scalar(name):
match = re.search(r'^' + name + r'=[\"\']?([^\"\'\n]+)', recipe, re.M)
if not match: raise ValueError('Missing recipe scalar: ' + name)
return match[1]
checksum = re.search(r"^sha256sums_aarch64=\('([a-f0-9]{64})'\)", recipe, re.M)
if not checksum: raise ValueError('Missing current ARM checksum')
print(json.dumps(discover(scalar('_debver'), scalar('pkgver'), checksum[1])))
if __name__ == '__main__':
try: main()
except Exception as error:
sys.exit('QEMU Debian update failed: ' + str(error))
@@ -0,0 +1,3 @@
#!/bin/bash
set -euo pipefail
exec python3 .omarchy/upstream.py
+10 -5
View File
@@ -5,8 +5,12 @@
pkgbase=qemu-user-static
pkgname=('qemu-user-static' 'qemu-user-static-binfmt')
pkgver=10.0.11
_debver="1:10.0.11+ds-0+deb13u1+b1"
# One-time epoch moves from upstream-only to full Debian revision ordering.
# pkgver records Debian epoch.upstream.revision; updates also verify vercmp.
epoch=1
pkgver=1.10.0.13+ds.0+deb13u1
_debver=1:10.0.13+ds-0+deb13u1
_snapshot=02b553a4532192bc999d15408d1e56d7b254906f
pkgrel=1
pkgdesc="A generic and open source machine emulator and virtualizer - static user-mode emulation"
arch=('aarch64')
@@ -14,10 +18,11 @@ url="https://www.qemu.org"
license=('GPL-2.0-only' 'LGPL-2.1-only')
source=("qemu-binfmt-conf.sh")
# Debian removes superseded pool files. Pin the official archive snapshot so
# the same checksum-verified binary remains rebuildable.
source_aarch64=("qemu-user_${pkgver}_arm64.deb::https://snapshot.debian.org/archive/debian/20260713T202617Z/pool/main/q/qemu/qemu-user_${_debver#*:}_arm64.deb")
# the same checksum-verified binary remains rebuildable. _snapshot is the
# content-addressed snapshot SHA1; SHA256 below remains the integrity check.
source_aarch64=("qemu-user_${pkgver}_arm64.deb::https://snapshot.debian.org/file/$_snapshot")
sha256sums=('5e12ef484b8364e1ba7bae4acda837d2e6320ffbeb7fab341034124553cdb137')
sha256sums_aarch64=('6c9483063bf60f37fe181ead911251deabe40c893ea5829bc34b0b7b88913b6a')
sha256sums_aarch64=('c73711af02b97cd5e2667e735d9c06890c57165517110ca4e646c95a7083158d')
noextract=("qemu-user_${pkgver}_arm64.deb")
prepare() {
+7 -7
View File
@@ -1,13 +1,13 @@
# ARM static QEMU candidate
# ARM static QEMU
This split recipe provides `qemu-user-static` and `qemu-user-static-binfmt` on aarch64, where Arch Linux ARM does not supply the static emulator packages requested by Omarchy's default package set. It repackages Debian's checksum-pinned ARM64 binary; it does not compile QEMU or replace upstream's build-worker emulation setup.
This aarch64-only split recipe provides qemu-user-static and qemu-user-static-binfmt using Debian’s static ARM64 binaries. It preserves Marcelo’s recipe, imported through omarchy-mac/omarchy-pkgs-aarch64#61, including licensing and binfmt behavior. It does not change x86 packages or the build workers.
Imported from Marcelo's `maralcbr/omarchy-pkgs` revision `83973903b7deb9b56ce75f02b432fba0561d6293`, through the validated candidate recipe in `omarchy-mac/omarchy-pkgs-aarch64` (#61). Debian copyright and upstream licenses are retained. The immutable Debian snapshot keeps the pinned binary available when the rolling pool removes old revisions.
## Updates
## Draft readiness gates
The package-local upstream hook checks Debian 13 (trixie), trixie-updates, and trixie-security ARM64 indexes. It compares full Debian versions, including epochs, security revisions and binary rebuilds, and resolves the selected binary through snapshot.debian.org’s API. The immutable content-addressed archive must match the index’s size and SHA256 as well as the snapshot SHA1 before an update is returned.
Automatic sync is deliberately disabled in this initial draft. This does **not** detect new releases or security updates. Do not mark this PR ready until a package-local, fixture-tested update integration handles Debian's full version (epoch, upstream version, Debian security revision and architecture rebuild), immutable archive location and SHA256 together. A new Debian revision at the same QEMU version must produce a forward package version; repeated checks must be idempotent. Missing or malformed metadata, unavailable snapshots and checksum inconsistencies must leave the recipe unchanged and fail visibly.
A one-time Arch epoch of 1 moves away from the previous upstream-only version. pkgver encodes Debian epoch.upstream.revision. The hook requires both Debian ordering and pacman vercmp to advance; unfamiliar prerelease conventions or ordering discrepancies fail for manual review. Debian distribution upgrades are explicit recipe changes, not automatic jumps to testing/unstable.
The existing simple custom-hook contract updates pkgver and SHA256 arrays but cannot update `_debver` and the snapshot timestamp together. Do not add a misleading upstream-version-only watch. Review the smallest compatible integration with upstream before removing the hold. Once enabled, updates should join upstream's existing six-hour update PR workflow; GitHub notification settings determine who sees those PRs. Until then, check Debian's package and security updates manually.
The existing six-hour upstream update PR workflow discovers the hook. Its pkgver, _debver, _snapshot and ARM checksum are applied atomically through the normal sync interface. Repeated checks are idempotent. Feed failures, malformed metadata, conflicting checksums and unavailable/corrupt snapshots fail visibly before recipe mutation. No update installs packages or registers binfmt rules.
Before readiness, build both outputs using upstream's ARM builder and test installation in a disposable ARM VM. Verify AArch64 static ELF executables, exact interpreter paths and ownership, the absence of native AArch64 binfmt rules, execution of a known foreign-architecture program both directly and through binfmt, and clean package removal. Never register binfmt rules on the developer host or assume a container isolates that kernel state. Recipe generation only writes rules into the package staging directory.
Offline fixtures cover version/epoch/security/binNMU updates, security-feed selection, unchanged versions, metadata and snapshot failures, atomic application, and hostile/missing scalar rejection. Existing GUI-independent VM qualification and packaging evidence are recorded in the PR; new binaries still receive build and runtime checks before publication.