diff options
Diffstat (limited to 'scripts/reproducible-check')
-rwxr-xr-x | scripts/reproducible-check | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/scripts/reproducible-check b/scripts/reproducible-check new file mode 100755 index 0000000..64b6617 --- /dev/null +++ b/scripts/reproducible-check @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2017 Chris Lamb <lamby@debian.org> +# +# 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 <http://www.gnu.org/licenses/>. + +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 <https://reproducible-builds.org>. + """ + + 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("<https://tests.reproducible-builds.org/debian/{}>".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) |