summaryrefslogtreecommitdiff
path: root/depcheck.py
diff options
context:
space:
mode:
authorHelmut Grohne <helmut@subdivi.de>2019-02-03 12:42:23 +0100
committerHelmut Grohne <helmut@subdivi.de>2019-02-03 12:42:23 +0100
commita3cc49725febb2cca1c915ef768604831563954f (patch)
tree5249650a54703ef9a10ffb808ec7f64007c616c9 /depcheck.py
downloadcrossqa-a3cc49725febb2cca1c915ef768604831563954f.tar.gz
initial checkin
Diffstat (limited to 'depcheck.py')
-rwxr-xr-xdepcheck.py445
1 files changed, 445 insertions, 0 deletions
diff --git a/depcheck.py b/depcheck.py
new file mode 100755
index 0000000..f1c1a1f
--- /dev/null
+++ b/depcheck.py
@@ -0,0 +1,445 @@
+#!/usr/bin/python3
+
+import collections
+import contextlib
+import datetime
+import hashlib
+import itertools
+import lzma
+import os.path
+import sqlite3
+import subprocess
+import tempfile
+import yaml
+
+import apt_pkg
+apt_pkg.init()
+version_compare = apt_pkg.version_compare
+import requests
+
+from common import decompress_stream, yield_lines
+
+BUILD_ARCH = "amd64"
+MIRROR = "http://proxy:3142/debian"
+PROFILES = frozenset(("cross", "nocheck"))
+
+CPUEntry = collections.namedtuple('CPUEntry',
+ 'debcpu gnucpu regex bits endianness')
+
+TupleEntry = collections.namedtuple('TupleEntry',
+ 'abi libc os cpu')
+
+class Architectures:
+ @staticmethod
+ def read_table(filename):
+ with open(filename) as f:
+ for line in f:
+ if not line.startswith("#"):
+ yield line.split()
+
+ def __init__(self, cputable="/usr/share/dpkg/cputable",
+ tupletable="/usr/share/dpkg/tupletable",
+ abitable="/usr/share/dpkg/abitable"):
+ self.cputable = {}
+ self.tupletable = {}
+ self.abitable = {}
+ self.read_cputable(cputable)
+ self.read_tupletable(tupletable)
+ self.read_abitable(abitable)
+
+ def read_cputable(self, cputable):
+ self.cputable.clear()
+ for values in self.read_table(cputable):
+ values[3] = int(values[3]) # bits
+ entry = CPUEntry(*values)
+ self.cputable[entry.debcpu] = entry
+
+ def read_tupletable(self, tupletable):
+ self.tupletable.clear()
+ for debtuple, debarch in self.read_table(tupletable):
+ if '<cpu>' in debtuple:
+ for cpu in self.cputable:
+ entry = TupleEntry(*debtuple.replace("<cpu>", cpu)
+ .split("-"))
+ self.tupletable[debarch.replace("<cpu>", cpu)] = entry
+ else:
+ self.tupletable[debarch] = TupleEntry(*debtuple.split("-"))
+
+ def read_abitable(self, abitable):
+ self.abitable.clear()
+ for arch, bits in self.read_table(abitable):
+ bits = int(bits)
+ self.abitable[arch] = bits
+
+ def match(self, arch, pattern):
+ parts = pattern.split("-")
+ if not "any" in parts:
+ return pattern == arch
+ while len(parts) < 4:
+ parts.insert(0, "any")
+ entry = self.tupletable[arch]
+ return all(parts[i] in (entry[i], "any") for i in range(4))
+
+ def getendianness(self, arch):
+ return self.cputable[self.tupletable[arch].cpu].endianness
+
+architectures = Architectures()
+arch_match = architectures.match
+
+def call_dose_builddebcheck(arguments):
+ """
+ @type arguments: [str]
+ @param arguments: command line arguments to dose-builddebcheck
+ @returns: an iterable over loaded yaml documents. The first document
+ is the header, all other documents are per-package.
+ @raises subprocess.CalledProcessError: if dose errors out
+ """
+ cmd = ["dose-builddebcheck"]
+ cmd.extend(arguments)
+
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+
+ lines = []
+ for line in proc.stdout:
+ if line.startswith(b' '):
+ lines.append(line)
+ elif line == b' -\n':
+ yield yaml.load(b"".join(lines), Loader=yaml.CBaseLoader)
+ lines = []
+ proc.stdout.close()
+ if lines:
+ yield yaml.load(b"".join(lines), Loader=yaml.CSafeLoader)
+ if proc.wait() not in (0, 1):
+ raise subprocess.CalledProcessError(proc.returncode, cmd)
+
+def parse_deb822(iterable):
+ """Parse an iterable of bytes into an iterable of str-dicts."""
+ mapping = {}
+ key = None
+ value = None
+ for line in yield_lines(iterable):
+ line = line.decode("utf8")
+ if line == "\n":
+ if key is not None:
+ mapping[key] = value.strip()
+ key = None
+ yield mapping
+ mapping = {}
+ elif key and line.startswith((" ", "\t")):
+ value += line
+ else:
+ if key is not None:
+ mapping[key] = value.strip()
+ try:
+ key, value = line.split(":", 1)
+ except ValueError:
+ raise ValueError("invalid input line %r" % line)
+ if key is not None:
+ mapping[key] = value.strip()
+ if mapping:
+ yield mapping
+
+def serialize_deb822(dct):
+ """Serialize a byte-dict into a single bytes object."""
+ return "".join(map("%s: %s\n".__mod__, dct.items())) + "\n"
+
+class HashSumMismatch(Exception):
+ pass
+
+def hash_check(iterable, hashobj, expected_digest):
+ """Wraps an iterable that yields bytes. It doesn't modify the sequence,
+ but on the final element it verifies that the concatenation of bytes
+ yields an expected digest value. Upon failure, the final next() results in
+ a HashSumMismatch rather than StopIteration.
+ """
+ for data in iterable:
+ hashobj.update(data)
+ yield data
+ if hashobj.hexdigest() != expected_digest:
+ raise HashSumMismatch()
+
+def parse_date(s):
+ return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S %Z")
+
+class GPGV:
+ def __init__(self, files=("/etc/apt/trusted.gpg",),
+ partsdir="/etc/apt/trusted.gpg.d"):
+ candidates = list(files)
+ candidates.extend(os.path.join(partsdir, e)
+ for e in os.listdir(partsdir))
+ self.keyrings = list(filter(lambda f: os.access(f, os.R_OK),
+ candidates))
+
+ def verify(self, content):
+ cmdline = ["gpgv", "--quiet", "--weak-digest", "SHA1", "--output", "-"]
+ for keyring in self.keyrings:
+ cmdline.extend(("--keyring", keyring))
+ proc = subprocess.Popen(cmdline, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, _ = proc.communicate(content)
+ if proc.wait() != 0:
+ raise ValueError("signature verififcation failed")
+ return stdout
+
+class DebianMirror:
+ hashfunc = "SHA256"
+ def __init__(self, uri, dist="sid"):
+ self.uri = uri
+ self.dist = dist
+ self.releasetime = None
+ self.byhash = None
+ self.files = {}
+
+ @staticmethod
+ def get_all_keyrings():
+ yield "/etc/apt/trusted.gpg"
+ partsdir = "/etc/apt/trusted.gpg.d"
+ try:
+ for e in os.listdir(partsdir):
+ yield os.path.join(partsdir, e)
+ except FileNotFoundError:
+ pass
+
+ @staticmethod
+ def get_keyrings():
+ return filter(lambda f: os.access(f, os.R_OK),
+ DebianMirror.get_all_keyrings())
+
+ def get_uri(self, filename):
+ return "%s/dists/%s/%s" % (self.uri, self.dist, filename)
+
+ def fetch_release(self):
+ resp = requests.get(self.get_uri("InRelease"))
+ resp.raise_for_status()
+ return GPGV().verify(resp.content)
+
+ def parse_release(self, content):
+ info, = list(parse_deb822([content]))
+ self.releasetime = parse_date(info["Date"])
+ valid_until = parse_date(info["Valid-Until"])
+ now = datetime.datetime.utcnow()
+ if self.releasetime > now:
+ raise ValueError("release file generated in future")
+ if valid_until < now:
+ raise ValueError("release signature expired")
+ self.byhash = info.pop("Acquire-By-Hash", "no") == "yes"
+ self.files = {}
+ for line in info[self.hashfunc].splitlines():
+ parts = line.split()
+ if not parts:
+ continue
+ if len(parts) != 3:
+ raise ValueError("invalid %s line %r" % (self.hashfunc, line))
+ self.files[parts[2]] = parts[0]
+
+ def update_release(self):
+ self.parse_release(self.fetch_release())
+
+ def fetch_list(self, listname):
+ if listname + ".xz" in self.files:
+ listname += ".xz"
+ wrapper = lambda i: decompress_stream(i, lzma.LZMADecompressor())
+ else:
+ wrapper = lambda i: i
+ hashvalue = self.files[listname]
+ if self.byhash:
+ listname = "%s/by-hash/%s/%s" % (os.path.dirname(listname),
+ self.hashfunc, hashvalue)
+ with requests.get(self.get_uri(listname), stream=True) as resp:
+ resp.raise_for_status()
+ it = resp.iter_content(65536)
+ it = hash_check(it, hashlib.new(self.hashfunc), hashvalue)
+ yield from wrapper(it)
+
+ def fetch_sources(self, component="main"):
+ return self.fetch_list("%s/source/Sources" % component)
+
+ def fetch_binaries(self, architecture, component="main"):
+ return self.fetch_list("%s/binary-%s/Packages" %
+ (component, architecture))
+
+binfields = frozenset((
+ "Architecture",
+ "Breaks",
+ "Conflicts",
+ "Depends",
+ "Essential",
+ "Multi-Arch",
+ "Package",
+ "Pre-Depends",
+ "Provides",
+ "Version",
+))
+
+srcdepfields = frozenset((
+ "Build-Conflicts",
+ "Build-Conflicts-Arch",
+ "Build-Depends",
+ "Build-Depends-Arch",
+))
+srcfields = srcdepfields.union((
+ "Architecture",
+ "Package",
+ "Version",
+))
+
+bad_foreign_packages = frozenset((
+ "flex-old", # cannot execute /usr/bin/flex
+ "icmake", # cannot execute /usr/bin/icmake, build system
+ "jam", # cannot execute /usr/bin/jam, build system
+ "libtool-bin", # #836123
+ "python2.7-minimal", # fails postinst
+ "python3.6-minimal", # fails postinst
+ "python3.7-minimal", # fails postinst
+ "swi-prolog-nox", # fails postinst
+ "xrdp", # fails postinst
+ "libgvc6", # fails postinst
+))
+
+def strip_dict(dct, keepfields):
+ keys = set(dct.keys())
+ keys.difference_update(keepfields)
+ for k in keys:
+ del dct[k]
+
+def strip_alternatvies(dct, fields):
+ for f in fields:
+ try:
+ value = dct[f]
+ except KeyError:
+ continue
+ dct[f] = ",".join(dep.split("|", 1)[0]
+ for dep in value.split(","))
+
+def latest_versions(pkgs):
+ packages = {}
+ for p in pkgs:
+ name = p["Package"]
+ try:
+ if version_compare(packages[name]["Version"], p["Version"]) > 0:
+ continue
+ except KeyError:
+ pass
+ packages[name] = p
+ return (p for p in packages.values()
+ if "Package" in p and not "Negative-Entry" in p)
+
+def make_binary_list_build(mirror, arch):
+ for p in parse_deb822(mirror.fetch_binaries(BUILD_ARCH)):
+ if p["Package"].startswith("crossbuild-essential-"):
+ if p["Package"] != "crossbuild-essential-" + arch:
+ continue
+ p["Depends"] += ", libc-dev:%s, libstdc++-dev:%s" % (arch, arch)
+ strip_dict(p, binfields)
+ yield p
+
+def make_binary_list_host(mirror, arch):
+ for p in parse_deb822(mirror.fetch_binaries(arch)):
+ if p["Architecture"] == "all":
+ continue
+ if p.get("Multi-Arch") == "foreign":
+ continue
+ if p.get("Essential") == "yes":
+ continue
+ if p["Package"] in bad_foreign_packages:
+ continue
+ strip_dict(p, binfields)
+ yield p
+
+def make_binary_list(mirror, arch):
+ return itertools.chain(make_binary_list_build(mirror, arch),
+ make_binary_list_host(mirror, arch))
+
+def make_source_list(mirror, arch):
+ for p in parse_deb822(mirror.fetch_sources()):
+ if p.get("Extra-Source-Only") == "yes":
+ continue
+ if any(arch_match(arch, pattern)
+ for pattern in p["Architecture"].split()):
+ strip_dict(p, srcfields)
+ strip_alternatvies(p, srcdepfields)
+ yield p
+ else:
+ # dummy entry preventing older matching versions
+ yield {"Package": p["Package"], "Version": p["Version"],
+ "Negative-Entry": "yes"}
+
+def check_bdsat(mirror, arch):
+ cmd = [
+ "--deb-native-arch=" + BUILD_ARCH,
+ "--deb-host-arch=" + arch,
+ "--deb-drop-b-d-indep",
+ "--deb-profiles=" + ",".join(PROFILES),
+ "--successes",
+ "--failures",
+ "--explain",
+ "--explain-minimal",
+ "--deb-emulate-sbuild",
+ ]
+
+ with tempfile.NamedTemporaryFile("w", encoding="utf8") as bintmp, \
+ tempfile.NamedTemporaryFile("w", encoding="utf8") as srctmp:
+ for p in make_binary_list(mirror, arch):
+ bintmp.write(serialize_deb822(p))
+ bintmp.flush()
+ cmd.append(bintmp.name)
+
+ for p in latest_versions(make_source_list(mirror, arch)):
+ srctmp.write(serialize_deb822(p))
+ srctmp.flush()
+ cmd.append(srctmp.name)
+
+ dose_result = call_dose_builddebcheck(cmd)
+ next(dose_result) # skip header
+ for d in dose_result:
+ if d["status"] == "ok":
+ yield (d["package"], d["version"], True, None)
+ else:
+ r = d["reasons"][0]
+ if "missing" in r:
+ reason = "missing %s" % r["missing"]["pkg"]["unsat-dependency"].split()[0].split(":", 1)[0]
+ elif "conflict" in r:
+ r = r["conflict"]["pkg1"]["unsat-conflict"]
+ reason = "skew " if ' (!= ' in r else "conflict "
+ reason += r.split()[0].split(':', 1)[0]
+ else:
+ assert False
+ yield (d["package"], d["version"], False, reason)
+
+def update_depcheck(mirror, db, architecture):
+ now = datetime.datetime.utcnow()
+ mirror.update_release()
+ state = {}
+ for source, version, satisfiable, reason in check_bdsat(mirror, architecture):
+ state[source] = (version, satisfiable, reason)
+ with contextlib.closing(db.cursor()) as cur:
+ cur.execute("BEGIN;")
+ cur.execute("SELECT source, version, satisfiable, reason FROM depstate WHERE architecture = ?;",
+ (architecture,))
+ for source, version, satisfiable, reason in list(cur.fetchall()):
+ if state.get(source) == (version, satisfiable, reason):
+ del state[source]
+ else:
+ cur.execute("DELETE FROM depstate WHERE source = ? AND version = ? AND architecture = ?;",
+ (source, version, architecture))
+ cur.executemany("INSERT INTO depstate (source, architecture, version, satisfiable, reason) VALUES (?, ?, ?, ?, ?);",
+ ((source, architecture, version, satisfiable, reason)
+ for source, (version, satisfiable, reason) in state.items()))
+ cur.execute("UPDATE depcheck SET releasetime = ?, updatetime = ?, giveback = 0 WHERE architecture = ?",
+ (mirror.releasetime, now, architecture))
+ db.commit()
+
+def main():
+ mirror = DebianMirror(MIRROR)
+ mirror.update_release()
+ db = sqlite3.connect("db", detect_types=sqlite3.PARSE_DECLTYPES)
+ cur = db.cursor()
+ cur.execute("SELECT architecture, releasetime, updatetime, giveback FROM depcheck;")
+ lastupdate = datetime.datetime.utcnow() - datetime.timedelta(hours=6)
+ for architecture, releasetime, updatetime, giveback in list(cur.fetchall()):
+ if giveback or updatetime < lastupdate or releasetime < mirror.releasetime:
+ print("update %s" % architecture)
+ update_depcheck(mirror, db, architecture)
+
+if __name__ == "__main__":
+ main()