1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright (c) 2014 Google, Inc 3# 4 5import errno 6import glob 7import os 8import shutil 9import sys 10import threading 11 12from patman import command 13from patman import gitutil 14 15RETURN_CODE_RETRY = -1 16BASE_ELF_FILENAMES = ['u-boot', 'spl/u-boot-spl', 'tpl/u-boot-tpl'] 17 18def Mkdir(dirname, parents = False): 19 """Make a directory if it doesn't already exist. 20 21 Args: 22 dirname: Directory to create 23 """ 24 try: 25 if parents: 26 os.makedirs(dirname) 27 else: 28 os.mkdir(dirname) 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) 33 sys.exit(1) 34 pass 35 else: 36 raise 37 38class BuilderJob: 39 """Holds information about a job to be performed by a thread 40 41 Members: 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. 48 """ 49 def __init__(self): 50 self.board = None 51 self.commits = [] 52 self.keep_outputs = False 53 self.step = 1 54 self.work_in_output = False 55 56 57class ResultThread(threading.Thread): 58 """This thread processes results from builder threads. 59 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. 62 """ 63 def __init__(self, builder): 64 """Set up a new result thread 65 66 Args: 67 builder: Builder which will be sent each result 68 """ 69 threading.Thread.__init__(self) 70 self.builder = builder 71 72 def run(self): 73 """Called to start up the result thread. 74 75 We collect the next result job and pass it on to the build. 76 """ 77 while True: 78 result = self.builder.out_queue.get() 79 self.builder.ProcessResult(result) 80 self.builder.out_queue.task_done() 81 82 83class BuilderThread(threading.Thread): 84 """This thread builds U-Boot for a particular board. 85 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. 88 89 Members: 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 """ 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 102 103 def Make(self, commit, brd, stage, cwd, *args, **kwargs): 104 """Run 'make' on a particular commit and board. 105 106 The source code will already be checked out, so the 'commit' 107 argument is only for information. 108 109 Args: 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() 118 119 Returns: 120 CommandResult object 121 """ 122 return self.builder.do_make(commit, brd, stage, cwd, *args, 123 **kwargs) 124 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. 128 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. 131 132 Args: 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 140 failure 141 work_in_output: Use the output directory as the work directory and 142 don't write to a separate output directory. 143 144 Returns: 145 tuple containing: 146 - CommandResult object containing the results of the build 147 - boolean indicating whether 'make config' is still needed 148 """ 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: 154 out_dir = work_dir 155 else: 156 if self.per_board_out_dir: 157 out_rel_dir = os.path.join('..', brd.target) 158 else: 159 out_rel_dir = 'build' 160 out_dir = os.path.join(work_dir, out_rel_dir) 161 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: 170 try: 171 result.return_code = int(fd.readline()) 172 except ValueError: 173 # The file may be empty due to running out of disk space. 174 # Try a rebuild 175 result.return_code = RETURN_CODE_RETRY 176 177 # Check the signal that the build needs to be retried 178 if result.return_code == RETURN_CODE_RETRY: 179 will_build = True 180 elif will_build: 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 186 will_build = False 187 188 if will_build: 189 # We are going to have to build it. First, get a toolchain 190 if not self.toolchain: 191 try: 192 self.toolchain = self.builder.toolchains.Select(brd.arch) 193 except ValueError as err: 194 result.return_code = 10 195 result.stdout = '' 196 result.stderr = str(err) 197 # TODO(sjg@chromium.org): This gets swallowed, but needs 198 # to be reported. 199 200 if self.toolchain: 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, 207 force=True) 208 else: 209 commit = 'current' 210 211 # Set up the environment and command line 212 env = self.toolchain.MakeEnvironment(self.builder.full_path) 213 Mkdir(out_dir) 214 args = [] 215 cwd = work_dir 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. 223 # 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) 228 cwd = None 229 src_dir = os.getcwd() 230 else: 231 args.append('O=%s' % out_rel_dir) 232 if self.builder.verbose_build: 233 args.append('V=1') 234 else: 235 args.append('-s') 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] 241 config_out = '' 242 args.extend(self.builder.toolchains.GetMakeArguments(brd)) 243 args.extend(self.toolchain.MakeArgs()) 244 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): 254 os.remove(fname) 255 256 # If we need to reconfigure, do that now 257 if do_config: 258 config_out = '' 259 if self.mrproper: 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: 268 if config_only: 269 args.append('cfg') 270 result = self.Make(commit, brd, 'build', cwd, *args, 271 env=env) 272 result.stderr = result.stderr.replace(src_dir + '/', '') 273 if self.builder.verbose_build: 274 result.stdout = config_out + result.stdout 275 else: 276 result.return_code = 1 277 result.stderr = 'No tool chain for %s\n' % brd.arch 278 result.already_done = False 279 280 result.toolchain = self.toolchain 281 result.brd = brd 282 result.commit_upto = commit_upto 283 result.out_dir = out_dir 284 return result, do_config 285 286 def _WriteResult(self, result, keep_outputs, work_in_output): 287 """Write a built result to the output directory. 288 289 Args: 290 result: CommandResult object containing result to write 291 keep_outputs: True to store the output binaries, False 292 to delete them 293 work_in_output: Use the output directory as the work directory and 294 don't write to a separate output directory. 295 """ 296 # Fatal error 297 if result.return_code < 0: 298 return 299 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 302 # it. 303 maybe_aborted = result.stderr and 'No child processes' in result.stderr 304 305 if result.already_done: 306 return 307 308 # Write the output and stderr 309 output_dir = self.builder._GetOutputDir(result.commit_upto) 310 Mkdir(output_dir) 311 build_dir = self.builder.GetBuildDir(result.commit_upto, 312 result.brd.target) 313 Mkdir(build_dir) 314 315 outfile = os.path.join(build_dir, 'log') 316 with open(outfile, 'w') as fd: 317 if result.stdout: 318 fd.write(result.stdout) 319 320 errfile = self.builder.GetErrFile(result.commit_upto, 321 result.brd.target) 322 if result.stderr: 323 with open(errfile, 'w') as fd: 324 fd.write(result.stderr) 325 elif os.path.exists(errfile): 326 os.remove(errfile) 327 328 if result.toolchain: 329 # Write the build result and toolchain information. 330 done_file = self.builder.GetDoneFile(result.commit_upto, 331 result.brd.target) 332 with open(done_file, 'w') as fd: 333 if maybe_aborted: 334 # Special code to indicate we need to retry 335 fd.write('%s' % RETURN_CODE_RETRY) 336 else: 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) 344 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) 350 lines = [] 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) 356 if nm_result.stdout: 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) 361 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) 366 rodata_size = '' 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] 376 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] + ' ' + 383 rodata_size) 384 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']) 395 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 399 # rodata size 400 if len(lines): 401 sizes = self.builder.GetSizesFile(result.commit_upto, 402 result.brd.target) 403 with open(sizes, 'w') as fd: 404 print('\n'.join(lines), file=fd) 405 406 if not work_in_output: 407 # Write out the configuration files, with a special case for SPL 408 for dirname in ['', 'spl', 'tpl']: 409 self.CopyFiles( 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']) 414 415 # Now write the actual build output 416 if keep_outputs: 417 self.CopyFiles( 418 result.out_dir, build_dir, '', 419 ['u-boot*', '*.bin', '*.map', '*.img', 'MLO', 'SPL', 420 'include/autoconf.mk', 'spl/u-boot-spl*']) 421 422 def CopyFiles(self, out_dir, build_dir, dirname, patterns): 423 """Copy files from the build directory to the output. 424 425 Args: 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 431 """ 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) 436 if dirname: 437 base, ext = os.path.splitext(target) 438 if ext: 439 target = '%s-%s%s' % (base, dirname, ext) 440 shutil.copy(fname, os.path.join(build_dir, target)) 441 442 def RunJob(self, job): 443 """Run a single job 444 445 A job consists of a building a list of commits for a particular board. 446 447 Args: 448 job: Job to build 449 450 Returns: 451 List of Result objects 452 """ 453 brd = job.board 454 work_dir = self.builder.GetThreadDir(self.thread_num) 455 self.toolchain = None 456 if job.commits: 457 # Run 'make board_defconfig' on the first commit 458 do_config = True 459 commit_upto = 0 460 force_build = False 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 471 # with a reconfig. 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) 476 did_config = True 477 if not self.builder.force_reconfig: 478 do_config = request_config 479 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 493 # have problems). 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 497 # reconfigure. 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'. 502 do_config = True 503 force_build = True 504 else: 505 force_build = False 506 if self.builder.force_config_on_failure: 507 if failed: 508 do_config = True 509 result.commit_upto = commit_upto 510 if result.return_code < 0: 511 raise ValueError('Interrupt') 512 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) 517 else: 518 self.builder.ProcessResult(result) 519 else: 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) 529 else: 530 self.builder.ProcessResult(result) 531 532 def run(self): 533 """Our thread's run function 534 535 This thread picks a job from the queue, runs it, and then goes to the 536 next job. 537 """ 538 while True: 539 job = self.builder.queue.get() 540 self.RunJob(job) 541 self.builder.queue.task_done() 542