#!/usr/bin/env python3 # # Copyright (C) 2017 Chris Lamb # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at # your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import argparse import bz2 import collections import json import logging import os import sys import time import apt import requests try: from xdg.BaseDirectory import xdg_cache_home except ImportError: print("This script requires the xdg python3 module.", file=sys.stderr) print("Please install the python3-xdg Debian package in order to use" "this utility.", file=sys.stderr) sys.exit(1) class ReproducibleCheck: HELP = """ Reports on the reproducible status of installed packages. For more details please see . """ NAME = os.path.basename(__file__) VERSION = 1 STATUS_URL = 'https://tests.reproducible-builds.org/debian/' \ 'reproducible-tracker.json.bz2' CACHE = os.path.join(xdg_cache_home, NAME, os.path.basename(STATUS_URL)) CACHE_AGE_SECONDS = 86400 @classmethod def parse(cls): parser = argparse.ArgumentParser(description=cls.HELP) parser.add_argument( '-d', '--debug', help="show debugging messages", default=False, action='store_true', ) parser.add_argument( '-r', '--raw', help="print unreproducible binary packages only (for dd-list -i)", default=False, action='store_true', ) parser.add_argument( '--version', help="print version and exit", default=False, action='store_true', ) return cls(parser.parse_args()) def __init__(self, args): self.args = args logging.basicConfig( format='%(asctime).19s %(levelname).1s: %(message)s', level=logging.DEBUG if args.debug else logging.INFO, ) self.log = logging.getLogger() def main(self): if self.args.version: print("{} version {}".format(self.NAME, self.VERSION)) return 0 self.update_cache() data = self.get_data() installed = self.get_installed_packages() unreproducible = {x: y for x, y in installed.items() if x in data} if self.args.raw: self.output_raw(unreproducible, installed) else: self.output_by_source(unreproducible, installed) return 0 def update_cache(self): self.log.debug("Checking cache file %s ...", self.CACHE) try: if os.path.getmtime(self.CACHE) >= \ time.time() - self.CACHE_AGE_SECONDS: self.log.debug("Cache is up to date") return except OSError: pass self.log.info("Updating cache...") response = requests.get(self.STATUS_URL) os.makedirs(os.path.dirname(self.CACHE), exist_ok=True) with open(self.CACHE, 'wb+') as f: f.write(response.content) def get_data(self): self.log.debug("Loading data from cache %s", self.CACHE) with bz2.open(self.CACHE) as f: return { (x['package'], y['architecture'], x['version']) for x in json.loads(f.read().decode('utf-8')) for y in x['architecture_details'] if y['status'] == 'unreproducible' } @staticmethod def get_installed_packages(): result = collections.defaultdict(list) for x in apt.Cache(): for y in x.versions: if not y.is_installed: continue key = (y.source_name, y.architecture, y.version) result[key].append(x.shortname) return result @staticmethod def output_by_source(unreproducible, installed): num_installed = sum(len(x) for x in installed.keys()) num_unreproducible = sum(len(x) for x in unreproducible.keys()) default_architecture = apt.apt_pkg.config.find('APT::Architecture') for key, binaries in sorted(unreproducible.items()): source, architecture, version = key binaries_fmt = '({}) '.format(', '.join(binaries)) \ if binaries != [source] else '' print("{}{} ({}) is unreproducible {}".format( source, '/{}'.format(architecture) if architecture != default_architecture else '', version, binaries_fmt, ), end='') print("".format( source, )) msg = "{}/{} ({:.2f}%) of installed binary packages are unreproducible." print(msg.format( num_unreproducible, num_installed, 100. * num_unreproducible / num_installed, )) @staticmethod def output_raw(unreproducible, installed): # pylint: disable=unused-argument for x in sorted(x for xs in unreproducible.values() for x in set(xs)): print(x) if __name__ == '__main__': try: sys.exit(ReproducibleCheck.parse().main()) except (KeyboardInterrupt, BrokenPipeError): sys.exit(1)