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 (str): Directory to create
31 parents (bool): True to also make parent directories
34 OSError: File already exists
41 except OSError as err:
42 if err.errno == errno.EEXIST:
43 if os.path.realpath('.') == os.path.realpath(dirname):
44 print(f"Cannot create the current working directory '{dirname}'!")
50 def _remove_old_outputs(out_dir):
51 """Remove any old output-target files
54 out_dir (str): Output directory for the build
56 Since we use a build directory that was previously used by another
57 board, it may have produced an SPL image. If we don't remove it (i.e.
58 see do_config and self.mrproper below) then it will appear to be the
59 output of this build, even if it does not produce SPL images.
61 for elf in BASE_ELF_FILENAMES:
62 fname = os.path.join(out_dir, elf)
63 if os.path.exists(fname):
67 def copy_files(out_dir, build_dir, dirname, patterns):
68 """Copy files from the build directory to the output.
71 out_dir (str): Path to output directory containing the files
72 build_dir (str): Place to copy the files
73 dirname (str): Source directory, '' for normal U-Boot, 'spl' for SPL
74 patterns (list of str): A list of filenames to copy, each relative
75 to the build directory
77 for pattern in patterns:
78 file_list = glob.glob(os.path.join(out_dir, dirname, pattern))
79 for fname in file_list:
80 target = os.path.basename(fname)
82 base, ext = os.path.splitext(target)
84 target = f'{base}-{dirname}{ext}'
85 shutil.copy(fname, os.path.join(build_dir, target))
88 # pylint: disable=R0903
90 """Holds information about a job to be performed by a thread
93 brd: Board object to build
94 commits: List of Commit objects to build
95 keep_outputs: True to save build output files
96 step: 1 to process every commit, n to process every nth commit
97 work_in_output: Use the output directory as the work directory and
98 don't write to a separate output directory.
103 self.keep_outputs = False
105 self.work_in_output = False
108 class ResultThread(threading.Thread):
109 """This thread processes results from builder threads.
111 It simply passes the results on to the builder. There is only one
112 result thread, and this helps to serialise the build output.
114 def __init__(self, builder):
115 """Set up a new result thread
118 builder: Builder which will be sent each result
120 threading.Thread.__init__(self)
121 self.builder = builder
124 """Called to start up the result thread.
126 We collect the next result job and pass it on to the build.
129 result = self.builder.out_queue.get()
130 self.builder.process_result(result)
131 self.builder.out_queue.task_done()
134 class BuilderThread(threading.Thread):
135 """This thread builds U-Boot for a particular board.
137 An input queue provides each new job. We run 'make' to build U-Boot
138 and then pass the results on to the output queue.
141 builder: The builder which contains information we might need
142 thread_num: Our thread number (0-n-1), used to decide on a
143 temporary directory. If this is -1 then there are no threads
144 and we are the (only) main process
145 mrproper: Use 'make mrproper' before each reconfigure
146 per_board_out_dir: True to build in a separate persistent directory per
147 board rather than a thread-specific directory
148 test_exception: Used for testing; True to raise an exception instead of
149 reporting the build result
151 def __init__(self, builder, thread_num, mrproper, per_board_out_dir,
152 test_exception=False):
153 """Set up a new builder thread"""
154 threading.Thread.__init__(self)
155 self.builder = builder
156 self.thread_num = thread_num
157 self.mrproper = mrproper
158 self.per_board_out_dir = per_board_out_dir
159 self.test_exception = test_exception
160 self.toolchain = None
162 def make(self, commit, brd, stage, cwd, *args, **kwargs):
163 """Run 'make' on a particular commit and board.
165 The source code will already be checked out, so the 'commit'
166 argument is only for information.
169 commit (Commit): Commit that is being built
170 brd (Board): Board that is being built
171 stage (str): Stage of the build. Valid stages are:
172 mrproper - can be called to clean source
173 config - called to configure for a board
174 build - the main make invocation - it does the build
175 cwd (str): Working directory to set, or None to leave it alone
176 *args (list of str): Arguments to pass to 'make'
177 **kwargs (dict): A list of keyword arguments to pass to
183 return self.builder.do_make(commit, brd, stage, cwd, *args,
186 def _build_args(self, brd, out_dir, out_rel_dir, work_dir, commit_upto):
187 """Set up arguments to the args list based on the settings
190 brd (Board): Board to create arguments for
191 out_dir (str): Path to output directory containing the files
192 out_rel_dir (str): Output directory relative to the current dir
193 work_dir (str): Directory to which the source will be checked out
194 commit_upto (int): Commit number to build (0...n-1)
198 list of str: Arguments to pass to make
199 str: Current working directory, or None if no commit
200 str: Source directory (typically the work directory)
204 src_dir = os.path.realpath(work_dir)
205 if not self.builder.in_tree:
206 if commit_upto is None:
207 # In this case we are building in the original source directory
208 # (i.e. the current directory where buildman is invoked. The
209 # output directory is set to this thread's selected work
212 # Symlinks can confuse U-Boot's Makefile since we may use '..'
213 # in our path, so remove them.
214 real_dir = os.path.realpath(out_dir)
215 args.append(f'O={real_dir}')
217 src_dir = os.getcwd()
219 args.append(f'O={out_rel_dir}')
220 if self.builder.verbose_build:
224 if self.builder.num_jobs is not None:
225 args.extend(['-j', str(self.builder.num_jobs)])
226 if self.builder.warnings_as_errors:
227 args.append('KCFLAGS=-Werror')
228 args.append('HOSTCFLAGS=-Werror')
229 if self.builder.allow_missing:
230 args.append('BINMAN_ALLOW_MISSING=1')
231 if self.builder.no_lto:
232 args.append('NO_LTO=1')
233 if self.builder.reproducible_builds:
234 args.append('SOURCE_DATE_EPOCH=0')
235 args.extend(self.builder.toolchains.GetMakeArguments(brd))
236 args.extend(self.toolchain.MakeArgs())
237 return args, cwd, src_dir
239 def _reconfigure(self, commit, brd, cwd, args, env, config_args, config_out,
241 """Reconfigure the build
244 commit (Commit): Commit only being built
245 brd (Board): Board being built
246 cwd (str): Current working directory
247 args (list of str): Arguments to pass to make
248 env (dict): Environment strings
249 config_args (list of str): defconfig arg for this board
250 cmd_list (list of str): List to add the commands to, for logging
256 result = self.make(commit, brd, 'mrproper', cwd, 'mrproper', *args,
258 config_out.write(result.combined)
259 cmd_list.append([self.builder.gnu_make, 'mrproper', *args])
260 result = self.make(commit, brd, 'config', cwd, *(args + config_args),
262 cmd_list.append([self.builder.gnu_make] + args + config_args)
263 config_out.write(result.combined)
266 def _build(self, commit, brd, cwd, args, env, cmd_list, config_only):
270 commit (Commit): Commit only being built
271 brd (Board): Board being built
272 cwd (str): Current working directory
273 args (list of str): Arguments to pass to make
274 env (dict): Environment strings
275 cmd_list (list of str): List to add the commands to, for logging
276 config_only (bool): True if this is a config-only build (using the
284 result = self.make(commit, brd, 'build', cwd, *args, env=env)
285 cmd_list.append([self.builder.gnu_make] + args)
286 if (result.return_code == 2 and
287 ('Some images are invalid' in result.stderr)):
288 # This is handled later by the check for output in stderr
289 result.return_code = 0
292 def _read_done_file(self, commit_upto, brd, force_build,
293 force_build_failures):
294 """Check the 'done' file and see if this commit should be built
297 commit (Commit): Commit only being built
298 brd (Board): Board being built
299 force_build (bool): Force a build even if one was previously done
300 force_build_failures (bool): Force a bulid if the previous result
305 bool: True if build should be built
306 CommandResult: if there was a previous run:
307 - already_done set to True
308 - return_code set to return code
309 - result.stderr set to 'bad' if stderr output was recorded
311 result = command.CommandResult()
312 done_file = self.builder.get_done_file(commit_upto, brd.target)
313 result.already_done = os.path.exists(done_file)
314 will_build = (force_build or force_build_failures or
315 not result.already_done)
316 if result.already_done:
317 with open(done_file, 'r', encoding='utf-8') as outf:
319 result.return_code = int(outf.readline())
321 # The file may be empty due to running out of disk space.
323 result.return_code = RETURN_CODE_RETRY
325 # Check the signal that the build needs to be retried
326 if result.return_code == RETURN_CODE_RETRY:
329 err_file = self.builder.get_err_file(commit_upto, brd.target)
330 if os.path.exists(err_file) and os.stat(err_file).st_size:
331 result.stderr = 'bad'
332 elif not force_build:
333 # The build passed, so no need to build it again
335 return will_build, result
337 def _decide_dirs(self, brd, work_dir, work_in_output):
338 """Decide the output directory to use
341 work_dir (str): Directory to which the source will be checked out
342 work_in_output (bool): Use the output directory as the work
343 directory and don't write to a separate output directory.
347 out_dir (str): Output directory for the build
348 out_rel_dir (str): Output directory relatie to the current dir
350 if work_in_output or self.builder.in_tree:
354 if self.per_board_out_dir:
355 out_rel_dir = os.path.join('..', brd.target)
357 out_rel_dir = 'build'
358 out_dir = os.path.join(work_dir, out_rel_dir)
359 return out_dir, out_rel_dir
361 def _checkout(self, commit_upto, work_dir):
362 """Checkout the right commit
365 commit_upto (int): Commit number to build (0...n-1)
366 work_dir (str): Directory to which the source will be checked out
369 Commit: Commit being built, or 'current' for current source
371 if self.builder.commits:
372 commit = self.builder.commits[commit_upto]
373 if self.builder.checkout:
374 git_dir = os.path.join(work_dir, '.git')
375 gitutil.checkout(commit.hash, git_dir, work_dir, force=True)
380 def _config_and_build(self, commit_upto, brd, work_dir, do_config,
381 config_only, adjust_cfg, commit, out_dir, out_rel_dir,
383 """Do the build, configuring first if necessary
386 commit_upto (int): Commit number to build (0...n-1)
387 brd (Board): Board to create arguments for
388 work_dir (str): Directory to which the source will be checked out
389 do_config (bool): True to run a make <board>_defconfig on the source
390 config_only (bool): Only configure the source, do not build it
391 adjust_cfg (list of str): See the cfgutil module and run_commit()
392 commit (Commit): Commit only being built
393 out_dir (str): Output directory for the build
394 out_rel_dir (str): Output directory relatie to the current dir
395 result (CommandResult): Previous result
399 result (CommandResult): Result of the build
400 do_config (bool): indicates whether 'make config' is needed on
401 the next incremental build
403 # Set up the environment and command line
404 env = self.toolchain.MakeEnvironment(self.builder.full_path)
407 args, cwd, src_dir = self._build_args(brd, out_dir, out_rel_dir,
408 work_dir, commit_upto)
409 config_args = [f'{brd.target}_defconfig']
410 config_out = io.StringIO()
412 _remove_old_outputs(out_dir)
414 # If we need to reconfigure, do that now
415 cfg_file = os.path.join(out_dir, '.config')
417 if do_config or adjust_cfg:
418 result = self._reconfigure(
419 commit, brd, cwd, args, env, config_args, config_out, cmd_list)
420 do_config = False # No need to configure next time
422 cfgutil.adjust_cfg_file(cfg_file, adjust_cfg)
424 # Now do the build, if everything looks OK
425 if result.return_code == 0:
426 result = self._build(commit, brd, cwd, args, env, cmd_list,
429 errs = cfgutil.check_cfg_file(cfg_file, adjust_cfg)
431 result.stderr += errs
432 result.return_code = 1
433 result.stderr = result.stderr.replace(src_dir + '/', '')
434 if self.builder.verbose_build:
435 result.stdout = config_out.getvalue() + result.stdout
436 result.cmd_list = cmd_list
437 return result, do_config
439 def run_commit(self, commit_upto, brd, work_dir, do_config, config_only,
440 force_build, force_build_failures, work_in_output,
442 """Build a particular commit.
444 If the build is already done, and we are not forcing a build, we skip
445 the build and just return the previously-saved results.
448 commit_upto (int): Commit number to build (0...n-1)
449 brd (Board): Board to build
450 work_dir (str): Directory to which the source will be checked out
451 do_config (bool): True to run a make <board>_defconfig on the source
452 config_only (bool): Only configure the source, do not build it
453 force_build (bool): Force a build even if one was previously done
454 force_build_failures (bool): Force a bulid if the previous result
456 work_in_output (bool) : Use the output directory as the work
457 directory and don't write to a separate output directory.
458 adjust_cfg (list of str): List of changes to make to .config file
459 before building. Each is one of (where C is either CONFIG_xxx
463 C=val to set the value of C (val must have quotes if C is
468 - CommandResult object containing the results of the build
469 - boolean indicating whether 'make config' is still needed
471 # Create a default result - it will be overwritte by the call to
472 # self.make() below, in the event that we do a build.
473 out_dir, out_rel_dir = self._decide_dirs(brd, work_dir, work_in_output)
475 # Check if the job was already completed last time
476 will_build, result = self._read_done_file(commit_upto, brd, force_build,
477 force_build_failures)
480 # We are going to have to build it. First, get a toolchain
481 if not self.toolchain:
483 self.toolchain = self.builder.toolchains.Select(brd.arch)
484 except ValueError as err:
485 result.return_code = 10
487 result.stderr = f'Tool chain error for {brd.arch}: {str(err)}'
490 commit = self._checkout(commit_upto, work_dir)
491 result, do_config = self._config_and_build(
492 commit_upto, brd, work_dir, do_config, config_only,
493 adjust_cfg, commit, out_dir, out_rel_dir, result)
494 result.already_done = False
496 result.toolchain = self.toolchain
498 result.commit_upto = commit_upto
499 result.out_dir = out_dir
500 return result, do_config
502 def _write_result(self, result, keep_outputs, work_in_output):
503 """Write a built result to the output directory.
506 result (CommandResult): result to write
507 keep_outputs (bool): True to store the output binaries, False
509 work_in_output (bool): Use the output directory as the work
510 directory and don't write to a separate output directory.
512 # If we think this might have been aborted with Ctrl-C, record the
513 # failure but not that we are 'done' with this board. A retry may fix
515 maybe_aborted = result.stderr and 'No child processes' in result.stderr
517 if result.return_code >= 0 and result.already_done:
520 # Write the output and stderr
521 output_dir = self.builder.get_output_dir(result.commit_upto)
523 build_dir = self.builder.get_build_dir(result.commit_upto,
527 outfile = os.path.join(build_dir, 'log')
528 with open(outfile, 'w', encoding='utf-8') as outf:
530 outf.write(result.stdout)
532 errfile = self.builder.get_err_file(result.commit_upto,
535 with open(errfile, 'w', encoding='utf-8') as outf:
536 outf.write(result.stderr)
537 elif os.path.exists(errfile):
541 if result.return_code < 0:
545 # Write the build result and toolchain information.
546 done_file = self.builder.get_done_file(result.commit_upto,
548 with open(done_file, 'w', encoding='utf-8') as outf:
550 # Special code to indicate we need to retry
551 outf.write(f'{RETURN_CODE_RETRY}')
553 outf.write(f'{result.return_code}')
554 with open(os.path.join(build_dir, 'toolchain'), 'w',
555 encoding='utf-8') as outf:
556 print('gcc', result.toolchain.gcc, file=outf)
557 print('path', result.toolchain.path, file=outf)
558 print('cross', result.toolchain.cross, file=outf)
559 print('arch', result.toolchain.arch, file=outf)
560 outf.write(f'{result.return_code}')
562 # Write out the image and function size information and an objdump
563 env = result.toolchain.MakeEnvironment(self.builder.full_path)
564 with open(os.path.join(build_dir, 'out-env'), 'wb') as outf:
565 for var in sorted(env.keys()):
566 outf.write(b'%s="%s"' % (var, env[var]))
568 with open(os.path.join(build_dir, 'out-cmd'), 'w',
569 encoding='utf-8') as outf:
570 for cmd in result.cmd_list:
571 print(' '.join(cmd), file=outf)
574 for fname in BASE_ELF_FILENAMES:
575 cmd = [f'{self.toolchain.cross}nm', '--size-sort', fname]
576 nm_result = command.run_pipe([cmd], capture=True,
577 capture_stderr=True, cwd=result.out_dir,
578 raise_on_error=False, env=env)
580 nm_fname = self.builder.get_func_sizes_file(
581 result.commit_upto, result.brd.target, fname)
582 with open(nm_fname, 'w', encoding='utf-8') as outf:
583 print(nm_result.stdout, end=' ', file=outf)
585 cmd = [f'{self.toolchain.cross}objdump', '-h', fname]
586 dump_result = command.run_pipe([cmd], capture=True,
587 capture_stderr=True, cwd=result.out_dir,
588 raise_on_error=False, env=env)
590 if dump_result.stdout:
591 objdump = self.builder.get_objdump_file(result.commit_upto,
592 result.brd.target, fname)
593 with open(objdump, 'w', encoding='utf-8') as outf:
594 print(dump_result.stdout, end=' ', file=outf)
595 for line in dump_result.stdout.splitlines():
596 fields = line.split()
597 if len(fields) > 5 and fields[1] == '.rodata':
598 rodata_size = fields[2]
600 cmd = [f'{self.toolchain.cross}size', fname]
601 size_result = command.run_pipe([cmd], capture=True,
602 capture_stderr=True, cwd=result.out_dir,
603 raise_on_error=False, env=env)
604 if size_result.stdout:
605 lines.append(size_result.stdout.splitlines()[1] + ' ' +
608 # Extract the environment from U-Boot and dump it out
609 cmd = [f'{self.toolchain.cross}objcopy', '-O', 'binary',
610 '-j', '.rodata.default_environment',
611 'env/built-in.o', 'uboot.env']
612 command.run_pipe([cmd], capture=True,
613 capture_stderr=True, cwd=result.out_dir,
614 raise_on_error=False, env=env)
615 if not work_in_output:
616 copy_files(result.out_dir, build_dir, '', ['uboot.env'])
618 # Write out the image sizes file. This is similar to the output
619 # of binutil's 'size' utility, but it omits the header line and
620 # adds an additional hex value at the end of each line for the
623 sizes = self.builder.get_sizes_file(result.commit_upto,
625 with open(sizes, 'w', encoding='utf-8') as outf:
626 print('\n'.join(lines), file=outf)
628 if not work_in_output:
629 # Write out the configuration files, with a special case for SPL
630 for dirname in ['', 'spl', 'tpl']:
632 result.out_dir, build_dir, dirname,
633 ['u-boot.cfg', 'spl/u-boot-spl.cfg', 'tpl/u-boot-tpl.cfg',
634 '.config', 'include/autoconf.mk',
635 'include/generated/autoconf.h'])
637 # Now write the actual build output
640 result.out_dir, build_dir, '',
641 ['u-boot*', '*.bin', '*.map', '*.img', 'MLO', 'SPL',
642 'include/autoconf.mk', 'spl/u-boot-spl*'])
644 def _send_result(self, result):
645 """Send a result to the builder for processing
648 result (CommandResult): results of the build
651 ValueError: self.test_exception is true (for testing)
653 if self.test_exception:
654 raise ValueError('test exception')
655 if self.thread_num != -1:
656 self.builder.out_queue.put(result)
658 self.builder.process_result(result)
660 def run_job(self, job):
663 A job consists of a building a list of commits for a particular board.
666 job (Job): Job to build
669 ValueError: Thread was interrupted
672 work_dir = self.builder.get_thread_dir(self.thread_num)
673 self.toolchain = None
675 # Run 'make board_defconfig' on the first commit
679 for commit_upto in range(0, len(job.commits), job.step):
680 result, request_config = self.run_commit(commit_upto, brd,
681 work_dir, do_config, self.builder.config_only,
682 force_build or self.builder.force_build,
683 self.builder.force_build_failures,
684 job.work_in_output, job.adjust_cfg)
685 failed = result.return_code or result.stderr
686 did_config = do_config
687 if failed and not do_config:
688 # If our incremental build failed, try building again
690 if self.builder.force_config_on_failure:
691 result, request_config = self.run_commit(commit_upto,
692 brd, work_dir, True, False, True, False,
693 job.work_in_output, job.adjust_cfg)
695 if not self.builder.force_reconfig:
696 do_config = request_config
698 # If we built that commit, then config is done. But if we got
699 # an warning, reconfig next time to force it to build the same
700 # files that created warnings this time. Otherwise an
701 # incremental build may not build the same file, and we will
702 # think that the warning has gone away.
703 # We could avoid this by using -Werror everywhere...
704 # For errors, the problem doesn't happen, since presumably
705 # the build stopped and didn't generate output, so will retry
706 # that file next time. So we could detect warnings and deal
707 # with them specially here. For now, we just reconfigure if
708 # anything goes work.
709 # Of course this is substantially slower if there are build
710 # errors/warnings (e.g. 2-3x slower even if only 10% of builds
712 if (failed and not result.already_done and not did_config and
713 self.builder.force_config_on_failure):
714 # If this build failed, try the next one with a
716 # Sometimes if the board_config.h file changes it can mess
717 # with dependencies, and we get:
718 # make: *** No rule to make target `include/autoconf.mk',
719 # needed by `depend'.
724 if self.builder.force_config_on_failure:
727 result.commit_upto = commit_upto
728 if result.return_code < 0:
729 raise ValueError('Interrupt')
731 # We have the build results, so output the result
732 self._write_result(result, job.keep_outputs, job.work_in_output)
733 self._send_result(result)
735 # Just build the currently checked-out build
736 result, request_config = self.run_commit(None, brd, work_dir, True,
737 self.builder.config_only, True,
738 self.builder.force_build_failures, job.work_in_output,
740 result.commit_upto = 0
741 self._write_result(result, job.keep_outputs, job.work_in_output)
742 self._send_result(result)
745 """Our thread's run function
747 This thread picks a job from the queue, runs it, and then goes to the
751 job = self.builder.queue.get()
754 except Exception as exc:
755 print('Thread exception (use -T0 to run without threads):',
757 self.builder.thread_exceptions.append(exc)
758 self.builder.queue.task_done()