blob: c235c139ed1cdc8ee2877e5598fb24e194a7db9f [file] [log] [blame]
Brad Bishop15ae2502019-06-18 21:44:24 -04001#
2# SPDX-License-Identifier: MIT
3#
4# Copyright 2019 by Garmin Ltd. or its subsidiaries
5
6from oeqa.selftest.case import OESelftestTestCase
7from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars
8import functools
9import multiprocessing
10import textwrap
Brad Bishop79641f22019-09-10 07:20:22 -040011import json
Brad Bishop15ae2502019-06-18 21:44:24 -040012import unittest
13
14MISSING = 'MISSING'
15DIFFERENT = 'DIFFERENT'
16SAME = 'SAME'
17
18@functools.total_ordering
19class CompareResult(object):
20 def __init__(self):
21 self.reference = None
22 self.test = None
23 self.status = 'UNKNOWN'
24
25 def __eq__(self, other):
26 return (self.status, self.test) == (other.status, other.test)
27
28 def __lt__(self, other):
29 return (self.status, self.test) < (other.status, other.test)
30
31class PackageCompareResults(object):
32 def __init__(self):
33 self.total = []
34 self.missing = []
35 self.different = []
36 self.same = []
37
38 def add_result(self, r):
39 self.total.append(r)
40 if r.status == MISSING:
41 self.missing.append(r)
42 elif r.status == DIFFERENT:
43 self.different.append(r)
44 else:
45 self.same.append(r)
46
47 def sort(self):
48 self.total.sort()
49 self.missing.sort()
50 self.different.sort()
51 self.same.sort()
52
53 def __str__(self):
54 return 'same=%i different=%i missing=%i total=%i' % (len(self.same), len(self.different), len(self.missing), len(self.total))
55
56def compare_file(reference, test, diffutils_sysroot):
57 result = CompareResult()
58 result.reference = reference
59 result.test = test
60
61 if not os.path.exists(reference):
62 result.status = MISSING
63 return result
64
65 r = runCmd(['cmp', '--quiet', reference, test], native_sysroot=diffutils_sysroot, ignore_status=True)
66
67 if r.status:
68 result.status = DIFFERENT
69 return result
70
71 result.status = SAME
72 return result
73
74class ReproducibleTests(OESelftestTestCase):
Brad Bishop00e122a2019-10-05 11:10:57 -040075 package_classes = ['deb', 'ipk']
Brad Bishop15ae2502019-06-18 21:44:24 -040076 images = ['core-image-minimal']
77
78 def setUpLocal(self):
79 super().setUpLocal()
80 needed_vars = ['TOPDIR', 'TARGET_PREFIX', 'BB_NUMBER_THREADS']
81 bb_vars = get_bb_vars(needed_vars)
82 for v in needed_vars:
83 setattr(self, v.lower(), bb_vars[v])
84
Brad Bishop79641f22019-09-10 07:20:22 -040085 self.extrasresults = {}
86 self.extrasresults.setdefault('reproducible.rawlogs', {})['log'] = ''
87 self.extrasresults.setdefault('reproducible', {}).setdefault('files', {})
Brad Bishop15ae2502019-06-18 21:44:24 -040088
89 def append_to_log(self, msg):
Brad Bishop79641f22019-09-10 07:20:22 -040090 self.extrasresults['reproducible.rawlogs']['log'] += msg
Brad Bishop15ae2502019-06-18 21:44:24 -040091
92 def compare_packages(self, reference_dir, test_dir, diffutils_sysroot):
93 result = PackageCompareResults()
94
95 old_cwd = os.getcwd()
96 try:
97 file_result = {}
98 os.chdir(test_dir)
99 with multiprocessing.Pool(processes=int(self.bb_number_threads or 0)) as p:
100 for root, dirs, files in os.walk('.'):
101 async_result = []
102 for f in files:
103 reference_path = os.path.join(reference_dir, root, f)
104 test_path = os.path.join(test_dir, root, f)
105 async_result.append(p.apply_async(compare_file, (reference_path, test_path, diffutils_sysroot)))
106
107 for a in async_result:
108 result.add_result(a.get())
109
110 finally:
111 os.chdir(old_cwd)
112
113 result.sort()
114 return result
115
Brad Bishop79641f22019-09-10 07:20:22 -0400116 def write_package_list(self, package_class, name, packages):
117 self.extrasresults['reproducible']['files'].setdefault(package_class, {})[name] = [
118 {'reference': p.reference, 'test': p.test} for p in packages]
119
Brad Bishop15ae2502019-06-18 21:44:24 -0400120 def test_reproducible_builds(self):
121 capture_vars = ['DEPLOY_DIR_' + c.upper() for c in self.package_classes]
122
Brad Bishop15ae2502019-06-18 21:44:24 -0400123 # Build native utilities
Brad Bishop79641f22019-09-10 07:20:22 -0400124 self.write_config('')
Brad Bishop15ae2502019-06-18 21:44:24 -0400125 bitbake("diffutils-native -c addto_recipe_sysroot")
126 diffutils_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "diffutils-native")
127
Brad Bishop79641f22019-09-10 07:20:22 -0400128 # Reproducible builds should not pull from sstate or mirrors, but
129 # sharing DL_DIR is fine
130 common_config = textwrap.dedent('''\
131 INHERIT += "reproducible_build"
132 PACKAGE_CLASSES = "%s"
Brad Bishop15ae2502019-06-18 21:44:24 -0400133 SSTATE_DIR = "${TMPDIR}/sstate"
Brad Bishop79641f22019-09-10 07:20:22 -0400134 ''') % (' '.join('package_%s' % c for c in self.package_classes))
135
136 # Perform a build.
137 reproducibleA_tmp = os.path.join(self.topdir, 'reproducibleA', 'tmp')
138 if os.path.exists(reproducibleA_tmp):
139 bb.utils.remove(reproducibleA_tmp, recurse=True)
140
141 self.write_config((textwrap.dedent('''\
142 TMPDIR = "%s"
143 ''') % reproducibleA_tmp) + common_config)
144 vars_A = get_bb_vars(capture_vars)
Brad Bishop15ae2502019-06-18 21:44:24 -0400145 bitbake(' '.join(self.images))
146
Brad Bishop79641f22019-09-10 07:20:22 -0400147 # Perform another build.
148 reproducibleB_tmp = os.path.join(self.topdir, 'reproducibleB', 'tmp')
149 if os.path.exists(reproducibleB_tmp):
150 bb.utils.remove(reproducibleB_tmp, recurse=True)
151
152 self.write_config((textwrap.dedent('''\
153 SSTATE_MIRROR = ""
154 TMPDIR = "%s"
155 ''') % reproducibleB_tmp) + common_config)
156 vars_B = get_bb_vars(capture_vars)
157 bitbake(' '.join(self.images))
158
159 # NOTE: The temp directories from the reproducible build are purposely
160 # kept after the build so it can be diffed for debugging.
161
Brad Bishop15ae2502019-06-18 21:44:24 -0400162 for c in self.package_classes:
Brad Bishop79641f22019-09-10 07:20:22 -0400163 with self.subTest(package_class=c):
164 package_class = 'package_' + c
Brad Bishop15ae2502019-06-18 21:44:24 -0400165
Brad Bishop79641f22019-09-10 07:20:22 -0400166 deploy_A = vars_A['DEPLOY_DIR_' + c.upper()]
167 deploy_B = vars_B['DEPLOY_DIR_' + c.upper()]
Brad Bishop15ae2502019-06-18 21:44:24 -0400168
Brad Bishop79641f22019-09-10 07:20:22 -0400169 result = self.compare_packages(deploy_A, deploy_B, diffutils_sysroot)
Brad Bishop15ae2502019-06-18 21:44:24 -0400170
Brad Bishop79641f22019-09-10 07:20:22 -0400171 self.logger.info('Reproducibility summary for %s: %s' % (c, result))
Brad Bishop15ae2502019-06-18 21:44:24 -0400172
Brad Bishop79641f22019-09-10 07:20:22 -0400173 self.append_to_log('\n'.join("%s: %s" % (r.status, r.test) for r in result.total))
Brad Bishop15ae2502019-06-18 21:44:24 -0400174
Brad Bishop79641f22019-09-10 07:20:22 -0400175 self.write_package_list(package_class, 'missing', result.missing)
176 self.write_package_list(package_class, 'different', result.different)
177 self.write_package_list(package_class, 'same', result.same)
178
179 if result.missing or result.different:
180 self.fail("The following %s packages are missing or different: %s" %
181 (c, ' '.join(r.test for r in (result.missing + result.different))))
Brad Bishop15ae2502019-06-18 21:44:24 -0400182