blob: a9110565a946acc932239991164b9d3b78b72502 [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
Brad Bishop1d80a2e2019-11-15 16:35:03 -05008import bb.utils
Brad Bishop15ae2502019-06-18 21:44:24 -04009import functools
10import multiprocessing
11import textwrap
Brad Bishop79641f22019-09-10 07:20:22 -040012import json
Brad Bishop15ae2502019-06-18 21:44:24 -040013import unittest
Brad Bishop1d80a2e2019-11-15 16:35:03 -050014import tempfile
15import shutil
16import stat
17import os
Brad Bishop15ae2502019-06-18 21:44:24 -040018
19MISSING = 'MISSING'
20DIFFERENT = 'DIFFERENT'
21SAME = 'SAME'
22
23@functools.total_ordering
24class CompareResult(object):
25 def __init__(self):
26 self.reference = None
27 self.test = None
28 self.status = 'UNKNOWN'
29
30 def __eq__(self, other):
31 return (self.status, self.test) == (other.status, other.test)
32
33 def __lt__(self, other):
34 return (self.status, self.test) < (other.status, other.test)
35
36class PackageCompareResults(object):
37 def __init__(self):
38 self.total = []
39 self.missing = []
40 self.different = []
41 self.same = []
42
43 def add_result(self, r):
44 self.total.append(r)
45 if r.status == MISSING:
46 self.missing.append(r)
47 elif r.status == DIFFERENT:
48 self.different.append(r)
49 else:
50 self.same.append(r)
51
52 def sort(self):
53 self.total.sort()
54 self.missing.sort()
55 self.different.sort()
56 self.same.sort()
57
58 def __str__(self):
59 return 'same=%i different=%i missing=%i total=%i' % (len(self.same), len(self.different), len(self.missing), len(self.total))
60
61def compare_file(reference, test, diffutils_sysroot):
62 result = CompareResult()
63 result.reference = reference
64 result.test = test
65
66 if not os.path.exists(reference):
67 result.status = MISSING
68 return result
69
70 r = runCmd(['cmp', '--quiet', reference, test], native_sysroot=diffutils_sysroot, ignore_status=True)
71
72 if r.status:
73 result.status = DIFFERENT
74 return result
75
76 result.status = SAME
77 return result
78
79class ReproducibleTests(OESelftestTestCase):
Brad Bishop00e122a2019-10-05 11:10:57 -040080 package_classes = ['deb', 'ipk']
Brad Bishop15ae2502019-06-18 21:44:24 -040081 images = ['core-image-minimal']
Brad Bishop1d80a2e2019-11-15 16:35:03 -050082 save_results = False
Brad Bishop15ae2502019-06-18 21:44:24 -040083
84 def setUpLocal(self):
85 super().setUpLocal()
86 needed_vars = ['TOPDIR', 'TARGET_PREFIX', 'BB_NUMBER_THREADS']
87 bb_vars = get_bb_vars(needed_vars)
88 for v in needed_vars:
89 setattr(self, v.lower(), bb_vars[v])
90
Brad Bishop79641f22019-09-10 07:20:22 -040091 self.extrasresults = {}
92 self.extrasresults.setdefault('reproducible.rawlogs', {})['log'] = ''
93 self.extrasresults.setdefault('reproducible', {}).setdefault('files', {})
Brad Bishop15ae2502019-06-18 21:44:24 -040094
95 def append_to_log(self, msg):
Brad Bishop79641f22019-09-10 07:20:22 -040096 self.extrasresults['reproducible.rawlogs']['log'] += msg
Brad Bishop15ae2502019-06-18 21:44:24 -040097
98 def compare_packages(self, reference_dir, test_dir, diffutils_sysroot):
99 result = PackageCompareResults()
100
101 old_cwd = os.getcwd()
102 try:
103 file_result = {}
104 os.chdir(test_dir)
105 with multiprocessing.Pool(processes=int(self.bb_number_threads or 0)) as p:
106 for root, dirs, files in os.walk('.'):
107 async_result = []
108 for f in files:
109 reference_path = os.path.join(reference_dir, root, f)
110 test_path = os.path.join(test_dir, root, f)
111 async_result.append(p.apply_async(compare_file, (reference_path, test_path, diffutils_sysroot)))
112
113 for a in async_result:
114 result.add_result(a.get())
115
116 finally:
117 os.chdir(old_cwd)
118
119 result.sort()
120 return result
121
Brad Bishop79641f22019-09-10 07:20:22 -0400122 def write_package_list(self, package_class, name, packages):
123 self.extrasresults['reproducible']['files'].setdefault(package_class, {})[name] = [
124 {'reference': p.reference, 'test': p.test} for p in packages]
125
Brad Bishop1d80a2e2019-11-15 16:35:03 -0500126 def copy_file(self, source, dest):
127 bb.utils.mkdirhier(os.path.dirname(dest))
128 shutil.copyfile(source, dest)
129
Brad Bishop15ae2502019-06-18 21:44:24 -0400130 def test_reproducible_builds(self):
131 capture_vars = ['DEPLOY_DIR_' + c.upper() for c in self.package_classes]
132
Brad Bishop1d80a2e2019-11-15 16:35:03 -0500133 if self.save_results:
134 save_dir = tempfile.mkdtemp(prefix='oe-reproducible-')
135 os.chmod(save_dir, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
136 self.logger.info('Non-reproducible packages will be copied to %s', save_dir)
137
Brad Bishop15ae2502019-06-18 21:44:24 -0400138 # Build native utilities
Brad Bishop79641f22019-09-10 07:20:22 -0400139 self.write_config('')
Brad Bishop15ae2502019-06-18 21:44:24 -0400140 bitbake("diffutils-native -c addto_recipe_sysroot")
141 diffutils_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "diffutils-native")
142
Brad Bishop79641f22019-09-10 07:20:22 -0400143 # Reproducible builds should not pull from sstate or mirrors, but
144 # sharing DL_DIR is fine
145 common_config = textwrap.dedent('''\
146 INHERIT += "reproducible_build"
147 PACKAGE_CLASSES = "%s"
Brad Bishop15ae2502019-06-18 21:44:24 -0400148 SSTATE_DIR = "${TMPDIR}/sstate"
Brad Bishop79641f22019-09-10 07:20:22 -0400149 ''') % (' '.join('package_%s' % c for c in self.package_classes))
150
151 # Perform a build.
152 reproducibleA_tmp = os.path.join(self.topdir, 'reproducibleA', 'tmp')
153 if os.path.exists(reproducibleA_tmp):
154 bb.utils.remove(reproducibleA_tmp, recurse=True)
155
156 self.write_config((textwrap.dedent('''\
157 TMPDIR = "%s"
158 ''') % reproducibleA_tmp) + common_config)
159 vars_A = get_bb_vars(capture_vars)
Brad Bishop15ae2502019-06-18 21:44:24 -0400160 bitbake(' '.join(self.images))
161
Brad Bishop79641f22019-09-10 07:20:22 -0400162 # Perform another build.
163 reproducibleB_tmp = os.path.join(self.topdir, 'reproducibleB', 'tmp')
164 if os.path.exists(reproducibleB_tmp):
165 bb.utils.remove(reproducibleB_tmp, recurse=True)
166
167 self.write_config((textwrap.dedent('''\
168 SSTATE_MIRROR = ""
169 TMPDIR = "%s"
170 ''') % reproducibleB_tmp) + common_config)
171 vars_B = get_bb_vars(capture_vars)
172 bitbake(' '.join(self.images))
173
174 # NOTE: The temp directories from the reproducible build are purposely
175 # kept after the build so it can be diffed for debugging.
176
Brad Bishop15ae2502019-06-18 21:44:24 -0400177 for c in self.package_classes:
Brad Bishop79641f22019-09-10 07:20:22 -0400178 with self.subTest(package_class=c):
179 package_class = 'package_' + c
Brad Bishop15ae2502019-06-18 21:44:24 -0400180
Brad Bishop79641f22019-09-10 07:20:22 -0400181 deploy_A = vars_A['DEPLOY_DIR_' + c.upper()]
182 deploy_B = vars_B['DEPLOY_DIR_' + c.upper()]
Brad Bishop15ae2502019-06-18 21:44:24 -0400183
Brad Bishop79641f22019-09-10 07:20:22 -0400184 result = self.compare_packages(deploy_A, deploy_B, diffutils_sysroot)
Brad Bishop15ae2502019-06-18 21:44:24 -0400185
Brad Bishop79641f22019-09-10 07:20:22 -0400186 self.logger.info('Reproducibility summary for %s: %s' % (c, result))
Brad Bishop15ae2502019-06-18 21:44:24 -0400187
Brad Bishop79641f22019-09-10 07:20:22 -0400188 self.append_to_log('\n'.join("%s: %s" % (r.status, r.test) for r in result.total))
Brad Bishop15ae2502019-06-18 21:44:24 -0400189
Brad Bishop79641f22019-09-10 07:20:22 -0400190 self.write_package_list(package_class, 'missing', result.missing)
191 self.write_package_list(package_class, 'different', result.different)
192 self.write_package_list(package_class, 'same', result.same)
193
Brad Bishop1d80a2e2019-11-15 16:35:03 -0500194 if self.save_results:
195 for d in result.different:
196 self.copy_file(d.reference, '/'.join([save_dir, d.reference]))
197 self.copy_file(d.test, '/'.join([save_dir, d.test]))
198
Brad Bishop79641f22019-09-10 07:20:22 -0400199 if result.missing or result.different:
200 self.fail("The following %s packages are missing or different: %s" %
201 (c, ' '.join(r.test for r in (result.missing + result.different))))
Brad Bishop15ae2502019-06-18 21:44:24 -0400202