summaryrefslogtreecommitdiffstats
path: root/scripts/reproducible-check
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/reproducible-check')
-rwxr-xr-xscripts/reproducible-check196
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)