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
95 def __init__(self, builder, thread_num, mrproper, per_board_out_dir):
96 """Set up a new builder thread"""
97 threading.Thread.__init__(self)
98 self.builder = builder
99 self.thread_num = thread_num
100 self.mrproper = mrproper
101 self.per_board_out_dir = per_board_out_dir
103 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
104 """Run 'make' on a particular commit and board.
106 The source code will already be checked out, so the 'commit'
107 argument is only for information.
110 commit: Commit object that is being built
111 brd: Board object that is being built
112 stage: Stage of the build. Valid stages are:
113 mrproper - can be called to clean source
114 config - called to configure for a board
115 build - the main make invocation - it does the build
116 args: A list of arguments to pass to 'make'
117 kwargs: A list of keyword arguments to pass to command.RunPipe()
122 return self.builder.do_make(commit, brd, stage, cwd, *args,
125 def RunCommit(self, commit_upto, brd, work_dir, do_config, config_only,
126 force_build, force_build_failures, work_in_output):
127 """Build a particular commit.
129 If the build is already done, and we are not forcing a build, we skip
130 the build and just return the previously-saved results.
133 commit_upto: Commit number to build (0...n-1)
134 brd: Board object to build
135 work_dir: Directory to which the source will be checked out
136 do_config: True to run a make <board>_defconfig on the source
137 config_only: Only configure the source, do not build it
138 force_build: Force a build even if one was previously done
139 force_build_failures: Force a bulid if the previous result showed
141 work_in_output: Use the output directory as the work directory and
142 don't write to a separate output directory.
146 - CommandResult object containing the results of the build
147 - boolean indicating whether 'make config' is still needed
149 # Create a default result - it will be overwritte by the call to
150 # self.Make() below, in the event that we do a build.
151 result = command.CommandResult()
152 result.return_code = 0
153 if work_in_output or self.builder.in_tree:
156 if self.per_board_out_dir:
157 out_rel_dir = os.path.join('..', brd.target)
159 out_rel_dir = 'build'
160 out_dir = os.path.join(work_dir, out_rel_dir)
162 # Check if the job was already completed last time
163 done_file = self.builder.GetDoneFile(commit_upto, brd.target)
164 result.already_done = os.path.exists(done_file)
165 will_build = (force_build or force_build_failures or
166 not result.already_done)
167 if result.already_done:
168 # Get the return code from that build and use it
169 with open(done_file, 'r') as fd:
171 result.return_code = int(fd.readline())
173 # The file may be empty due to running out of disk space.
175 result.return_code = RETURN_CODE_RETRY
177 # Check the signal that the build needs to be retried
178 if result.return_code == RETURN_CODE_RETRY:
181 err_file = self.builder.GetErrFile(commit_upto, brd.target)
182 if os.path.exists(err_file) and os.stat(err_file).st_size:
183 result.stderr = 'bad'
184 elif not force_build:
185 # The build passed, so no need to build it again
189 # We are going to have to build it. First, get a toolchain
190 if not self.toolchain:
192 self.toolchain = self.builder.toolchains.Select(brd.arch)
193 except ValueError as err:
194 result.return_code = 10
196 result.stderr = str(err)
201 # Checkout the right commit
202 if self.builder.commits:
203 commit = self.builder.commits[commit_upto]
204 if self.builder.checkout:
205 git_dir = os.path.join(work_dir, '.git')
206 gitutil.Checkout(commit.hash, git_dir, work_dir,
211 # Set up the environment and command line
212 env = self.toolchain.MakeEnvironment(self.builder.full_path)
216 src_dir = os.path.realpath(work_dir)
217 if not self.builder.in_tree:
218 if commit_upto is None:
219 # In this case we are building in the original source
220 # directory (i.e. the current directory where buildman
221 # is invoked. The output directory is set to this
222 # thread's selected work directory.
224 # Symlinks can confuse U-Boot's Makefile since
225 # we may use '..' in our path, so remove them.
226 out_dir = os.path.realpath(out_dir)
227 args.append('O=%s' % out_dir)
229 src_dir = os.getcwd()
231 args.append('O=%s' % out_rel_dir)
232 if self.builder.verbose_build:
236 if self.builder.num_jobs is not None:
237 args.extend(['-j', str(self.builder.num_jobs)])
238 if self.builder.warnings_as_errors:
239 args.append('KCFLAGS=-Werror')
240 config_args = ['%s_defconfig' % brd.target]
242 args.extend(self.builder.toolchains.GetMakeArguments(brd))
243 args.extend(self.toolchain.MakeArgs())
245 # Remove any output targets. Since we use a build directory that
246 # was previously used by another board, it may have produced an
247 # SPL image. If we don't remove it (i.e. see do_config and
248 # self.mrproper below) then it will appear to be the output of
249 # this build, even if it does not produce SPL images.
250 build_dir = self.builder.GetBuildDir(commit_upto, brd.target)
251 for elf in BASE_ELF_FILENAMES:
252 fname = os.path.join(out_dir, elf)
253 if os.path.exists(fname):
256 # If we need to reconfigure, do that now
260 result = self.Make(commit, brd, 'mrproper', cwd,
261 'mrproper', *args, env=env)
262 config_out += result.combined
263 result = self.Make(commit, brd, 'config', cwd,
264 *(args + config_args), env=env)
265 config_out += result.combined
266 do_config = False # No need to configure next time
267 if result.return_code == 0:
270 result = self.Make(commit, brd, 'build', cwd, *args,
272 result.stderr = result.stderr.replace(src_dir + '/', '')
273 if self.builder.verbose_build:
274 result.stdout = config_out + result.stdout
276 result.return_code = 1
277 result.stderr = 'No tool chain for %s\n' % brd.arch
278 result.already_done = False
280 result.toolchain = self.toolchain
282 result.commit_upto = commit_upto
283 result.out_dir = out_dir
284 return result, do_config
286 def _WriteResult(self, result, keep_outputs, work_in_output):
287 """Write a built result to the output directory.
290 result: CommandResult object containing result to write
291 keep_outputs: True to store the output binaries, False
293 work_in_output: Use the output directory as the work directory and
294 don't write to a separate output directory.
297 if result.return_code < 0:
300 # If we think this might have been aborted with Ctrl-C, record the
301 # failure but not that we are 'done' with this board. A retry may fix
303 maybe_aborted = result.stderr and 'No child processes' in result.stderr
305 if result.already_done:
308 # Write the output and stderr
309 output_dir = self.builder._GetOutputDir(result.commit_upto)
311 build_dir = self.builder.GetBuildDir(result.commit_upto,
315 outfile = os.path.join(build_dir, 'log')
316 with open(outfile, 'w') as fd:
318 fd.write(result.stdout)
320 errfile = self.builder.GetErrFile(result.commit_upto,
323 with open(errfile, 'w') as fd:
324 fd.write(result.stderr)
325 elif os.path.exists(errfile):
329 # Write the build result and toolchain information.
330 done_file = self.builder.GetDoneFile(result.commit_upto,
332 with open(done_file, 'w') as fd:
334 # Special code to indicate we need to retry
335 fd.write('%s' % RETURN_CODE_RETRY)
337 fd.write('%s' % result.return_code)
338 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
339 print('gcc', result.toolchain.gcc, file=fd)
340 print('path', result.toolchain.path, file=fd)
341 print('cross', result.toolchain.cross, file=fd)
342 print('arch', result.toolchain.arch, file=fd)
343 fd.write('%s' % result.return_code)
345 # Write out the image and function size information and an objdump
346 env = result.toolchain.MakeEnvironment(self.builder.full_path)
347 with open(os.path.join(build_dir, 'out-env'), 'w') as fd:
348 for var in sorted(env.keys()):
349 print('%s="%s"' % (var, env[var]), file=fd)
351 for fname in BASE_ELF_FILENAMES:
352 cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
353 nm_result = command.RunPipe([cmd], capture=True,
354 capture_stderr=True, cwd=result.out_dir,
355 raise_on_error=False, env=env)
357 nm = self.builder.GetFuncSizesFile(result.commit_upto,
358 result.brd.target, fname)
359 with open(nm, 'w') as fd:
360 print(nm_result.stdout, end=' ', file=fd)
362 cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
363 dump_result = command.RunPipe([cmd], capture=True,
364 capture_stderr=True, cwd=result.out_dir,
365 raise_on_error=False, env=env)
367 if dump_result.stdout:
368 objdump = self.builder.GetObjdumpFile(result.commit_upto,
369 result.brd.target, fname)
370 with open(objdump, 'w') as fd:
371 print(dump_result.stdout, end=' ', file=fd)
372 for line in dump_result.stdout.splitlines():
373 fields = line.split()
374 if len(fields) > 5 and fields[1] == '.rodata':
375 rodata_size = fields[2]
377 cmd = ['%ssize' % self.toolchain.cross, fname]
378 size_result = command.RunPipe([cmd], capture=True,
379 capture_stderr=True, cwd=result.out_dir,
380 raise_on_error=False, env=env)
381 if size_result.stdout:
382 lines.append(size_result.stdout.splitlines()[1] + ' ' +
385 # Extract the environment from U-Boot and dump it out
386 cmd = ['%sobjcopy' % self.toolchain.cross, '-O', 'binary',
387 '-j', '.rodata.default_environment',
388 'env/built-in.o', 'uboot.env']
389 command.RunPipe([cmd], capture=True,
390 capture_stderr=True, cwd=result.out_dir,
391 raise_on_error=False, env=env)
392 ubootenv = os.path.join(result.out_dir, 'uboot.env')
393 if not work_in_output:
394 self.CopyFiles(result.out_dir, build_dir, '', ['uboot.env'])
396 # Write out the image sizes file. This is similar to the output
397 # of binutil's 'size' utility, but it omits the header line and
398 # adds an additional hex value at the end of each line for the
401 sizes = self.builder.GetSizesFile(result.commit_upto,
403 with open(sizes, 'w') as fd:
404 print('\n'.join(lines), file=fd)
406 if not work_in_output:
407 # Write out the configuration files, with a special case for SPL
408 for dirname in ['', 'spl', 'tpl']:
410 result.out_dir, build_dir, dirname,
411 ['u-boot.cfg', 'spl/u-boot-spl.cfg', 'tpl/u-boot-tpl.cfg',
412 '.config', 'include/autoconf.mk',
413 'include/generated/autoconf.h'])
415 # Now write the actual build output
418 result.out_dir, build_dir, '',
419 ['u-boot*', '*.bin', '*.map', '*.img', 'MLO', 'SPL',
420 'include/autoconf.mk', 'spl/u-boot-spl*'])
422 def CopyFiles(self, out_dir, build_dir, dirname, patterns):
423 """Copy files from the build directory to the output.
426 out_dir: Path to output directory containing the files
427 build_dir: Place to copy the files
428 dirname: Source directory, '' for normal U-Boot, 'spl' for SPL
429 patterns: A list of filenames (strings) to copy, each relative
430 to the build directory
432 for pattern in patterns:
433 file_list = glob.glob(os.path.join(out_dir, dirname, pattern))
434 for fname in file_list:
435 target = os.path.basename(fname)
437 base, ext = os.path.splitext(target)
439 target = '%s-%s%s' % (base, dirname, ext)
440 shutil.copy(fname, os.path.join(build_dir, target))
442 def RunJob(self, job):
445 A job consists of a building a list of commits for a particular board.
451 List of Result objects
454 work_dir = self.builder.GetThreadDir(self.thread_num)
455 self.toolchain = None
457 # Run 'make board_defconfig' on the first commit
461 for commit_upto in range(0, len(job.commits), job.step):
462 result, request_config = self.RunCommit(commit_upto, brd,
463 work_dir, do_config, self.builder.config_only,
464 force_build or self.builder.force_build,
465 self.builder.force_build_failures,
466 work_in_output=job.work_in_output)
467 failed = result.return_code or result.stderr
468 did_config = do_config
469 if failed and not do_config:
470 # If our incremental build failed, try building again
472 if self.builder.force_config_on_failure:
473 result, request_config = self.RunCommit(commit_upto,
474 brd, work_dir, True, False, True, False,
475 work_in_output=job.work_in_output)
477 if not self.builder.force_reconfig:
478 do_config = request_config
480 # If we built that commit, then config is done. But if we got
481 # an warning, reconfig next time to force it to build the same
482 # files that created warnings this time. Otherwise an
483 # incremental build may not build the same file, and we will
484 # think that the warning has gone away.
485 # We could avoid this by using -Werror everywhere...
486 # For errors, the problem doesn't happen, since presumably
487 # the build stopped and didn't generate output, so will retry
488 # that file next time. So we could detect warnings and deal
489 # with them specially here. For now, we just reconfigure if
490 # anything goes work.
491 # Of course this is substantially slower if there are build
492 # errors/warnings (e.g. 2-3x slower even if only 10% of builds
494 if (failed and not result.already_done and not did_config and
495 self.builder.force_config_on_failure):
496 # If this build failed, try the next one with a
498 # Sometimes if the board_config.h file changes it can mess
499 # with dependencies, and we get:
500 # make: *** No rule to make target `include/autoconf.mk',
501 # needed by `depend'.
506 if self.builder.force_config_on_failure:
509 result.commit_upto = commit_upto
510 if result.return_code < 0:
511 raise ValueError('Interrupt')
513 # We have the build results, so output the result
514 self._WriteResult(result, job.keep_outputs, job.work_in_output)
515 if self.thread_num != -1:
516 self.builder.out_queue.put(result)
518 self.builder.ProcessResult(result)
520 # Just build the currently checked-out build
521 result, request_config = self.RunCommit(None, brd, work_dir, True,
522 self.builder.config_only, True,
523 self.builder.force_build_failures,
524 work_in_output=job.work_in_output)
525 result.commit_upto = 0
526 self._WriteResult(result, job.keep_outputs, job.work_in_output)
527 if self.thread_num != -1:
528 self.builder.out_queue.put(result)
530 self.builder.ProcessResult(result)
533 """Our thread's run function
535 This thread picks a job from the queue, runs it, and then goes to the
539 job = self.builder.queue.get()
541 self.builder.queue.task_done()