diff options
Diffstat (limited to 'meta/lib/oeqa/selftest/cases/reproducible.py')
-rw-r--r-- | meta/lib/oeqa/selftest/cases/reproducible.py | 111 |
1 files changed, 90 insertions, 21 deletions
diff --git a/meta/lib/oeqa/selftest/cases/reproducible.py b/meta/lib/oeqa/selftest/cases/reproducible.py index 80e830136f..f06027cb03 100644 --- a/meta/lib/oeqa/selftest/cases/reproducible.py +++ b/meta/lib/oeqa/selftest/cases/reproducible.py | |||
@@ -97,8 +97,10 @@ def compare_file(reference, test, diffutils_sysroot): | |||
97 | result.status = SAME | 97 | result.status = SAME |
98 | return result | 98 | return result |
99 | 99 | ||
100 | def run_diffoscope(a_dir, b_dir, html_dir, max_report_size=0, **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): |
101 | return runCmd(['diffoscope', '--no-default-limits', '--max-report-size', str(max_report_size), | 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), | ||
102 | '--exclude-directory-metadata', 'yes', '--html-dir', html_dir, a_dir, b_dir], | 104 | '--exclude-directory-metadata', 'yes', '--html-dir', html_dir, a_dir, b_dir], |
103 | **kwargs) | 105 | **kwargs) |
104 | 106 | ||
@@ -132,8 +134,14 @@ class ReproducibleTests(OESelftestTestCase): | |||
132 | # Maximum report size, in bytes | 134 | # Maximum report size, in bytes |
133 | max_report_size = 250 * 1024 * 1024 | 135 | max_report_size = 250 * 1024 * 1024 |
134 | 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 | |||
135 | # targets are the things we want to test the reproducibility of | 142 | # targets are the things we want to test the reproducibility of |
136 | 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'] | ||
137 | 145 | ||
138 | # 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 |
139 | sstate_targets = [] | 147 | sstate_targets = [] |
@@ -161,6 +169,7 @@ class ReproducibleTests(OESelftestTestCase): | |||
161 | 'OEQA_REPRODUCIBLE_TEST_TARGET', | 169 | 'OEQA_REPRODUCIBLE_TEST_TARGET', |
162 | 'OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS', | 170 | 'OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS', |
163 | 'OEQA_REPRODUCIBLE_EXCLUDED_PACKAGES', | 171 | 'OEQA_REPRODUCIBLE_EXCLUDED_PACKAGES', |
172 | 'OEQA_REPRODUCIBLE_TEST_LEAF_TARGETS', | ||
164 | ] | 173 | ] |
165 | bb_vars = get_bb_vars(needed_vars) | 174 | bb_vars = get_bb_vars(needed_vars) |
166 | for v in needed_vars: | 175 | for v in needed_vars: |
@@ -169,19 +178,20 @@ class ReproducibleTests(OESelftestTestCase): | |||
169 | if bb_vars['OEQA_REPRODUCIBLE_TEST_PACKAGE']: | 178 | if bb_vars['OEQA_REPRODUCIBLE_TEST_PACKAGE']: |
170 | self.package_classes = bb_vars['OEQA_REPRODUCIBLE_TEST_PACKAGE'].split() | 179 | self.package_classes = bb_vars['OEQA_REPRODUCIBLE_TEST_PACKAGE'].split() |
171 | 180 | ||
172 | if bb_vars['OEQA_REPRODUCIBLE_TEST_TARGET']: | 181 | if bb_vars['OEQA_REPRODUCIBLE_TEST_TARGET'] or bb_vars['OEQA_REPRODUCIBLE_TEST_LEAF_TARGETS']: |
173 | self.targets = bb_vars['OEQA_REPRODUCIBLE_TEST_TARGET'].split() | 182 | self.targets = (bb_vars['OEQA_REPRODUCIBLE_TEST_TARGET'] or "").split() + (bb_vars['OEQA_REPRODUCIBLE_TEST_LEAF_TARGETS'] or "").split() |
174 | 183 | ||
175 | if bb_vars['OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS']: | 184 | if bb_vars['OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS']: |
176 | self.sstate_targets = bb_vars['OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS'].split() | 185 | self.sstate_targets = bb_vars['OEQA_REPRODUCIBLE_TEST_SSTATE_TARGETS'].split() |
177 | 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 | |||
178 | self.extraresults = {} | 192 | self.extraresults = {} |
179 | self.extraresults.setdefault('reproducible.rawlogs', {})['log'] = '' | ||
180 | self.extraresults.setdefault('reproducible', {}).setdefault('files', {}) | 193 | self.extraresults.setdefault('reproducible', {}).setdefault('files', {}) |
181 | 194 | ||
182 | def append_to_log(self, msg): | ||
183 | self.extraresults['reproducible.rawlogs']['log'] += msg | ||
184 | |||
185 | def compare_packages(self, reference_dir, test_dir, diffutils_sysroot): | 195 | def compare_packages(self, reference_dir, test_dir, diffutils_sysroot): |
186 | result = PackageCompareResults(self.oeqa_reproducible_excluded_packages) | 196 | result = PackageCompareResults(self.oeqa_reproducible_excluded_packages) |
187 | 197 | ||
@@ -208,7 +218,7 @@ class ReproducibleTests(OESelftestTestCase): | |||
208 | 218 | ||
209 | def write_package_list(self, package_class, name, packages): | 219 | def write_package_list(self, package_class, name, packages): |
210 | self.extraresults['reproducible']['files'].setdefault(package_class, {})[name] = [ | 220 | self.extraresults['reproducible']['files'].setdefault(package_class, {})[name] = [ |
211 | {'reference': p.reference, 'test': p.test} for p in packages] | 221 | p.reference.split("/./")[1] for p in packages] |
212 | 222 | ||
213 | def copy_file(self, source, dest): | 223 | def copy_file(self, source, dest): |
214 | bb.utils.mkdirhier(os.path.dirname(dest)) | 224 | bb.utils.mkdirhier(os.path.dirname(dest)) |
@@ -220,7 +230,6 @@ class ReproducibleTests(OESelftestTestCase): | |||
220 | tmpdir = os.path.join(self.topdir, name, 'tmp') | 230 | tmpdir = os.path.join(self.topdir, name, 'tmp') |
221 | if os.path.exists(tmpdir): | 231 | if os.path.exists(tmpdir): |
222 | bb.utils.remove(tmpdir, recurse=True) | 232 | bb.utils.remove(tmpdir, recurse=True) |
223 | |||
224 | config = textwrap.dedent('''\ | 233 | config = textwrap.dedent('''\ |
225 | PACKAGE_CLASSES = "{package_classes}" | 234 | PACKAGE_CLASSES = "{package_classes}" |
226 | TMPDIR = "{tmpdir}" | 235 | TMPDIR = "{tmpdir}" |
@@ -233,11 +242,41 @@ class ReproducibleTests(OESelftestTestCase): | |||
233 | ''').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), |
234 | tmpdir=tmpdir) | 243 | tmpdir=tmpdir) |
235 | 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 | ||
236 | if not use_sstate: | 270 | if not use_sstate: |
237 | if self.sstate_targets: | 271 | if self.sstate_targets: |
238 | self.logger.info("Building prebuild for %s (sstate allowed)..." % (name)) | 272 | self.logger.info("Building prebuild for %s (sstate allowed)..." % (name)) |
239 | self.write_config(config) | 273 | self.write_config(config) |
240 | 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)) | ||
241 | 280 | ||
242 | # This config fragment will disable using shared and the sstate | 281 | # This config fragment will disable using shared and the sstate |
243 | # mirror, forcing a complete build from scratch | 282 | # mirror, forcing a complete build from scratch |
@@ -249,9 +288,24 @@ class ReproducibleTests(OESelftestTestCase): | |||
249 | 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')) |
250 | self.write_config(config) | 289 | self.write_config(config) |
251 | d = get_bb_vars(capture_vars) | 290 | d = get_bb_vars(capture_vars) |
252 | # targets used to be called images | 291 | try: |
253 | bitbake(' '.join(getattr(self, 'images', self.targets))) | 292 | # targets used to be called images |
254 | 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) | ||
255 | 309 | ||
256 | def test_reproducible_builds(self): | 310 | def test_reproducible_builds(self): |
257 | def strip_topdir(s): | 311 | def strip_topdir(s): |
@@ -273,15 +327,30 @@ class ReproducibleTests(OESelftestTestCase): | |||
273 | 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) |
274 | 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) |
275 | 329 | ||
276 | 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. | ||
277 | 333 | ||
278 | 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 | ||
279 | 349 | ||
350 | vars_A, vars_B = vars_list | ||
280 | # NOTE: The temp directories from the reproducible build are purposely | 351 | # NOTE: The temp directories from the reproducible build are purposely |
281 | # kept after the build so it can be diffed for debugging. | 352 | # kept after the build so it can be diffed for debugging. |
282 | 353 | ||
283 | fails = [] | ||
284 | |||
285 | for c in self.package_classes: | 354 | for c in self.package_classes: |
286 | with self.subTest(package_class=c): | 355 | with self.subTest(package_class=c): |
287 | package_class = 'package_' + c | 356 | package_class = 'package_' + c |
@@ -294,8 +363,6 @@ class ReproducibleTests(OESelftestTestCase): | |||
294 | 363 | ||
295 | self.logger.info('Reproducibility summary for %s: %s' % (c, result)) | 364 | self.logger.info('Reproducibility summary for %s: %s' % (c, result)) |
296 | 365 | ||
297 | self.append_to_log('\n'.join("%s: %s" % (r.status, r.test) for r in result.total)) | ||
298 | |||
299 | self.write_package_list(package_class, 'missing', result.missing) | 366 | self.write_package_list(package_class, 'missing', result.missing) |
300 | self.write_package_list(package_class, 'different', result.different) | 367 | self.write_package_list(package_class, 'different', result.different) |
301 | self.write_package_list(package_class, 'different_excluded', result.different_excluded) | 368 | self.write_package_list(package_class, 'different_excluded', result.different_excluded) |
@@ -330,7 +397,9 @@ class ReproducibleTests(OESelftestTestCase): | |||
330 | # Copy jquery to improve the diffoscope output usability | 397 | # Copy jquery to improve the diffoscope output usability |
331 | 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')) |
332 | 399 | ||
333 | run_diffoscope('reproducibleA', 'reproducibleB', package_html_dir, max_report_size=self.max_report_size, | 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, | ||
334 | native_sysroot=diffoscope_sysroot, ignore_status=True, cwd=package_dir) | 403 | native_sysroot=diffoscope_sysroot, ignore_status=True, cwd=package_dir) |
335 | 404 | ||
336 | if fails: | 405 | if fails: |