1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2014 Google, Inc
12 from patman import command
13 from patman import gitutil
15 RETURN_CODE_RETRY = -1
16 BASE_ELF_FILENAMES = ['u-boot', 'spl/u-boot-spl', 'tpl/u-boot-tpl']
18 def Mkdir(dirname, parents = False):
19 """Make a directory if it doesn't already exist.
22 dirname: Directory to create
29 except OSError as err:
30 if err.errno == errno.EEXIST:
31 if os.path.realpath('.') == os.path.realpath(dirname):
32 print("Cannot create the current working directory '%s'!" % dirname)
39 """Holds information about a job to be performed by a thread
42 board: Board object to build
43 commits: List of Commit objects to build
44 keep_outputs: True to save build output files
45 step: 1 to process every commit, n to process every nth commit
46 work_in_output: Use the output directory as the work directory and
47 don't write to a separate output directory.
52 self.keep_outputs = False
54 self.work_in_output = False
57 class ResultThread(threading.Thread):
58 """This thread processes results from builder threads.
60 It simply passes the results on to the builder. There is only one
61 result thread, and this helps to serialise the build output.
63 def __init__(self, builder):
64 """Set up a new result thread
67 builder: Builder which will be sent each result
69 threading.Thread.__init__(self)
70 self.builder = builder
73 """Called to start up the result thread.
75 We collect the next result job and pass it on to the build.
78 result = self.builder.out_queue.get()
79 self.builder.ProcessResult(result)
80 self.builder.out_queue.task_done()
83 class BuilderThread(threading.Thread):
84 """This thread builds U-Boot for a particular board.
86 An input queue provides each new job. We run 'make' to build U-Boot
87 and then pass the results on to the output queue.
90 builder: The builder which contains information we might need
91 thread_num: Our thread number (0-n-1), used to decide on a
92 temporary directory. If this is -1 then there are no threads
93 and we are the (only) main process
94 mrproper: Use 'make mrproper' before each reconfigure
95 per_board_out_dir: True to build in a separate persistent directory per
96 board rather than a thread-specific directory
97 test_exception: Used for testing; True to raise an exception instead of
98 reporting the build result
100 """Set up a new builder thread"""
101 threading.Thread.__init__(self)
102 self.builder = builder
103 self.thread_num = thread_num
104 self.mrproper = mrproper
105 self.per_board_out_dir = per_board_out_dir
107 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
108 """Run 'make' on a particular commit and board.
110 The source code will already be checked out, so the 'commit'
111 argument is only for information.
114 commit: Commit object that is being built
115 brd: Board object that is being built
116 stage: Stage of the build. Valid stages are:
117 mrproper - can be called to clean source
118 config - called to configure for a board
119 build - the main make invocation - it does the build
120 args: A list of arguments to pass to 'make'
121 kwargs: A list of keyword arguments to pass to command.RunPipe()
126 return self.builder.do_make(commit, brd, stage, cwd, *args,
129 def RunCommit(self, commit_upto, brd, work_dir, do_config, config_only,
130 force_build, force_build_failures, work_in_output):
131 """Build a particular commit.
133 If the build is already done, and we are not forcing a build, we skip
134 the build and just return the previously-saved results.
137 commit_upto: Commit number to build (0...n-1)
138 brd: Board object to build
139 work_dir: Directory to which the source will be checked out
140 do_config: True to run a make <board>_defconfig on the source
141 config_only: Only configure the source, do not build it
142 force_build: Force a build even if one was previously done
143 force_build_failures: Force a bulid if the previous result showed
145 work_in_output: Use the output directory as the work directory and
146 don't write to a separate output directory.
150 - CommandResult object containing the results of the build
151 - boolean indicating whether 'make config' is still needed
153 # Create a default result - it will be overwritte by the call to
154 # self.Make() below, in the event that we do a build.
155 result = command.CommandResult()
156 result.return_code = 0
157 if work_in_output or self.builder.in_tree:
160 if self.per_board_out_dir:
161 out_rel_dir = os.path.join('..', brd.target)
163 out_rel_dir = 'build'
164 out_dir = os.path.join(work_dir, out_rel_dir)
166 # Check if the job was already completed last time
167 done_file = self.builder.GetDoneFile(commit_upto, brd.target)
168 result.already_done = os.path.exists(done_file)
169 will_build = (force_build or force_build_failures or
170 not result.already_done)
171 if result.already_done:
172 # Get the return code from that build and use it
173 with open(done_file, 'r') as fd:
175 result.return_code = int(fd.readline())
177 # The file may be empty due to running out of disk space.
179 result.return_code = RETURN_CODE_RETRY
181 # Check the signal that the build needs to be retried
182 if result.return_code == RETURN_CODE_RETRY:
185 err_file = self.builder.GetErrFile(commit_upto, brd.target)
186 if os.path.exists(err_file) and os.stat(err_file).st_size:
187 result.stderr = 'bad'
188 elif not force_build:
189 # The build passed, so no need to build it again
193 # We are going to have to build it. First, get a toolchain
194 if not self.toolchain:
196 self.toolchain = self.builder.toolchains.Select(brd.arch)
197 except ValueError as err:
198 result.return_code = 10
200 result.stderr = str(err)
201 # TODO(sjg@chromium.org): This gets swallowed, but needs
205 # Checkout the right commit
206 if self.builder.commits:
207 commit = self.builder.commits[commit_upto]
208 if self.builder.checkout:
209 git_dir = os.path.join(work_dir, '.git')
210 gitutil.Checkout(commit.hash, git_dir, work_dir,
215 # Set up the environment and command line
216 env = self.toolchain.MakeEnvironment(self.builder.full_path)
220 src_dir = os.path.realpath(work_dir)
221 if not self.builder.in_tree:
222 if commit_upto is None:
223 # In this case we are building in the original source
224 # directory (i.e. the current directory where buildman
225 # is invoked. The output directory is set to this
226 # thread's selected work directory.
228 # Symlinks can confuse U-Boot's Makefile since
229 # we may use '..' in our path, so remove them.
230 out_dir = os.path.realpath(out_dir)
231 args.append('O=%s' % out_dir)
233 src_dir = os.getcwd()
235 args.append('O=%s' % out_rel_dir)
236 if self.builder.verbose_build:
240 if self.builder.num_jobs is not None:
241 args.extend(['-j', str(self.builder.num_jobs)])
242 if self.builder.warnings_as_errors:
243 args.append('KCFLAGS=-Werror')
244 config_args = ['%s_defconfig' % brd.target]
246 args.extend(self.builder.toolchains.GetMakeArguments(brd))
247 args.extend(self.toolchain.MakeArgs())
249 # Remove any output targets. Since we use a build directory that
250 # was previously used by another board, it may have produced an
251 # SPL image. If we don't remove it (i.e. see do_config and
252 # self.mrproper below) then it will appear to be the output of
253 # this build, even if it does not produce SPL images.
254 build_dir = self.builder.GetBuildDir(commit_upto, brd.target)
255 for elf in BASE_ELF_FILENAMES:
256 fname = os.path.join(out_dir, elf)
257 if os.path.exists(fname):
260 # If we need to reconfigure, do that now
264 result = self.Make(commit, brd, 'mrproper', cwd,
265 'mrproper', *args, env=env)
266 config_out += result.combined
267 result = self.Make(commit, brd, 'config', cwd,
268 *(args + config_args), env=env)
269 config_out += result.combined
270 do_config = False # No need to configure next time
271 if result.return_code == 0:
274 result = self.Make(commit, brd, 'build', cwd, *args,
276 result.stderr = result.stderr.replace(src_dir + '/', '')
277 if self.builder.verbose_build:
278 result.stdout = config_out + result.stdout
280 result.return_code = 1
281 result.stderr = 'No tool chain for %s\n' % brd.arch
282 result.already_done = False
284 result.toolchain = self.toolchain
286 result.commit_upto = commit_upto
287 result.out_dir = out_dir
288 return result, do_config
290 def _WriteResult(self, result, keep_outputs, work_in_output):
291 """Write a built result to the output directory.
294 result: CommandResult object containing result to write
295 keep_outputs: True to store the output binaries, False
297 work_in_output: Use the output directory as the work directory and
298 don't write to a separate output directory.
301 if result.return_code < 0:
304 # If we think this might have been aborted with Ctrl-C, record the
305 # failure but not that we are 'done' with this board. A retry may fix
307 maybe_aborted = result.stderr and 'No child processes' in result.stderr
309 if result.already_done:
312 # Write the output and stderr
313 output_dir = self.builder._GetOutputDir(result.commit_upto)
315 build_dir = self.builder.GetBuildDir(result.commit_upto,
319 outfile = os.path.join(build_dir, 'log')
320 with open(outfile, 'w') as fd:
322 fd.write(result.stdout)
324 errfile = self.builder.GetErrFile(result.commit_upto,
327 with open(errfile, 'w') as fd:
328 fd.write(result.stderr)
329 elif os.path.exists(errfile):
333 # Write the build result and toolchain information.
334 done_file = self.builder.GetDoneFile(result.commit_upto,
336 with open(done_file, 'w') as fd:
338 # Special code to indicate we need to retry
339 fd.write('%s' % RETURN_CODE_RETRY)
341 fd.write('%s' % result.return_code)
342 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
343 print('gcc', result.toolchain.gcc, file=fd)
344 print('path', result.toolchain.path, file=fd)
345 print('cross', result.toolchain.cross, file=fd)
346 print('arch', result.toolchain.arch, file=fd)
347 fd.write('%s' % result.return_code)
349 # Write out the image and function size information and an objdump
350 env = result.toolchain.MakeEnvironment(self.builder.full_path)
351 with open(os.path.join(build_dir, 'out-env'), 'w',
352 encoding='utf-8') as fd:
353 for var in sorted(env.keys()):
354 print('%s="%s"' % (var, env[var]), file=fd)
356 for fname in BASE_ELF_FILENAMES:
357 cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
358 nm_result = command.RunPipe([cmd], capture=True,
359 capture_stderr=True, cwd=result.out_dir,
360 raise_on_error=False, env=env)
362 nm = self.builder.GetFuncSizesFile(result.commit_upto,
363 result.brd.target, fname)
364 with open(nm, 'w') as fd:
365 print(nm_result.stdout, end=' ', file=fd)
367 cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
368 dump_result = command.RunPipe([cmd], capture=True,
369 capture_stderr=True, cwd=result.out_dir,
370 raise_on_error=False, env=env)
372 if dump_result.stdout:
373 objdump = self.builder.GetObjdumpFile(result.commit_upto,
374 result.brd.target, fname)
375 with open(objdump, 'w') as fd:
376 print(dump_result.stdout, end=' ', file=fd)
377 for line in dump_result.stdout.splitlines():
378 fields = line.split()
379 if len(fields) > 5 and fields[1] == '.rodata':
380 rodata_size = fields[2]
382 cmd = ['%ssize' % self.toolchain.cross, fname]
383 size_result = command.RunPipe([cmd], capture=True,
384 capture_stderr=True, cwd=result.out_dir,
385 raise_on_error=False, env=env)
386 if size_result.stdout:
387 lines.append(size_result.stdout.splitlines()[1] + ' ' +
390 # Extract the environment from U-Boot and dump it out
391 cmd = ['%sobjcopy' % self.toolchain.cross, '-O', 'binary',
392 '-j', '.rodata.default_environment',
393 'env/built-in.o', 'uboot.env']
394 command.RunPipe([cmd], capture=True,
395 capture_stderr=True, cwd=result.out_dir,
396 raise_on_error=False, env=env)
397 ubootenv = os.path.join(result.out_dir, 'uboot.env')
398 if not work_in_output:
399 self.CopyFiles(result.out_dir, build_dir, '', ['uboot.env'])
401 # Write out the image sizes file. This is similar to the output
402 # of binutil's 'size' utility, but it omits the header line and
403 # adds an additional hex value at the end of each line for the
406 sizes = self.builder.GetSizesFile(result.commit_upto,
408 with open(sizes, 'w') as fd:
409 print('\n'.join(lines), file=fd)
411 if not work_in_output:
412 # Write out the configuration files, with a special case for SPL
413 for dirname in ['', 'spl', 'tpl']:
415 result.out_dir, build_dir, dirname,
416 ['u-boot.cfg', 'spl/u-boot-spl.cfg', 'tpl/u-boot-tpl.cfg',
417 '.config', 'include/autoconf.mk',
418 'include/generated/autoconf.h'])
420 # Now write the actual build output
423 result.out_dir, build_dir, '',
424 ['u-boot*', '*.bin', '*.map', '*.img', 'MLO', 'SPL',
425 'include/autoconf.mk', 'spl/u-boot-spl*'])
427 def CopyFiles(self, out_dir, build_dir, dirname, patterns):
428 """Copy files from the build directory to the output.
431 out_dir: Path to output directory containing the files
432 build_dir: Place to copy the files
433 dirname: Source directory, '' for normal U-Boot, 'spl' for SPL
434 patterns: A list of filenames (strings) to copy, each relative
435 to the build directory
437 for pattern in patterns:
438 file_list = glob.glob(os.path.join(out_dir, dirname, pattern))
439 for fname in file_list:
440 target = os.path.basename(fname)
442 base, ext = os.path.splitext(target)
444 target = '%s-%s%s' % (base, dirname, ext)
445 shutil.copy(fname, os.path.join(build_dir, target))
447 def RunJob(self, job):
450 A job consists of a building a list of commits for a particular board.
456 List of Result objects
459 work_dir = self.builder.GetThreadDir(self.thread_num)
460 self.toolchain = None
462 # Run 'make board_defconfig' on the first commit
466 for commit_upto in range(0, len(job.commits), job.step):
467 result, request_config = self.RunCommit(commit_upto, brd,
468 work_dir, do_config, self.builder.config_only,
469 force_build or self.builder.force_build,
470 self.builder.force_build_failures,
471 work_in_output=job.work_in_output)
472 failed = result.return_code or result.stderr
473 did_config = do_config
474 if failed and not do_config:
475 # If our incremental build failed, try building again
477 if self.builder.force_config_on_failure:
478 result, request_config = self.RunCommit(commit_upto,
479 brd, work_dir, True, False, True, False,
480 work_in_output=job.work_in_output)
482 if not self.builder.force_reconfig:
483 do_config = request_config
485 # If we built that commit, then config is done. But if we got
486 # an warning, reconfig next time to force it to build the same
487 # files that created warnings this time. Otherwise an
488 # incremental build may not build the same file, and we will
489 # think that the warning has gone away.
490 # We could avoid this by using -Werror everywhere...
491 # For errors, the problem doesn't happen, since presumably
492 # the build stopped and didn't generate output, so will retry
493 # that file next time. So we could detect warnings and deal
494 # with them specially here. For now, we just reconfigure if
495 # anything goes work.
496 # Of course this is substantially slower if there are build
497 # errors/warnings (e.g. 2-3x slower even if only 10% of builds
499 if (failed and not result.already_done and not did_config and
500 self.builder.force_config_on_failure):
501 # If this build failed, try the next one with a
503 # Sometimes if the board_config.h file changes it can mess
504 # with dependencies, and we get:
505 # make: *** No rule to make target `include/autoconf.mk',
506 # needed by `depend'.
511 if self.builder.force_config_on_failure:
514 result.commit_upto = commit_upto
515 if result.return_code < 0:
516 raise ValueError('Interrupt')
518 # We have the build results, so output the result
519 self._WriteResult(result, job.keep_outputs, job.work_in_output)
520 if self.thread_num != -1:
521 self.builder.out_queue.put(result)
523 self.builder.ProcessResult(result)
525 # Just build the currently checked-out build
526 result, request_config = self.RunCommit(None, brd, work_dir, True,
527 self.builder.config_only, True,
528 self.builder.force_build_failures,
529 work_in_output=job.work_in_output)
530 result.commit_upto = 0
531 self._WriteResult(result, job.keep_outputs, job.work_in_output)
532 if self.thread_num != -1:
533 self.builder.out_queue.put(result)
535 self.builder.ProcessResult(result)
538 """Our thread's run function
540 This thread picks a job from the queue, runs it, and then goes to the
544 job = self.builder.queue.get()
546 self.builder.queue.task_done()