| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 1 | # | 
 | 2 | # SPDX-License-Identifier: MIT | 
 | 3 | # | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 4 | # Copyright 2019-2020 by Garmin Ltd. or its subsidiaries | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 5 |  | 
 | 6 | from oeqa.selftest.case import OESelftestTestCase | 
 | 7 | from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars | 
| Brad Bishop | 1d80a2e | 2019-11-15 16:35:03 -0500 | [diff] [blame] | 8 | import bb.utils | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 9 | import functools | 
 | 10 | import multiprocessing | 
 | 11 | import textwrap | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 12 | import json | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 13 | import unittest | 
| Brad Bishop | 1d80a2e | 2019-11-15 16:35:03 -0500 | [diff] [blame] | 14 | import tempfile | 
 | 15 | import shutil | 
 | 16 | import stat | 
 | 17 | import os | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 18 | import datetime | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 19 |  | 
 | 20 | MISSING = 'MISSING' | 
 | 21 | DIFFERENT = 'DIFFERENT' | 
 | 22 | SAME = 'SAME' | 
 | 23 |  | 
 | 24 | @functools.total_ordering | 
 | 25 | class CompareResult(object): | 
 | 26 |     def __init__(self): | 
 | 27 |         self.reference = None | 
 | 28 |         self.test = None | 
 | 29 |         self.status = 'UNKNOWN' | 
 | 30 |  | 
 | 31 |     def __eq__(self, other): | 
 | 32 |         return (self.status, self.test) == (other.status, other.test) | 
 | 33 |  | 
 | 34 |     def __lt__(self, other): | 
 | 35 |         return (self.status, self.test) < (other.status, other.test) | 
 | 36 |  | 
 | 37 | class PackageCompareResults(object): | 
 | 38 |     def __init__(self): | 
 | 39 |         self.total = [] | 
 | 40 |         self.missing = [] | 
 | 41 |         self.different = [] | 
 | 42 |         self.same = [] | 
 | 43 |  | 
 | 44 |     def add_result(self, r): | 
 | 45 |         self.total.append(r) | 
 | 46 |         if r.status == MISSING: | 
 | 47 |             self.missing.append(r) | 
 | 48 |         elif r.status == DIFFERENT: | 
 | 49 |             self.different.append(r) | 
 | 50 |         else: | 
 | 51 |             self.same.append(r) | 
 | 52 |  | 
 | 53 |     def sort(self): | 
 | 54 |         self.total.sort() | 
 | 55 |         self.missing.sort() | 
 | 56 |         self.different.sort() | 
 | 57 |         self.same.sort() | 
 | 58 |  | 
 | 59 |     def __str__(self): | 
 | 60 |         return 'same=%i different=%i missing=%i total=%i' % (len(self.same), len(self.different), len(self.missing), len(self.total)) | 
 | 61 |  | 
 | 62 | def compare_file(reference, test, diffutils_sysroot): | 
 | 63 |     result = CompareResult() | 
 | 64 |     result.reference = reference | 
 | 65 |     result.test = test | 
 | 66 |  | 
 | 67 |     if not os.path.exists(reference): | 
 | 68 |         result.status = MISSING | 
 | 69 |         return result | 
 | 70 |  | 
 | 71 |     r = runCmd(['cmp', '--quiet', reference, test], native_sysroot=diffutils_sysroot, ignore_status=True) | 
 | 72 |  | 
 | 73 |     if r.status: | 
 | 74 |         result.status = DIFFERENT | 
 | 75 |         return result | 
 | 76 |  | 
 | 77 |     result.status = SAME | 
 | 78 |     return result | 
 | 79 |  | 
 | 80 | class ReproducibleTests(OESelftestTestCase): | 
| Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 81 |     package_classes = ['deb', 'ipk'] | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 82 |     images = ['core-image-minimal', 'core-image-sato', 'core-image-full-cmdline'] | 
| Brad Bishop | 1d80a2e | 2019-11-15 16:35:03 -0500 | [diff] [blame] | 83 |     save_results = False | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 84 |     if 'OEQA_DEBUGGING_SAVED_OUTPUT' in os.environ: | 
 | 85 |         save_results = os.environ['OEQA_DEBUGGING_SAVED_OUTPUT'] | 
 | 86 |  | 
 | 87 |     # This variable controls if one of the test builds is allowed to pull from | 
 | 88 |     # an sstate cache/mirror. The other build is always done clean as a point of | 
 | 89 |     # comparison. | 
 | 90 |     # If you know that your sstate archives are reproducible, enabling this | 
 | 91 |     # will test that and also make the test run faster. If your sstate is not | 
 | 92 |     # reproducible, disable this in your derived test class | 
 | 93 |     build_from_sstate = True | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 94 |  | 
 | 95 |     def setUpLocal(self): | 
 | 96 |         super().setUpLocal() | 
 | 97 |         needed_vars = ['TOPDIR', 'TARGET_PREFIX', 'BB_NUMBER_THREADS'] | 
 | 98 |         bb_vars = get_bb_vars(needed_vars) | 
 | 99 |         for v in needed_vars: | 
 | 100 |             setattr(self, v.lower(), bb_vars[v]) | 
 | 101 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 102 |         self.extraresults = {} | 
 | 103 |         self.extraresults.setdefault('reproducible.rawlogs', {})['log'] = '' | 
 | 104 |         self.extraresults.setdefault('reproducible', {}).setdefault('files', {}) | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 105 |  | 
 | 106 |     def append_to_log(self, msg): | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 107 |         self.extraresults['reproducible.rawlogs']['log'] += msg | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 108 |  | 
 | 109 |     def compare_packages(self, reference_dir, test_dir, diffutils_sysroot): | 
 | 110 |         result = PackageCompareResults() | 
 | 111 |  | 
 | 112 |         old_cwd = os.getcwd() | 
 | 113 |         try: | 
 | 114 |             file_result = {} | 
 | 115 |             os.chdir(test_dir) | 
 | 116 |             with multiprocessing.Pool(processes=int(self.bb_number_threads or 0)) as p: | 
 | 117 |                 for root, dirs, files in os.walk('.'): | 
 | 118 |                     async_result = [] | 
 | 119 |                     for f in files: | 
 | 120 |                         reference_path = os.path.join(reference_dir, root, f) | 
 | 121 |                         test_path = os.path.join(test_dir, root, f) | 
 | 122 |                         async_result.append(p.apply_async(compare_file, (reference_path, test_path, diffutils_sysroot))) | 
 | 123 |  | 
 | 124 |                     for a in async_result: | 
 | 125 |                         result.add_result(a.get()) | 
 | 126 |  | 
 | 127 |         finally: | 
 | 128 |             os.chdir(old_cwd) | 
 | 129 |  | 
 | 130 |         result.sort() | 
 | 131 |         return result | 
 | 132 |  | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 133 |     def write_package_list(self, package_class, name, packages): | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 134 |         self.extraresults['reproducible']['files'].setdefault(package_class, {})[name] = [ | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 135 |                 {'reference': p.reference, 'test': p.test} for p in packages] | 
 | 136 |  | 
| Brad Bishop | 1d80a2e | 2019-11-15 16:35:03 -0500 | [diff] [blame] | 137 |     def copy_file(self, source, dest): | 
 | 138 |         bb.utils.mkdirhier(os.path.dirname(dest)) | 
 | 139 |         shutil.copyfile(source, dest) | 
 | 140 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 141 |     def do_test_build(self, name, use_sstate): | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 142 |         capture_vars = ['DEPLOY_DIR_' + c.upper() for c in self.package_classes] | 
 | 143 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 144 |         tmpdir = os.path.join(self.topdir, name, 'tmp') | 
 | 145 |         if os.path.exists(tmpdir): | 
 | 146 |             bb.utils.remove(tmpdir, recurse=True) | 
 | 147 |  | 
 | 148 |         config = textwrap.dedent('''\ | 
 | 149 |             INHERIT += "reproducible_build" | 
 | 150 |             PACKAGE_CLASSES = "{package_classes}" | 
 | 151 |             INHIBIT_PACKAGE_STRIP = "1" | 
 | 152 |             TMPDIR = "{tmpdir}" | 
 | 153 |             ''').format(package_classes=' '.join('package_%s' % c for c in self.package_classes), | 
 | 154 |                         tmpdir=tmpdir) | 
 | 155 |  | 
 | 156 |         if not use_sstate: | 
 | 157 |             # This config fragment will disable using shared and the sstate | 
 | 158 |             # mirror, forcing a complete build from scratch | 
 | 159 |             config += textwrap.dedent('''\ | 
 | 160 |                 SSTATE_DIR = "${TMPDIR}/sstate" | 
 | 161 |                 SSTATE_MIRROR = "" | 
 | 162 |                 ''') | 
 | 163 |  | 
 | 164 |         self.write_config(config) | 
 | 165 |         d = get_bb_vars(capture_vars) | 
 | 166 |         bitbake(' '.join(self.images)) | 
 | 167 |         return d | 
 | 168 |  | 
 | 169 |     def test_reproducible_builds(self): | 
 | 170 |         def strip_topdir(s): | 
 | 171 |             if s.startswith(self.topdir): | 
 | 172 |                 return s[len(self.topdir):] | 
 | 173 |             return s | 
