diff options
Diffstat (limited to 'meta/lib/oeqa/selftest/cases/reproducible.py')
-rw-r--r-- | meta/lib/oeqa/selftest/cases/reproducible.py | 180 |
1 files changed, 127 insertions, 53 deletions
diff --git a/meta/lib/oeqa/selftest/cases/reproducible.py b/meta/lib/oeqa/selftest/cases/reproducible.py index 0d0259477e..f06027cb03 100644 --- a/meta/lib/oeqa/selftest/cases/reproducible.py +++ b/meta/lib/oeqa/selftest/cases/reproducible.py | |||
@@ -9,35 +9,13 @@ import bb.utils | |||
9 | import functools | 9 | import functools |
10 | import multiprocessing | 10 | import multiprocessing |
11 | import textwrap | 11 | import textwrap |
12 | import json | ||
13 | import unittest | ||
14 | import tempfile | 12 | import tempfile |
15 | import shutil | 13 | import shutil |
16 | import stat | 14 | import stat |
17 | import os | 15 | import os |
18 | import datetime | 16 | import datetime |
19 | 17 | ||
20 | # For sample packages, see: | ||
21 | # https://autobuilder.yocto.io/pub/repro-fail/oe-reproducible-20201127-0t7wr_oo/ | ||
22 | # https://autobuilder.yocto.io/pub/repro-fail/oe-reproducible-20201127-4s9ejwyp/ | ||
23 | # https://autobuilder.yocto.io/pub/repro-fail/oe-reproducible-20201127-haiwdlbr/ | ||
24 | # https://autobuilder.yocto.io/pub/repro-fail/oe-reproducible-20201127-hwds3mcl/ | ||
25 | # https://autobuilder.yocto.io/pub/repro-fail/oe-reproducible-20201203-sua0pzvc/ | ||
26 | # (both packages/ and packages-excluded/) | ||
27 | |||
28 | # ruby-ri-docs, meson: | ||
29 | #https://autobuilder.yocto.io/pub/repro-fail/oe-reproducible-20210215-0_td9la2/packages/diff-html/ | ||
30 | exclude_packages = [ | 18 | exclude_packages = [ |
31 | 'glide', | ||
32 | 'go-dep', | ||
33 | 'go-helloworld', | ||
34 | 'go-runtime', | ||
35 | 'go_', | ||
36 | 'go-', | ||
37 | 'meson', | ||
38 | 'ovmf-shell-efi', | ||
39 | 'perf', | ||
40 | 'ruby-ri-docs' | ||
41 | ] | 19 | ] |
42 | 20 | ||
43 | def is_excluded(package): | 21 | def is_excluded(package): |
@@ -65,13 +43,14 @@ class CompareResult(object): | |||
65 | return (self.status, self.test) < (other.status, other.test) | 43 | return (self.status, self.test) < (other.status, other.test) |
66 | 44 | ||
67 | class PackageCompareResults(object): | 45 | class PackageCompareResults(object): |
68 | def __init__(self): | 46 | def __init__(self, exclusions): |
69 | self.total = [] | 47 | self.total = [] |
70 | self.missing = [] | 48 | self.missing = [] |
71 | self.different = [] | 49 | self.different = [] |
72 | self.different_excluded = [] | 50 | self.different_excluded = [] |
73 | self.same = [] | 51 | self.same = [] |
74 | self.active_exclusions = set() | 52 | self.active_exclusions = set() |
53 | exclude_packages.extend((exclusions or "").split()) | ||
75 | 54 | ||
76 | def add_result(self, r): | 55 | def add_result(self, r): |
77 | self.total.append(r) | 56 | self.total.append(r) |
@@ -118,8 +97,11 @@ def compare_file(reference, test, diffutils_sysroot): | |||
118 | result.status = SAME | 97 | result.status = SAME |
119 | return result | 98 | return result |
120 | 99 | ||
121 | def run_diffoscope(a_dir, b_dir, html_dir, **kwargs): | 100 | def run_diffoscope(a_dir, b_dir, html_dir, max_report_size=0, max_diff_block_lines=1024, max_diff_block_lines_saved=0, **kwargs): |
122 | return runCmd(['diffoscope', '--no-default-limits', '--exclude-directory-metadata', 'yes', '--html-dir', html_dir, a_dir, b_dir], | 101 | return runCmd(['diffoscope', '--no-default-limits', '--max-report-size', str(max_report_size), |
102 | '--max-diff-block-lines-saved', str(max_diff_block_lines_saved), | ||
103 | '--max-diff-block-lines', str(max_diff_block_lines), | ||
104 | '--exclude-directory-metadata', 'yes', '--html-dir', html_dir, a_dir, b_dir], | ||
123 | **kwargs) | 105 | **kwargs) |
124 | 106 | ||
125 | class DiffoscopeTests(OESelftestTestCase): | 107 | class DiffoscopeTests(OESelftestTestCase): |
@@ -149,10 +131,21 @@ class ReproducibleTests(OESelftestTestCase): | |||
149 | 131 | ||
150 | package_classes = ['deb', 'ipk', 'rpm'] | 132 | package_classes = ['deb', 'ipk', 'rpm'] |
151 | 133 | ||
134 | # Maximum report size, in bytes | ||
135 | max_report_size = 250 * 1024 * 1024 | ||
136 | |||
137 | # Maximum diff blocks size, in lines | ||
138 | max_diff_block_lines = 1024 | ||
139 | # Maximum diff blocks size (saved in memory), in lines | ||
140 | max_diff_block_lines_saved = max_diff_block_lines | ||
141 | |||
152 | # targets are the things we want to test the reproducibility of | 142 | # targets are the things we want to test the reproducibility of |
153 | targets = ['core-image-minimal', 'core-image-sato', 'core-image-full-cmdline', 'core-image-weston', 'world'] | 143 | # Have to add the virtual targets manually for now as builds may or may not include them as they're exclude from world |
144 | targets = ['core-image-minimal', 'core-image-sato', 'core-image-full-cmdline', 'core-image-weston', 'world', 'virtual/librpc', 'virtual/libsdl2', 'virtual/crypt'] | ||
145 | |||
154 | # sstate targets are things to pull from sstate to potentially cut build/debugging time | 146 | # sstate targets are things to pull from sstate to potentially cut build/debugging time |
155 | sstate_targets = [] | 147 | sstate_targets = [] |
148 | |||
156 | save_results = False | 149 | save_results = False |
157 | if 'OEQA_DEBUGGING_SAVED_OUTPUT' in os.environ: | 150 | if 'OEQA_DEBUGGING_SAVED_OUTPUT' in os.environ: |
158 | save_results = os.environ['OEQA_DEBUGGING_SAVED_OUTPUT'] | 151 | save_results = os.environ['OEQA_DEBUGGING_SAVED_OUTPUT'] |
@@ -167,20 +160,40 @@ class ReproducibleTests(OESelftestTestCase): | |||
167 | 160 | ||
168 | def setUpLocal(self): | 161 | def setUpLocal(self): |
169 | super().setUpLocal() | 162 | super().setUpLocal() |
170 | needed_vars = ['TOPDIR', 'TARGET_PREFIX', 'BB_NUMBER_THREADS'] | 163 | needed_vars = [ |
164 | 'TOPDIR', | ||
165 | 'TARGET_PREFIX', | ||
166 | 'BB_NUMBER_THREADS', | ||
167 | 'BB_HASHSERVE', | ||
168 | 'OEQA_REPRODUCIBLE_TEST_PACKAGE', | ||
169 | 'OEQA_REPRODUCIBLE_TEST_TARGET', | ||
170 | 'OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS', | ||
171 | 'OEQA_REPRODUCIBLE_EXCLUDED_PACKAGES', | ||
172 | 'OEQA_REPRODUCIBLE_TEST_LEAF_TARGETS', | ||
173 | ] | ||
171 | bb_vars = get_bb_vars(needed_vars) | 174 | bb_vars = get_bb_vars(needed_vars) |
172 | for v in needed_vars: | 175 | for v in needed_vars: |
173 | setattr(self, v.lower(), bb_vars[v]) | 176 | setattr(self, v.lower(), bb_vars[v]) |
174 | 177 | ||
178 | if bb_vars['OEQA_REPRODUCIBLE_TEST_PACKAGE']: | ||
179 | self.package_classes = bb_vars['OEQA_REPRODUCIBLE_TEST_PACKAGE'].split() | ||
180 | |||
181 | if bb_vars['OEQA_REPRODUCIBLE_TEST_TARGET'] or bb_vars['OEQA_REPRODUCIBLE_TEST_LEAF_TARGETS']: | ||
182 | self.targets = (bb_vars['OEQA_REPRODUCIBLE_TEST_TARGET'] or "").split() + (bb_vars['OEQA_REPRODUCIBLE_TEST_LEAF_TARGETS'] or "").split() | ||
183 | |||
184 | if bb_vars['OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS']: | ||
185 | self.sstate_targets = bb_vars['OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS'].split() | ||
186 | |||
187 | if bb_vars['OEQA_REPRODUCIBLE_TEST_LEAF_TARGETS']: | ||
188 | # Setup to build every DEPENDS of leaf recipes using sstate | ||
189 | for leaf_recipe in bb_vars['OEQA_REPRODUCIBLE_TEST_LEAF_TARGETS'].split(): | ||
190 | self.sstate_targets.extend(get_bb_var('DEPENDS', leaf_recipe).split()) | ||
191 | |||
175 | self.extraresults = {} | 192 | self.extraresults = {} |
176 | self.extraresults.setdefault('reproducible.rawlogs', {})['log'] = '' | ||
177 | self.extraresults.setdefault('reproducible', {}).setdefault('files', {}) | 193 | self.extraresults.setdefault('reproducible', {}).setdefault('files', {}) |
178 | 194 | ||
179 | def append_to_log(self, msg): | ||
180 | self.extraresults['reproducible.rawlogs']['log'] += msg | ||
181 | |||
182 | def compare_packages(self, reference_dir, test_dir, diffutils_sysroot): | 195 | def compare_packages(self, reference_dir, test_dir, diffutils_sysroot): |
183 | result = PackageCompareResults() | 196 | result = PackageCompareResults(self.oeqa_reproducible_excluded_packages) |
184 | 197 | ||
185 | old_cwd = os.getcwd() | 198 | old_cwd = os.getcwd() |
186 | try: | 199 | try: |
@@ -205,7 +218,7 @@ class ReproducibleTests(OESelftestTestCase): | |||
205 | 218 | ||
206 | def write_package_list(self, package_class, name, packages): | 219 | def write_package_list(self, package_class, name, packages): |
207 | self.extraresults['reproducible']['files'].setdefault(package_class, {})[name] = [ | 220 | self.extraresults['reproducible']['files'].setdefault(package_class, {})[name] = [ |
208 | {'reference': p.reference, 'test': p.test} for p in packages] | 221 | p.reference.split("/./")[1] for p in packages] |
209 | 222 | ||
210 | def copy_file(self, source, dest): | 223 | def copy_file(self, source, dest): |
211 | bb.utils.mkdirhier(os.path.dirname(dest)) | 224 | bb.utils.mkdirhier(os.path.dirname(dest)) |
@@ -217,14 +230,11 @@ class ReproducibleTests(OESelftestTestCase): | |||
217 | tmpdir = os.path.join(self.topdir, name, 'tmp') | 230 | tmpdir = os.path.join(self.topdir, name, 'tmp') |
218 | if os.path.exists(tmpdir): | 231 | if os.path.exists(tmpdir): |
219 | bb.utils.remove(tmpdir, recurse=True) | 232 | bb.utils.remove(tmpdir, recurse=True) |
220 | |||
221 | config = textwrap.dedent('''\ | 233 | config = textwrap.dedent('''\ |
222 | INHERIT += "reproducible_build" | ||
223 | PACKAGE_CLASSES = "{package_classes}" | 234 | PACKAGE_CLASSES = "{package_classes}" |
224 | INHIBIT_PACKAGE_STRIP = "1" | ||
225 | TMPDIR = "{tmpdir}" | 235 | TMPDIR = "{tmpdir}" |
226 | LICENSE_FLAGS_WHITELIST = "commercial" | 236 | LICENSE_FLAGS_ACCEPTED = "commercial" |
227 | DISTRO_FEATURES_append = ' systemd pam' | 237 | DISTRO_FEATURES:append = ' pam' |
228 | USERADDEXTENSION = "useradd-staticids" | 238 | USERADDEXTENSION = "useradd-staticids" |
229 | USERADD_ERROR_DYNAMIC = "skip" | 239 | USERADD_ERROR_DYNAMIC = "skip" |
230 | USERADD_UID_TABLES += "files/static-passwd" | 240 | USERADD_UID_TABLES += "files/static-passwd" |
@@ -232,25 +242,70 @@ class ReproducibleTests(OESelftestTestCase): | |||
232 | ''').format(package_classes=' '.join('package_%s' % c for c in self.package_classes), | 242 | ''').format(package_classes=' '.join('package_%s' % c for c in self.package_classes), |
233 | tmpdir=tmpdir) | 243 | tmpdir=tmpdir) |
234 | 244 | ||
245 | # Export BB_CONSOLELOG to the calling function and make it constant to | ||
246 | # avoid a case where bitbake would get a timestamp-based filename but | ||
247 | # oe-selftest would, later, get another. | ||
248 | capture_vars.append("BB_CONSOLELOG") | ||
249 | config += 'BB_CONSOLELOG = "${LOG_DIR}/cooker/${MACHINE}/console.log"\n' | ||
250 | |||
251 | # We want different log files for each build, but a persistent bitbake | ||
252 | # may reuse the previous log file so restart the bitbake server. | ||
253 | bitbake("--kill-server") | ||
254 | |||
255 | def print_condensed_error_log(logs, context_lines=10, tail_lines=20): | ||
256 | """Prints errors with context and the end of the log.""" | ||
257 | |||
258 | logs = logs.split("\n") | ||
259 | for i, line in enumerate(logs): | ||
260 | if line.startswith("ERROR"): | ||
261 | self.logger.info("Found ERROR (line %d):" % (i + 1)) | ||
262 | for l in logs[i-context_lines:i+context_lines]: | ||
263 | self.logger.info(" " + l) | ||
264 | |||
265 | self.logger.info("End of log:") | ||
266 | for l in logs[-tail_lines:]: | ||
267 | self.logger.info(" " + l) | ||
268 | |||
269 | bitbake_failure_count = 0 | ||
235 | if not use_sstate: | 270 | if not use_sstate: |
236 | if self.sstate_targets: | 271 | if self.sstate_targets: |
237 | self.logger.info("Building prebuild for %s (sstate allowed)..." % (name)) | 272 | self.logger.info("Building prebuild for %s (sstate allowed)..." % (name)) |
238 | self.write_config(config) | 273 | self.write_config(config) |
239 | bitbake(' '.join(self.sstate_targets)) | 274 | try: |
275 | bitbake("--continue "+' '.join(self.sstate_targets)) | ||
276 | except AssertionError as e: | ||
277 | bitbake_failure_count += 1 | ||
278 | self.logger.error("Bitbake failed! but keep going... Log:") | ||
279 | print_condensed_error_log(str(e)) | ||
240 | 280 | ||
241 | # This config fragment will disable using shared and the sstate | 281 | # This config fragment will disable using shared and the sstate |
242 | # mirror, forcing a complete build from scratch | 282 | # mirror, forcing a complete build from scratch |
243 | config += textwrap.dedent('''\ | 283 | config += textwrap.dedent('''\ |
244 | SSTATE_DIR = "${TMPDIR}/sstate" | 284 | SSTATE_DIR = "${TMPDIR}/sstate" |
245 | SSTATE_MIRRORS = "" | 285 | SSTATE_MIRRORS = "file://.*/.*-native.* http://sstate.yoctoproject.org/all/PATH;downloadfilename=PATH file://.*/.*-cross.* http://sstate.yoctoproject.org/all/PATH;downloadfilename=PATH" |
246 | ''') | 286 | ''') |
247 | 287 | ||
248 | self.logger.info("Building %s (sstate%s allowed)..." % (name, '' if use_sstate else ' NOT')) | 288 | self.logger.info("Building %s (sstate%s allowed)..." % (name, '' if use_sstate else ' NOT')) |
249 | self.write_config(config) | 289 | self.write_config(config) |
250 | d = get_bb_vars(capture_vars) | 290 | d = get_bb_vars(capture_vars) |
251 | # targets used to be called images | 291 | try: |
252 | bitbake(' '.join(getattr(self, 'images', self.targets))) | 292 | # targets used to be called images |
253 | return d | 293 | bitbake("--continue "+' '.join(getattr(self, 'images', self.targets))) |
294 | except AssertionError as e: | ||
295 | bitbake_failure_count += 1 | ||
296 | self.logger.error("Bitbake failed! but keep going... Log:") | ||
297 | print_condensed_error_log(str(e)) | ||
298 | |||
299 | # The calling function expects the existence of the deploy | ||
300 | # directories containing the packages. | ||
301 | # If bitbake failed to create them, do it manually | ||
302 | for c in self.package_classes: | ||
303 | deploy = d['DEPLOY_DIR_' + c.upper()] | ||
304 | if not os.path.exists(deploy): | ||
305 | self.logger.info("Manually creating %s" % deploy) | ||
306 | bb.utils.mkdirhier(deploy) | ||
307 | |||
308 | return (d, bitbake_failure_count) | ||
254 | 309 | ||
255 | def test_reproducible_builds(self): | 310 | def test_reproducible_builds(self): |
256 | def strip_topdir(s): | 311 | def strip_topdir(s): |
@@ -272,15 +327,30 @@ class ReproducibleTests(OESelftestTestCase): | |||
272 | os.chmod(save_dir, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) | 327 | os.chmod(save_dir, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) |
273 | self.logger.info('Non-reproducible packages will be copied to %s', save_dir) | 328 | self.logger.info('Non-reproducible packages will be copied to %s', save_dir) |
274 | 329 | ||
275 | vars_A = self.do_test_build('reproducibleA', self.build_from_sstate) | 330 | # The below bug shows that a few reproducible issues are depends on build dir path length. |
331 | # https://bugzilla.yoctoproject.org/show_bug.cgi?id=15554 | ||
332 | # So, the reproducibleA & reproducibleB directories are changed to reproducibleA & reproducibleB-extended to have different size. | ||
276 | 333 | ||
277 | vars_B = self.do_test_build('reproducibleB', False) | 334 | fails = [] |
335 | vars_list = [None, None] | ||
336 | |||
337 | for i, (name, use_sstate) in enumerate( | ||
338 | (('reproducibleA', self.build_from_sstate), | ||
339 | ('reproducibleB-extended', False))): | ||
340 | (variables, bitbake_failure_count) = self.do_test_build(name, use_sstate) | ||
341 | if bitbake_failure_count > 0: | ||
342 | self.logger.error('%s build failed. Trying to compute built packages differences but the test will fail.' % name) | ||
343 | fails.append("Bitbake %s failure" % name) | ||
344 | if self.save_results: | ||
345 | failure_log_path = os.path.join(save_dir, "bitbake-%s.log" % name) | ||
346 | self.logger.info('Failure log for %s will be copied to %s'% (name, failure_log_path)) | ||
347 | self.copy_file(variables["BB_CONSOLELOG"], failure_log_path) | ||
348 | vars_list[i] = variables | ||
278 | 349 | ||
350 | vars_A, vars_B = vars_list | ||
279 | # NOTE: The temp directories from the reproducible build are purposely | 351 | # NOTE: The temp directories from the reproducible build are purposely |
280 | # kept after the build so it can be diffed for debugging. | 352 | # kept after the build so it can be diffed for debugging. |
281 | 353 | ||
282 | fails = [] | ||
283 | |||
284 | for c in self.package_classes: | 354 | for c in self.package_classes: |
285 | with self.subTest(package_class=c): | 355 | with self.subTest(package_class=c): |
286 | package_class = 'package_' + c | 356 | package_class = 'package_' + c |
@@ -293,8 +363,6 @@ class ReproducibleTests(OESelftestTestCase): | |||
293 | 363 | ||
294 | self.logger.info('Reproducibility summary for %s: %s' % (c, result)) | 364 | self.logger.info('Reproducibility summary for %s: %s' % (c, result)) |
295 | 365 | ||
296 | self.append_to_log('\n'.join("%s: %s" % (r.status, r.test) for r in result.total)) | ||
297 | |||
298 | self.write_package_list(package_class, 'missing', result.missing) | 366 | self.write_package_list(package_class, 'missing', result.missing) |
299 | self.write_package_list(package_class, 'different', result.different) | 367 | self.write_package_list(package_class, 'different', result.different) |
300 | self.write_package_list(package_class, 'different_excluded', result.different_excluded) | 368 | self.write_package_list(package_class, 'different_excluded', result.different_excluded) |
@@ -309,9 +377,13 @@ class ReproducibleTests(OESelftestTestCase): | |||
309 | self.copy_file(d.reference, '/'.join([save_dir, 'packages-excluded', strip_topdir(d.reference)])) | 377 | self.copy_file(d.reference, '/'.join([save_dir, 'packages-excluded', strip_topdir(d.reference)])) |
310 | self.copy_file(d.test, '/'.join([save_dir, 'packages-excluded', strip_topdir(d.test)])) | 378 | self.copy_file(d.test, '/'.join([save_dir, 'packages-excluded', strip_topdir(d.test)])) |
311 | 379 | ||
312 | if result.missing or result.different: | 380 | if result.different: |
313 | fails.append("The following %s packages are missing or different and not in exclusion list: %s" % | 381 | fails.append("The following %s packages are different and not in exclusion list:\n%s" % |
314 | (c, '\n'.join(r.test for r in (result.missing + result.different)))) | 382 | (c, '\n'.join(r.test for r in (result.different)))) |
383 | |||
384 | if result.missing and len(self.sstate_targets) == 0: | ||
385 | fails.append("The following %s packages are missing and not in exclusion list:\n%s" % | ||
386 | (c, '\n'.join(r.test for r in (result.missing)))) | ||
315 | 387 | ||
316 | # Clean up empty directories | 388 | # Clean up empty directories |
317 | if self.save_results: | 389 | if self.save_results: |
@@ -325,7 +397,9 @@ class ReproducibleTests(OESelftestTestCase): | |||
325 | # Copy jquery to improve the diffoscope output usability | 397 | # Copy jquery to improve the diffoscope output usability |
326 | self.copy_file(os.path.join(jquery_sysroot, 'usr/share/javascript/jquery/jquery.min.js'), os.path.join(package_html_dir, 'jquery.js')) | 398 | self.copy_file(os.path.join(jquery_sysroot, 'usr/share/javascript/jquery/jquery.min.js'), os.path.join(package_html_dir, 'jquery.js')) |
327 | 399 | ||
328 | run_diffoscope('reproducibleA', 'reproducibleB', package_html_dir, | 400 | run_diffoscope('reproducibleA', 'reproducibleB-extended', package_html_dir, max_report_size=self.max_report_size, |
401 | max_diff_block_lines_saved=self.max_diff_block_lines_saved, | ||
402 | max_diff_block_lines=self.max_diff_block_lines, | ||
329 | native_sysroot=diffoscope_sysroot, ignore_status=True, cwd=package_dir) | 403 | native_sysroot=diffoscope_sysroot, ignore_status=True, cwd=package_dir) |
330 | 404 | ||
331 | if fails: | 405 | if fails: |