1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2014 Google, Inc
5 """Implementation the bulider threads
7 This module provides the BuilderThread class, which handles calling the builder
8 based on the jobs provided.
19 from buildman import cfgutil
20 from patman import gitutil
21 from u_boot_pylib import command
23 RETURN_CODE_RETRY = -1
24 BASE_ELF_FILENAMES = ['u-boot', 'spl/u-boot-spl', 'tpl/u-boot-tpl']
26 def mkdir(dirname, parents = False):
27 """Make a directory if it doesn't already exist.
30 dirname: Directory to create
37 except OSError as err:
38 if err.errno == errno.EEXIST:
39 if os.path.realpath('.') == os.path.realpath(dirname):
40 print(f"Cannot create the current working directory '{dirname}'!")
45 # pylint: disable=R0903
47 """Holds information about a job to be performed by a thread
50 brd: Board object to build
51 commits: List of Commit objects to build
52 keep_outputs: True to save build output files
53 step: 1 to process every commit, n to process every nth commit
54 work_in_output: Use the output directory as the work directory and
55 don't write to a separate output directory.
60 self.keep_outputs = False
62 self.work_in_output = False
65 class ResultThread(threading.Thread):
66 """This thread processes results from builder threads.
68 It simply passes the results on to the builder. There is only one
69 result thread, and this helps to serialise the build output.
71 def __init__(self, builder):
72 """Set up a new result thread
75 builder: Builder which will be sent each result
77 threading.Thread.__init__(self)
78 self.builder = builder
81 """Called to start up the result thread.
83 We collect the next result job and pass it on to the build.
86 result = self.builder.out_queue.get()
87 self.builder.process_result(result)
88 self.builder.out_queue.task_done()
91 class BuilderThread(threading.Thread):
92 """This thread builds U-Boot for a particular board.
94 An input queue provides each new job. We run 'make' to build U-Boot
95 and then pass the results on to the output queue.
98 builder: The builder which contains information we might need
99 thread_num: Our thread number (0-n-1), used to decide on a
100 temporary directory. If this is -1 then there are no threads
101 and we are the (only) main process
102 mrproper: Use 'make mrproper' before each reconfigure
103 per_board_out_dir: True to build in a separate persistent directory per
104 board rather than a thread-specific directory
105 test_exception: Used for testing; True to raise an exception instead of
106 reporting the build result
108 def __init__(self, builder, thread_num, mrproper, per_board_out_dir,
109 test_exception=False):
110 """Set up a new builder thread"""
111 threading.Thread.__init__(self)
112 self.builder = builder
113 self.thread_num = thread_num
114 self.mrproper = mrproper
115 self.per_board_out_dir = per_board_out_dir
116 self.test_exception = test_exception
117 self.toolchain = None
119 def make(self, commit, brd, stage, cwd, *args, **kwargs):
120 """Run 'make' on a particular commit and board.
122 The source code will already be checked out, so the 'commit'
123 argument is only for information.
126 commit: Commit object that is being built
127 brd: Board object that is being built
128 stage: Stage of the build. Valid stages are:
129 mrproper - can be called to clean source
130 config - called to configure for a board
131 build - the main make invocation - it does the build
132 args: A list of arguments to pass to 'make'
133 kwargs: A list of keyword arguments to pass to command.run_pipe()
138 return self.builder.do_make(commit, brd, stage, cwd, *args,
141 def _build_args(self, brd, out_dir, out_rel_dir, work_dir, commit_upto):
142 """Set up arguments to the args list based on the settings
145 brd (Board): Board to create arguments for
146 out_dir (str): Path to output directory containing the files
147 out_rel_dir (str): Output directory relative to the current dir
148 work_dir (str): Directory to which the source will be checked out
149 commit_upto (int): Commit number to build (0...n-1)
153 list of str: Arguments to pass to make
154 str: Current working directory, or None if no commit
155 str: Source directory (typically the work directory)
159 src_dir = os.path.realpath(work_dir)
160 if not self.builder.in_tree:
161 if commit_upto is None:
162 # In this case we are building in the original source directory
163 # (i.e. the current directory where buildman is invoked. The
164 # output directory is set to this thread's selected work
167 # Symlinks can confuse U-Boot's Makefile since we may use '..'
168 # in our path, so remove them.
169 real_dir = os.path.realpath(out_dir)
170 args.append(f'O={real_dir}')
172 src_dir = os.getcwd()
174 args.append(f'O={out_rel_dir}')
175 if self.builder.verbose_build:
179 if self.builder.num_jobs is not None:
180 args.extend(['-j', str(self.builder.num_jobs)])
181 if self.builder.warnings_as_errors:
182 args.append('KCFLAGS=-Werror')
183 args.append('HOSTCFLAGS=-Werror')
184 if self.builder.allow_missing:
185 args.append('BINMAN_ALLOW_MISSING=1')
186 if self.builder.no_lto:
187 args.append('NO_LTO=1')
188 if self.builder.reproducible_builds:
189 args.append('SOURCE_DATE_EPOCH=0')
190 args.extend(self.builder.toolchains.GetMakeArguments(brd))
191 args.extend(self.toolchain.MakeArgs())
192 return args, cwd, src_dir
194 def run_commit(self, commit_upto, brd, work_dir, do_config, config_only,
195 force_build, force_build_failures, work_in_output,
197 """Build a particular commit.
199 If the build is already done, and we are not forcing a build, we skip
200 the build and just return the previously-saved results.
203 commit_upto: Commit number to build (0...n-1)
204 brd: Board object to build
205 work_dir: Directory to which the source will be checked out
206 do_config: True to run a make <board>_defconfig on the source
207 config_only: Only configure the source, do not build it
208 force_build: Force a build even if one was previously done
209 force_build_failures: Force a bulid if the previous result showed
211 work_in_output: Use the output directory as the work directory and
212 don't write to a separate output directory.
213 adjust_cfg (list of str): List of changes to make to .config file
214 before building. Each is one of (where C is either CONFIG_xxx
218 C=val to set the value of C (val must have quotes if C is
223 - CommandResult object containing the results of the build
224 - boolean indicating whether 'make config' is still needed
226 # Create a default result - it will be overwritte by the call to
227 # self.make() below, in the event that we do a build.
228 result = command.CommandResult()
229 result.return_code = 0
230 if work_in_output or self.builder.in_tree:
234 if self.per_board_out_dir:
235 out_rel_dir = os.path.join('..', brd.target)
237 out_rel_dir = 'build'
238 out_dir = os.path.join(work_dir, out_rel_dir)
240 # Check if the job was already completed last time
241 done_file = self.builder.get_done_file(commit_upto, brd.target)
242 result.already_done = os.path.exists(done_file)
243 will_build = (force_build or force_build_failures or
244 not result.already_done)
245 if result.already_done:
246 # Get the return code from that build and use it
247 with open(done_file, 'r', encoding='utf-8') as outf:
249 result.return_code = int(outf.readline())
251 # The file may be empty due to running out of disk space.
253 result.return_code = RETURN_CODE_RETRY
255 # Check the signal that the build needs to be retried
256 if result.return_code == RETURN_CODE_RETRY:
259 err_file = self.builder.get_err_file(commit_upto, brd.target)
260 if os.path.exists(err_file) and os.stat(err_file).st_size:
261 result.stderr = 'bad'
262 elif not force_build:
263 # The build passed, so no need to build it again
267 # We are going to have to build it. First, get a toolchain
268 if not self.toolchain:
270 self.toolchain = self.builder.toolchains.Select(brd.arch)
271 except ValueError as err:
272 result.return_code = 10
274 result.stderr = str(err)
275 # TODO(sjg@chromium.org): This gets swallowed, but needs
279 # Checkout the right commit
280 if self.builder.commits:
281 commit = self.builder.commits[commit_upto]
282 if self.builder.checkout:
283 git_dir = os.path.join(work_dir, '.git')
284 gitutil.checkout(commit.hash, git_dir, work_dir,
289 # Set up the environment and command line
290 env = self.toolchain.MakeEnvironment(self.builder.full_path)
293 args, cwd, src_dir = self._build_args(brd, out_dir, out_rel_dir,
294 work_dir, commit_upto)
295 config_args = [f'{brd.target}_defconfig']
296 config_out = io.StringIO()
298 # Remove any output targets. Since we use a build directory that
299 # was previously used by another board, it may have produced an
300 # SPL image. If we don't remove it (i.e. see do_config and
301 # self.mrproper below) then it will appear to be the output of
302 # this build, even if it does not produce SPL images.
303 for elf in BASE_ELF_FILENAMES:
304 fname = os.path.join(out_dir, elf)
305 if os.path.exists(fname):
308 # If we need to reconfigure, do that now
309 cfg_file = os.path.join(out_dir, '.config')
311 if do_config or adjust_cfg:
313 result = self.make(commit, brd, 'mrproper', cwd,
314 'mrproper', *args, env=env)
315 config_out.write(result.combined)
316 cmd_list.append([self.builder.gnu_make, 'mrproper',
318 result = self.make(commit, brd, 'config', cwd,
319 *(args + config_args), env=env)
320 cmd_list.append([self.builder.gnu_make] + args +
322 config_out.write(result.combined)
323 do_config = False # No need to configure next time
325 cfgutil.adjust_cfg_file(cfg_file, adjust_cfg)
326 if result.return_code == 0:
329 result = self.make(commit, brd, 'build', cwd, *args,
331 cmd_list.append([self.builder.gnu_make] + args)
332 if (result.return_code == 2 and
333 ('Some images are invalid' in result.stderr)):
334 # This is handled later by the check for output in
336 result.return_code = 0
338 errs = cfgutil.check_cfg_file(cfg_file, adjust_cfg)
340 result.stderr += errs
341 result.return_code = 1
342 result.stderr = result.stderr.replace(src_dir + '/', '')
343 if self.builder.verbose_build:
344 result.stdout = config_out.getvalue() + result.stdout
345 result.cmd_list = cmd_list
347 result.return_code = 1
348 result.stderr = f'No tool chain for {brd.arch}\n'
349 result.already_done = False
351 result.toolchain = self.toolchain
353 result.commit_upto = commit_upto
354 result.out_dir = out_dir
355 return result, do_config
357 def _write_result(self, result, keep_outputs, work_in_output):
358 """Write a built result to the output directory.
361 result: CommandResult object containing result to write
362 keep_outputs: True to store the output binaries, False
364 work_in_output: Use the output directory as the work directory and
365 don't write to a separate output directory.
367 # If we think this might have been aborted with Ctrl-C, record the
368 # failure but not that we are 'done' with this board. A retry may fix
370 maybe_aborted = result.stderr and 'No child processes' in result.stderr
372 if result.return_code >= 0 and result.already_done:
375 # Write the output and stderr
376 output_dir = self.builder.get_output_dir(result.commit_upto)
378 build_dir = self.builder.get_build_dir(result.commit_upto,
382 outfile = os.path.join(build_dir, 'log')
383 with open(outfile, 'w', encoding='utf-8') as outf:
385 outf.write(result.stdout)
387 errfile = self.builder.get_err_file(result.commit_upto,
390 with open(errfile, 'w', encoding='utf-8') as outf:
391 outf.write(result.stderr)
392 elif os.path.exists(errfile):
396 if result.return_code < 0:
400 # Write the build result and toolchain information.
401 done_file = self.builder.get_done_file(result.commit_upto,
403 with open(done_file, 'w', encoding='utf-8') as outf:
405 # Special code to indicate we need to retry
406 outf.write(f'{RETURN_CODE_RETRY}')
408 outf.write(f'{result.return_code}')
409 with open(os.path.join(build_dir, 'toolchain'), 'w',
410 encoding='utf-8') as outf:
411 print('gcc', result.toolchain.gcc, file=outf)
412 print('path', result.toolchain.path, file=outf)
413 print('cross', result.toolchain.cross, file=outf)
414 print('arch', result.toolchain.arch, file=outf)
415 outf.write(f'{result.return_code}')
417 # Write out the image and function size information and an objdump
418 env = result.toolchain.MakeEnvironment(self.builder.full_path)
419 with open(os.path.join(build_dir, 'out-env'), 'wb') as outf:
420 for var in sorted(env.keys()):
421 outf.write(b'%s="%s"' % (var, env[var]))
423 with open(os.path.join(build_dir, 'out-cmd'), 'w',
424 encoding='utf-8') as outf:
425 for cmd in result.cmd_list:
426 print(' '.join(cmd), file=outf)
429 for fname in BASE_ELF_FILENAMES:
430 cmd = [f'{self.toolchain.cross}nm', '--size-sort', fname]
431 nm_result = command.run_pipe([cmd], capture=True,
432 capture_stderr=True, cwd=result.out_dir,
433 raise_on_error=False, env=env)
435 nm_fname = self.builder.get_func_sizes_file(
436 result.commit_upto, result.brd.target, fname)
437 with open(nm_fname, 'w', encoding='utf-8') as outf:
438 print(nm_result.stdout, end=' ', file=outf)
440 cmd = [f'{self.toolchain.cross}objdump', '-h', fname]
441 dump_result = command.run_pipe([cmd], capture=True,
442 capture_stderr=True, cwd=result.out_dir,
443 raise_on_error=False, env=env)
445 if dump_result.stdout:
446 objdump = self.builder.get_objdump_file(result.commit_upto,
447 result.brd.target, fname)
448 with open(objdump, 'w', encoding='utf-8') as outf:
449 print(dump_result.stdout, end=' ', file=outf)
450 for line in dump_result.stdout.splitlines():
451 fields = line.split()
452 if len(fields) > 5 and fields[1] == '.rodata':
453 rodata_size = fields[2]
455 cmd = [f'{self.toolchain.cross}size', fname]
456 size_result = command.run_pipe([cmd], capture=True,
457 capture_stderr=True, cwd=result.out_dir,
458 raise_on_error=False, env=env)
459 if size_result.stdout:
460 lines.append(size_result.stdout.splitlines()[1] + ' ' +
463 # Extract the environment from U-Boot and dump it out
464 cmd = [f'{self.toolchain.cross}objcopy', '-O', 'binary',
465 '-j', '.rodata.default_environment',
466 'env/built-in.o', 'uboot.env']
467 command.run_pipe([cmd], capture=True,
468 capture_stderr=True, cwd=result.out_dir,
469 raise_on_error=False, env=env)
470 if not work_in_output:
471 self.copy_files(result.out_dir, build_dir, '', ['uboot.env'])
473 # Write out the image sizes file. This is similar to the output
474 # of binutil's 'size' utility, but it omits the header line and
475 # adds an additional hex value at the end of each line for the
478 sizes = self.builder.get_sizes_file(result.commit_upto,
480 with open(sizes, 'w', encoding='utf-8') as outf:
481 print('\n'.join(lines), file=outf)
483 if not work_in_output:
484 # Write out the configuration files, with a special case for SPL
485 for dirname in ['', 'spl', 'tpl']:
487 result.out_dir, build_dir, dirname,
488 ['u-boot.cfg', 'spl/u-boot-spl.cfg', 'tpl/u-boot-tpl.cfg',
489 '.config', 'include/autoconf.mk',
490 'include/generated/autoconf.h'])
492 # Now write the actual build output
495 result.out_dir, build_dir, '',
496 ['u-boot*', '*.bin', '*.map', '*.img', 'MLO', 'SPL',
497 'include/autoconf.mk', 'spl/u-boot-spl*'])
499 def copy_files(self, out_dir, build_dir, dirname, patterns):
500 """Copy files from the build directory to the output.
503 out_dir: Path to output directory containing the files
504 build_dir: Place to copy the files
505 dirname: Source directory, '' for normal U-Boot, 'spl' for SPL
506 patterns: A list of filenames (strings) to copy, each relative
507 to the build directory
509 for pattern in patterns:
510 file_list = glob.glob(os.path.join(out_dir, dirname, pattern))
511 for fname in file_list:
512 target = os.path.basename(fname)
514 base, ext = os.path.splitext(target)
516 target = f'{base}-{dirname}{ext}'
517 shutil.copy(fname, os.path.join(build_dir, target))
519 def _send_result(self, result):
520 """Send a result to the builder for processing
523 result: CommandResult object containing the results of the build
526 ValueError if self.test_exception is true (for testing)
528 if self.test_exception:
529 raise ValueError('test exception')
530 if self.thread_num != -1:
531 self.builder.out_queue.put(result)
533 self.builder.process_result(result)
535 def run_job(self, job):
538 A job consists of a building a list of commits for a particular board.
544 List of Result objects
547 work_dir = self.builder.get_thread_dir(self.thread_num)
548 self.toolchain = None
550 # Run 'make board_defconfig' on the first commit
554 for commit_upto in range(0, len(job.commits), job.step):
555 result, request_config = self.run_commit(commit_upto, brd,
556 work_dir, do_config, self.builder.config_only,
557 force_build or self.builder.force_build,
558 self.builder.force_build_failures,
559 job.work_in_output, job.adjust_cfg)
560 failed = result.return_code or result.stderr
561 did_config = do_config
562 if failed and not do_config:
563 # If our incremental build failed, try building again
565 if self.builder.force_config_on_failure:
566 result, request_config = self.run_commit(commit_upto,
567 brd, work_dir, True, False, True, False,
568 job.work_in_output, job.adjust_cfg)
570 if not self.builder.force_reconfig:
571 do_config = request_config
573 # If we built that commit, then config is done. But if we got
574 # an warning, reconfig next time to force it to build the same
575 # files that created warnings this time. Otherwise an
576 # incremental build may not build the same file, and we will
577 # think that the warning has gone away.
578 # We could avoid this by using -Werror everywhere...
579 # For errors, the problem doesn't happen, since presumably
580 # the build stopped and didn't generate output, so will retry
581 # that file next time. So we could detect warnings and deal
582 # with them specially here. For now, we just reconfigure if
583 # anything goes work.
584 # Of course this is substantially slower if there are build
585 # errors/warnings (e.g. 2-3x slower even if only 10% of builds
587 if (failed and not result.already_done and not did_config and
588 self.builder.force_config_on_failure):
589 # If this build failed, try the next one with a
591 # Sometimes if the board_config.h file changes it can mess
592 # with dependencies, and we get:
593 # make: *** No rule to make target `include/autoconf.mk',
594 # needed by `depend'.
599 if self.builder.force_config_on_failure:
602 result.commit_upto = commit_upto
603 if result.return_code < 0:
604 raise ValueError('Interrupt')
606 # We have the build results, so output the result
607 self._write_result(result, job.keep_outputs, job.work_in_output)
608 self._send_result(result)
610 # Just build the currently checked-out build
611 result, request_config = self.run_commit(None, brd, work_dir, True,
612 self.builder.config_only, True,
613 self.builder.force_build_failures, job.work_in_output,
615 result.commit_upto = 0
616 self._write_result(result, job.keep_outputs, job.work_in_output)
617 self._send_result(result)
620 """Our thread's run function
622 This thread picks a job from the queue, runs it, and then goes to the
626 job = self.builder.queue.get()
629 except Exception as exc:
630 print('Thread exception (use -T0 to run without threads):',
632 self.builder.thread_exceptions.append(exc)
633 self.builder.queue.task_done()