| Brad Bishop | 1d80a2e | 2019-11-15 16:35:03 -0500 | [diff] [blame] | 174 |  | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 175 |         # Build native utilities | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 176 |         self.write_config('') | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 177 |         bitbake("diffoscope-native diffutils-native jquery-native -c addto_recipe_sysroot") | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 178 |         diffutils_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "diffutils-native") | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 179 |         diffoscope_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "diffoscope-native") | 
 | 180 |         jquery_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "jquery-native") | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 181 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 182 |         if self.save_results: | 
 | 183 |             os.makedirs(self.save_results, exist_ok=True) | 
 | 184 |             datestr = datetime.datetime.now().strftime('%Y%m%d') | 
 | 185 |             save_dir = tempfile.mkdtemp(prefix='oe-reproducible-%s-' % datestr, dir=self.save_results) | 
 | 186 |             os.chmod(save_dir, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) | 
 | 187 |             self.logger.info('Non-reproducible packages will be copied to %s', save_dir) | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 188 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 189 |         vars_A = self.do_test_build('reproducibleA', self.build_from_sstate) | 
 | 190 |         vars_B = self.do_test_build('reproducibleB', False) | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 191 |  | 
 | 192 |         # NOTE: The temp directories from the reproducible build are purposely | 
 | 193 |         # kept after the build so it can be diffed for debugging. | 
 | 194 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 195 |         fails = [] | 
 | 196 |  | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 197 |         for c in self.package_classes: | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 198 |             with self.subTest(package_class=c): | 
 | 199 |                 package_class = 'package_' + c | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 200 |  | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 201 |                 deploy_A = vars_A['DEPLOY_DIR_' + c.upper()] | 
 | 202 |                 deploy_B = vars_B['DEPLOY_DIR_' + c.upper()] | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 203 |  | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 204 |                 result = self.compare_packages(deploy_A, deploy_B, diffutils_sysroot) | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 205 |  | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 206 |                 self.logger.info('Reproducibility summary for %s: %s' % (c, result)) | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 207 |  | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 208 |                 self.append_to_log('\n'.join("%s: %s" % (r.status, r.test) for r in result.total)) | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 209 |  | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 210 |                 self.write_package_list(package_class, 'missing', result.missing) | 
 | 211 |                 self.write_package_list(package_class, 'different', result.different) | 
 | 212 |                 self.write_package_list(package_class, 'same', result.same) | 
 | 213 |  | 
| Brad Bishop | 1d80a2e | 2019-11-15 16:35:03 -0500 | [diff] [blame] | 214 |                 if self.save_results: | 
 | 215 |                     for d in result.different: | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 216 |                         self.copy_file(d.reference, '/'.join([save_dir, 'packages', strip_topdir(d.reference)])) | 
 | 217 |                         self.copy_file(d.test, '/'.join([save_dir, 'packages', strip_topdir(d.test)])) | 
| Brad Bishop | 1d80a2e | 2019-11-15 16:35:03 -0500 | [diff] [blame] | 218 |  | 
| Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 219 |                 if result.missing or result.different: | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 220 |                     fails.append("The following %s packages are missing or different: %s" % | 
 | 221 |                             (c, '\n'.join(r.test for r in (result.missing + result.different)))) | 
 | 222 |  | 
 | 223 |         # Clean up empty directories | 
 | 224 |         if self.save_results: | 
 | 225 |             if not os.listdir(save_dir): | 
 | 226 |                 os.rmdir(save_dir) | 
 | 227 |             else: | 
 | 228 |                 self.logger.info('Running diffoscope') | 
 | 229 |                 package_dir = os.path.join(save_dir, 'packages') | 
 | 230 |                 package_html_dir = os.path.join(package_dir, 'diff-html') | 
 | 231 |  | 
 | 232 |                 # Copy jquery to improve the diffoscope output usability | 
 | 233 |                 self.copy_file(os.path.join(jquery_sysroot, 'usr/share/javascript/jquery/jquery.min.js'), os.path.join(package_html_dir, 'jquery.js')) | 
 | 234 |  | 
 | 235 |                 runCmd(['diffoscope', '--no-default-limits', '--exclude-directory-metadata', '--html-dir', package_html_dir, 'reproducibleA', 'reproducibleB'], | 
 | 236 |                         native_sysroot=diffoscope_sysroot, ignore_status=True, cwd=package_dir) | 
 | 237 |  | 
 | 238 |         if fails: | 
 | 239 |             self.fail('\n'.join(fails)) | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 240 |  |