1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2014 Google, Inc
3#
4
5"""Implementation the bulider threads
6
7This module provides the BuilderThread class, which handles calling the builder
8based on the jobs provided.
9"""
10
11import errno
12import glob
13import io
14import os
15import shutil
16import sys
17import threading
18
19from buildman import cfgutil
20from patman import gitutil
21from u_boot_pylib import command
22
23RETURN_CODE_RETRY = -1
24BASE_ELF_FILENAMES = ['u-boot', 'spl/u-boot-spl', 'tpl/u-boot-tpl']
25
26# Common extensions for images
27COMMON_EXTS = ['.bin', '.rom', '.itb', '.img']
28
29def mkdir(dirname, parents=False):
30    """Make a directory if it doesn't already exist.
31
32    Args:
33        dirname (str): Directory to create
34        parents (bool): True to also make parent directories
35
36    Raises:
37        OSError: File already exists
38    """
39    try:
40        if parents:
41            os.makedirs(dirname)
42        else:
43            os.mkdir(dirname)
44    except OSError as err:
45        if err.errno == errno.EEXIST:
46            if os.path.realpath('.') == os.path.realpath(dirname):
47                print(f"Cannot create the current working directory '{dirname}'!")
48                sys.exit(1)
49        else:
50            raise
51
52
53def _remove_old_outputs(out_dir):
54    """Remove any old output-target files
55
56    Args:
57        out_dir (str): Output directory for the build
58
59    Since we use a build directory that was previously used by another
60    board, it may have produced an SPL image. If we don't remove it (i.e.
61    see do_config and self.mrproper below) then it will appear to be the
62    output of this build, even if it does not produce SPL images.
63    """
64    for elf in BASE_ELF_FILENAMES:
65        fname = os.path.join(out_dir, elf)
66        if os.path.exists(fname):
67            os.remove(fname)
68
69
70def copy_files(out_dir, build_dir, dirname, patterns):
71    """Copy files from the build directory to the output.
72
73    Args:
74        out_dir (str): Path to output directory containing the files
75        build_dir (str): Place to copy the files
76        dirname (str): Source directory, '' for normal U-Boot, 'spl' for SPL
77        patterns (list of str): A list of filenames to copy, each relative
78           to the build directory
79    """
80    for pattern in patterns:
81        file_list = glob.glob(os.path.join(out_dir, dirname, pattern))
82        for fname in file_list:
83            target = os.path.basename(fname)
84            if dirname:
85                base, ext = os.path.splitext(target)
86                if ext:
87                    target = f'{base}-{dirname}{ext}'
88            shutil.copy(fname, os.path.join(build_dir, target))
89
90
91# pylint: disable=R0903
92class BuilderJob:
93    """Holds information about a job to be performed by a thread
94
95    Members:
96        brd: Board object to build
97        commits: List of Commit objects to build
98        keep_outputs: True to save build output files
99        step: 1 to process every commit, n to process every nth commit
100        work_in_output: Use the output directory as the work directory and
101            don't write to a separate output directory.
102    """
103    def __init__(self):
104        self.brd = None
105        self.commits = []
106        self.keep_outputs = False
107        self.step = 1
108        self.work_in_output = False
109
110
111class ResultThread(threading.Thread):
112    """This thread processes results from builder threads.
113
114    It simply passes the results on to the builder. There is only one
115    result thread, and this helps to serialise the build output.
116    """
117    def __init__(self, builder):
118        """Set up a new result thread
119
120        Args:
121            builder: Builder which will be sent each result
122        """
123        threading.Thread.__init__(self)
124        self.builder = builder
125
126    def run(self):
127        """Called to start up the result thread.
128
129        We collect the next result job and pass it on to the build.
130        """
131        while True:
132            result = self.builder.out_queue.get()
133            self.builder.process_result(result)
134            self.builder.out_queue.task_done()
135
136
137class BuilderThread(threading.Thread):
138    """This thread builds U-Boot for a particular board.
139
140    An input queue provides each new job. We run 'make' to build U-Boot
141    and then pass the results on to the output queue.
142
143    Members:
144        builder: The builder which contains information we might need
145        thread_num: Our thread number (0-n-1), used to decide on a
146            temporary directory. If this is -1 then there are no threads
147            and we are the (only) main process
148        mrproper: Use 'make mrproper' before each reconfigure
149        per_board_out_dir: True to build in a separate persistent directory per
150            board rather than a thread-specific directory
151        test_exception: Used for testing; True to raise an exception instead of
152            reporting the build result
153    """
154    def __init__(self, builder, thread_num, mrproper, per_board_out_dir,
155                 test_exception=False):
156        """Set up a new builder thread"""
157        threading.Thread.__init__(self)
158        self.builder = builder
159        self.thread_num = thread_num
160        self.mrproper = mrproper
161        self.per_board_out_dir = per_board_out_dir
162        self.test_exception = test_exception
163        self.toolchain = None
164
165    def make(self, commit, brd, stage, cwd, *args, **kwargs):
166        """Run 'make' on a particular commit and board.
167
168        The source code will already be checked out, so the 'commit'
169        argument is only for information.
170
171        Args:
172            commit (Commit): Commit that is being built
173            brd (Board): Board that is being built
174            stage (str): Stage of the build. Valid stages are:
175                        mrproper - can be called to clean source
176                        config - called to configure for a board
177                        build - the main make invocation - it does the build
178            cwd (str): Working directory to set, or None to leave it alone
179            *args (list of str): Arguments to pass to 'make'
180            **kwargs (dict): A list of keyword arguments to pass to
181                command.run_pipe()
182
183        Returns:
184            CommandResult object
185        """
186        return self.builder.do_make(commit, brd, stage, cwd, *args,
187                **kwargs)
188
189    def _build_args(self, brd, out_dir, out_rel_dir, work_dir, commit_upto):
190        """Set up arguments to the args list based on the settings
191
192        Args:
193            brd (Board): Board to create arguments for
194            out_dir (str): Path to output directory containing the files
195            out_rel_dir (str): Output directory relative to the current dir
196            work_dir (str): Directory to which the source will be checked out
197            commit_upto (int): Commit number to build (0...n-1)
198
199        Returns:
200            tuple:
201                list of str: Arguments to pass to make
202                str: Current working directory, or None if no commit
203                str: Source directory (typically the work directory)
204        """
205        args = []
206        cwd = work_dir
207        src_dir = os.path.realpath(work_dir)
208        if not self.builder.in_tree:
209            if commit_upto is None:
210                # In this case we are building in the original source directory
211                # (i.e. the current directory where buildman is invoked. The
212                # output directory is set to this thread's selected work
213                # directory.
214                #
215                # Symlinks can confuse U-Boot's Makefile since we may use '..'
216                # in our path, so remove them.
217                real_dir = os.path.realpath(out_dir)
218                args.append(f'O={real_dir}')
219                cwd = None
220                src_dir = os.getcwd()
221            else:
222                args.append(f'O={out_rel_dir}')
223        if self.builder.verbose_build:
224            args.append('V=1')
225        else:
226            args.append('-s')
227        if self.builder.num_jobs is not None:
228            args.extend(['-j', str(self.builder.num_jobs)])
229        if self.builder.warnings_as_errors:
230            args.append('KCFLAGS=-Werror')
231            args.append('HOSTCFLAGS=-Werror')
232        if self.builder.allow_missing:
233            args.append('BINMAN_ALLOW_MISSING=1')
234        if self.builder.no_lto:
235            args.append('NO_LTO=1')
236        if self.builder.reproducible_builds:
237            args.append('SOURCE_DATE_EPOCH=0')
238        args.extend(self.builder.toolchains.GetMakeArguments(brd))
239        args.extend(self.toolchain.MakeArgs())
240        return args, cwd, src_dir
241
242    def _reconfigure(self, commit, brd, cwd, args, env, config_args, config_out,
243                     cmd_list):
244        """Reconfigure the build
245
246        Args:
247            commit (Commit): Commit only being built
248            brd (Board): Board being built
249            cwd (str): Current working directory
250            args (list of str): Arguments to pass to make
251            env (dict): Environment strings
252            config_args (list of str): defconfig arg for this board
253            cmd_list (list of str): List to add the commands to, for logging
254
255        Returns:
256            CommandResult object
257        """
258        if self.mrproper:
259            result = self.make(commit, brd, 'mrproper', cwd, 'mrproper', *args,
260                               env=env)
261            config_out.write(result.combined)
262            cmd_list.append([self.builder.gnu_make, 'mrproper', *args])
263        result = self.make(commit, brd, 'config', cwd, *(args + config_args),
264                           env=env)
265        cmd_list.append([self.builder.gnu_make] + args + config_args)
266        config_out.write(result.combined)
267        return result
268
269    def _build(self, commit, brd, cwd, args, env, cmd_list, config_only):
270        """Perform the build
271
272        Args:
273            commit (Commit): Commit only being built
274            brd (Board): Board being built
275            cwd (str): Current working directory
276            args (list of str): Arguments to pass to make
277            env (dict): Environment strings
278            cmd_list (list of str): List to add the commands to, for logging
279            config_only (bool): True if this is a config-only build (using the
280                'make cfg' target)
281
282        Returns:
283            CommandResult object
284        """
285        if config_only:
286            args.append('cfg')
287        result = self.make(commit, brd, 'build', cwd, *args, env=env)
288        cmd_list.append([self.builder.gnu_make] + args)
289        if (result.return_code == 2 and
290            ('Some images are invalid' in result.stderr)):
291            # This is handled later by the check for output in stderr
292            result.return_code = 0
293        return result
294
295    def _read_done_file(self, commit_upto, brd, force_build,
296                        force_build_failures):
297        """Check the 'done' file and see if this commit should be built
298
299        Args:
300            commit (Commit): Commit only being built
301            brd (Board): Board being built
302            force_build (bool): Force a build even if one was previously done
303            force_build_failures (bool): Force a bulid if the previous result
304                showed failure
305
306        Returns:
307            tuple:
308                bool: True if build should be built
309                CommandResult: if there was a previous run:
310                    - already_done set to True
311                    - return_code set to return code
312                    - result.stderr set to 'bad' if stderr output was recorded
313        """
314        result = command.CommandResult()
315        done_file = self.builder.get_done_file(commit_upto, brd.target)
316        result.already_done = os.path.exists(done_file)
317        will_build = (force_build or force_build_failures or
318            not result.already_done)
319        if result.already_done:
320            with open(done_file, 'r', encoding='utf-8') as outf:
321                try:
322                    result.return_code = int(outf.readline())
323                except ValueError:
324                    # The file may be empty due to running out of disk space.
325                    # Try a rebuild
326                    result.return_code = RETURN_CODE_RETRY
327
328            # Check the signal that the build needs to be retried
329            if result.return_code == RETURN_CODE_RETRY:
330                will_build = True
331            elif will_build:
332                err_file = self.builder.get_err_file(commit_upto, brd.target)
333                if os.path.exists(err_file) and os.stat(err_file).st_size:
334                    result.stderr = 'bad'
335                elif not force_build:
336                    # The build passed, so no need to build it again
337                    will_build = False
338        return will_build, result
339
340    def _decide_dirs(self, brd, work_dir, work_in_output):
341        """Decide the output directory to use
342
343        Args:
344            work_dir (str): Directory to which the source will be checked out
345            work_in_output (bool): Use the output directory as the work
346                directory and don't write to a separate output directory.
347
348        Returns:
349            tuple:
350                out_dir (str): Output directory for the build
351                out_rel_dir (str): Output directory relatie to the current dir
352        """
353        if work_in_output or self.builder.in_tree:
354            out_rel_dir = None
355            out_dir = work_dir
356        else:
357            if self.per_board_out_dir:
358                out_rel_dir = os.path.join('..', brd.target)
359            else:
360                out_rel_dir = 'build'
361            out_dir = os.path.join(work_dir, out_rel_dir)
362        return out_dir, out_rel_dir
363
364    def _checkout(self, commit_upto, work_dir):
365        """Checkout the right commit
366
367        Args:
368            commit_upto (int): Commit number to build (0...n-1)
369            work_dir (str): Directory to which the source will be checked out
370
371        Returns:
372            Commit: Commit being built, or 'current' for current source
373        """
374        if self.builder.commits:
375            commit = self.builder.commits[commit_upto]
376            if self.builder.checkout:
377                git_dir = os.path.join(work_dir, '.git')
378                gitutil.checkout(commit.hash, git_dir, work_dir, force=True)
379        else:
380            commit = 'current'
381        return commit
382
383    def _config_and_build(self, commit_upto, brd, work_dir, do_config,
384                          config_only, adjust_cfg, commit, out_dir, out_rel_dir,
385                          result):
386        """Do the build, configuring first if necessary
387
388        Args:
389            commit_upto (int): Commit number to build (0...n-1)
390            brd (Board): Board to create arguments for
391            work_dir (str): Directory to which the source will be checked out
392            do_config (bool): True to run a make <board>_defconfig on the source
393            config_only (bool): Only configure the source, do not build it
394            adjust_cfg (list of str): See the cfgutil module and run_commit()
395            commit (Commit): Commit only being built
396            out_dir (str): Output directory for the build
397            out_rel_dir (str): Output directory relatie to the current dir
398            result (CommandResult): Previous result
399
400        Returns:
401            tuple:
402                result (CommandResult): Result of the build
403                do_config (bool): indicates whether 'make config' is needed on
404                    the next incremental build
405        """
406        # Set up the environment and command line
407        env = self.toolchain.MakeEnvironment(self.builder.full_path)
408        mkdir(out_dir)
409
410        args, cwd, src_dir = self._build_args(brd, out_dir, out_rel_dir,
411                                              work_dir, commit_upto)
412        config_args = [f'{brd.target}_defconfig']
413        config_out = io.StringIO()
414
415        _remove_old_outputs(out_dir)
416
417        # If we need to reconfigure, do that now
418        cfg_file = os.path.join(out_dir, '.config')
419        cmd_list = []
420        if do_config or adjust_cfg:
421            result = self._reconfigure(
422                commit, brd, cwd, args, env, config_args, config_out, cmd_list)
423            do_config = False   # No need to configure next time
424            if adjust_cfg:
425                cfgutil.adjust_cfg_file(cfg_file, adjust_cfg)
426
427        # Now do the build, if everything looks OK
428        if result.return_code == 0:
429            if adjust_cfg:
430                oldc_args = list(args) + ['oldconfig']
431                oldc_result = self.make(commit, brd, 'oldconfig', cwd,
432                                        *oldc_args, env=env)
433                if oldc_result.return_code:
434                    return oldc_result
435            result = self._build(commit, brd, cwd, args, env, cmd_list,
436                                 config_only)
437            if adjust_cfg:
438                errs = cfgutil.check_cfg_file(cfg_file, adjust_cfg)
439                if errs:
440                    result.stderr += errs
441                    result.return_code = 1
442        result.stderr = result.stderr.replace(src_dir + '/', '')
443        if self.builder.verbose_build:
444            result.stdout = config_out.getvalue() + result.stdout
445        result.cmd_list = cmd_list
446        return result, do_config
447
448    def run_commit(self, commit_upto, brd, work_dir, do_config, config_only,
449                  force_build, force_build_failures, work_in_output,
450                  adjust_cfg):
451        """Build a particular commit.
452
453        If the build is already done, and we are not forcing a build, we skip
454        the build and just return the previously-saved results.
455
456        Args:
457            commit_upto (int): Commit number to build (0...n-1)
458            brd (Board): Board to build
459            work_dir (str): Directory to which the source will be checked out
460            do_config (bool): True to run a make <board>_defconfig on the source
461            config_only (bool): Only configure the source, do not build it
462            force_build (bool): Force a build even if one was previously done
463            force_build_failures (bool): Force a bulid if the previous result
464                showed failure
465            work_in_output (bool) : Use the output directory as the work
466                directory and don't write to a separate output directory.
467            adjust_cfg (list of str): List of changes to make to .config file
468                before building. Each is one of (where C is either CONFIG_xxx
469                or just xxx):
470                     C to enable C
471                     ~C to disable C
472                     C=val to set the value of C (val must have quotes if C is
473                         a string Kconfig
474
475        Returns:
476            tuple containing:
477                - CommandResult object containing the results of the build
478                - boolean indicating whether 'make config' is still needed
479        """
480        # Create a default result - it will be overwritte by the call to
481        # self.make() below, in the event that we do a build.
482        out_dir, out_rel_dir = self._decide_dirs(brd, work_dir, work_in_output)
483
484        # Check if the job was already completed last time
485        will_build, result = self._read_done_file(commit_upto, brd, force_build,
486                                                  force_build_failures)
487
488        if will_build:
489            # We are going to have to build it. First, get a toolchain
490            if not self.toolchain:
491                try:
492                    self.toolchain = self.builder.toolchains.Select(brd.arch)
493                except ValueError as err:
494                    result.return_code = 10
495                    result.stdout = ''
496                    result.stderr = f'Tool chain error for {brd.arch}: {str(err)}'
497
498            if self.toolchain:
499                commit = self._checkout(commit_upto, work_dir)
500                result, do_config = self._config_and_build(
501                    commit_upto, brd, work_dir, do_config, config_only,
502                    adjust_cfg, commit, out_dir, out_rel_dir, result)
503            result.already_done = False
504
505        result.toolchain = self.toolchain
506        result.brd = brd
507        result.commit_upto = commit_upto
508        result.out_dir = out_dir
509        return result, do_config
510
511    def _write_result(self, result, keep_outputs, work_in_output):
512        """Write a built result to the output directory.
513
514        Args:
515            result (CommandResult): result to write
516            keep_outputs (bool): True to store the output binaries, False
517                to delete them
518            work_in_output (bool): Use the output directory as the work
519                directory and don't write to a separate output directory.
520        """
521        # If we think this might have been aborted with Ctrl-C, record the
522        # failure but not that we are 'done' with this board. A retry may fix
523        # it.
524        maybe_aborted = result.stderr and 'No child processes' in result.stderr
525
526        if result.return_code >= 0 and result.already_done:
527            return
528
529        # Write the output and stderr
530        output_dir = self.builder.get_output_dir(result.commit_upto)
531        mkdir(output_dir)
532        build_dir = self.builder.get_build_dir(result.commit_upto,
533                result.brd.target)
534        mkdir(build_dir)
535
536        outfile = os.path.join(build_dir, 'log')
537        with open(outfile, 'w', encoding='utf-8') as outf:
538            if result.stdout:
539                outf.write(result.stdout)
540
541        errfile = self.builder.get_err_file(result.commit_upto,
542                result.brd.target)
543        if result.stderr:
544            with open(errfile, 'w', encoding='utf-8') as outf:
545                outf.write(result.stderr)
546        elif os.path.exists(errfile):
547            os.remove(errfile)
548
549        # Fatal error
550        if result.return_code < 0:
551            return
552
553        if result.toolchain:
554            # Write the build result and toolchain information.
555            done_file = self.builder.get_done_file(result.commit_upto,
556                    result.brd.target)
557            with open(done_file, 'w', encoding='utf-8') as outf:
558                if maybe_aborted:
559                    # Special code to indicate we need to retry
560                    outf.write(f'{RETURN_CODE_RETRY}')
561                else:
562                    outf.write(f'{result.return_code}')
563            with open(os.path.join(build_dir, 'toolchain'), 'w',
564                      encoding='utf-8') as outf:
565                print('gcc', result.toolchain.gcc, file=outf)
566                print('path', result.toolchain.path, file=outf)
567                print('cross', result.toolchain.cross, file=outf)
568                print('arch', result.toolchain.arch, file=outf)
569                outf.write(f'{result.return_code}')
570
571            # Write out the image and function size information and an objdump
572            env = result.toolchain.MakeEnvironment(self.builder.full_path)
573            with open(os.path.join(build_dir, 'out-env'), 'wb') as outf:
574                for var in sorted(env.keys()):
575                    outf.write(b'%s="%s"' % (var, env[var]))
576
577            with open(os.path.join(build_dir, 'out-cmd'), 'w',
578                      encoding='utf-8') as outf:
579                for cmd in result.cmd_list:
580                    print(' '.join(cmd), file=outf)
581
582            lines = []
583            for fname in BASE_ELF_FILENAMES:
584                cmd = [f'{self.toolchain.cross}nm', '--size-sort', fname]
585                nm_result = command.run_pipe([cmd], capture=True,
586                        capture_stderr=True, cwd=result.out_dir,
587                        raise_on_error=False, env=env)
588                if nm_result.stdout:
589                    nm_fname = self.builder.get_func_sizes_file(
590                        result.commit_upto, result.brd.target, fname)
591                    with open(nm_fname, 'w', encoding='utf-8') as outf:
592                        print(nm_result.stdout, end=' ', file=outf)
593
594                cmd = [f'{self.toolchain.cross}objdump', '-h', fname]
595                dump_result = command.run_pipe([cmd], capture=True,
596                        capture_stderr=True, cwd=result.out_dir,
597                        raise_on_error=False, env=env)
598                rodata_size = ''
599                if dump_result.stdout:
600                    objdump = self.builder.get_objdump_file(result.commit_upto,
601                                    result.brd.target, fname)
602                    with open(objdump, 'w', encoding='utf-8') as outf:
603                        print(dump_result.stdout, end=' ', file=outf)
604                    for line in dump_result.stdout.splitlines():
605                        fields = line.split()
606                        if len(fields) > 5 and fields[1] == '.rodata':
607                            rodata_size = fields[2]
608
609                cmd = [f'{self.toolchain.cross}size', fname]
610                size_result = command.run_pipe([cmd], capture=True,
611                        capture_stderr=True, cwd=result.out_dir,
612                        raise_on_error=False, env=env)
613                if size_result.stdout:
614                    lines.append(size_result.stdout.splitlines()[1] + ' ' +
615                                 rodata_size)
616
617            # Extract the environment from U-Boot and dump it out
618            cmd = [f'{self.toolchain.cross}objcopy', '-O', 'binary',
619                   '-j', '.rodata.default_environment',
620                   'env/built-in.o', 'uboot.env']
621            command.run_pipe([cmd], capture=True,
622                            capture_stderr=True, cwd=result.out_dir,
623                            raise_on_error=False, env=env)
624            if not work_in_output:
625                copy_files(result.out_dir, build_dir, '', ['uboot.env'])
626
627            # Write out the image sizes file. This is similar to the output
628            # of binutil's 'size' utility, but it omits the header line and
629            # adds an additional hex value at the end of each line for the
630            # rodata size
631            if lines:
632                sizes = self.builder.get_sizes_file(result.commit_upto,
633                                result.brd.target)
634                with open(sizes, 'w', encoding='utf-8') as outf:
635                    print('\n'.join(lines), file=outf)
636
637        if not work_in_output:
638            # Write out the configuration files, with a special case for SPL
639            for dirname in ['', 'spl', 'tpl']:
640                copy_files(
641                    result.out_dir, build_dir, dirname,
642                    ['u-boot.cfg', 'spl/u-boot-spl.cfg', 'tpl/u-boot-tpl.cfg',
643                     '.config', 'include/autoconf.mk',
644                     'include/generated/autoconf.h'])
645
646            # Now write the actual build output
647            if keep_outputs:
648                to_copy = ['u-boot*', '*.map', 'MLO', 'SPL',
649                           'include/autoconf.mk', 'spl/u-boot-spl*',
650                           'tpl/u-boot-tpl*', 'vpl/u-boot-vpl*']
651                to_copy += [f'*{ext}' for ext in COMMON_EXTS]
652                copy_files(result.out_dir, build_dir, '', to_copy)
653
654    def _send_result(self, result):
655        """Send a result to the builder for processing
656
657        Args:
658            result (CommandResult): results of the build
659
660        Raises:
661            ValueError: self.test_exception is true (for testing)
662        """
663        if self.test_exception:
664            raise ValueError('test exception')
665        if self.thread_num != -1:
666            self.builder.out_queue.put(result)
667        else:
668            self.builder.process_result(result)
669
670    def run_job(self, job):
671        """Run a single job
672
673        A job consists of a building a list of commits for a particular board.
674
675        Args:
676            job (Job): Job to build
677
678        Raises:
679            ValueError: Thread was interrupted
680        """
681        brd = job.brd
682        work_dir = self.builder.get_thread_dir(self.thread_num)
683        self.toolchain = None
684        if job.commits:
685            # Run 'make board_defconfig' on the first commit
686            do_config = True
687            commit_upto  = 0
688            force_build = False
689            for commit_upto in range(0, len(job.commits), job.step):
690                result, request_config = self.run_commit(commit_upto, brd,
691                        work_dir, do_config, self.builder.config_only,
692                        force_build or self.builder.force_build,
693                        self.builder.force_build_failures,
694                        job.work_in_output, job.adjust_cfg)
695                failed = result.return_code or result.stderr
696                did_config = do_config
697                if failed and not do_config:
698                    # If our incremental build failed, try building again
699                    # with a reconfig.
700                    if self.builder.force_config_on_failure:
701                        result, request_config = self.run_commit(commit_upto,
702                            brd, work_dir, True, False, True, False,
703                            job.work_in_output, job.adjust_cfg)
704                        did_config = True
705                if not self.builder.force_reconfig:
706                    do_config = request_config
707
708                # If we built that commit, then config is done. But if we got
709                # an warning, reconfig next time to force it to build the same
710                # files that created warnings this time. Otherwise an
711                # incremental build may not build the same file, and we will
712                # think that the warning has gone away.
713                # We could avoid this by using -Werror everywhere...
714                # For errors, the problem doesn't happen, since presumably
715                # the build stopped and didn't generate output, so will retry
716                # that file next time. So we could detect warnings and deal
717                # with them specially here. For now, we just reconfigure if
718                # anything goes work.
719                # Of course this is substantially slower if there are build
720                # errors/warnings (e.g. 2-3x slower even if only 10% of builds
721                # have problems).
722                if (failed and not result.already_done and not did_config and
723                        self.builder.force_config_on_failure):
724                    # If this build failed, try the next one with a
725                    # reconfigure.
726                    # Sometimes if the board_config.h file changes it can mess
727                    # with dependencies, and we get:
728                    # make: *** No rule to make target `include/autoconf.mk',
729                    #     needed by `depend'.
730                    do_config = True
731                    force_build = True
732                else:
733                    force_build = False
734                    if self.builder.force_config_on_failure:
735                        if failed:
736                            do_config = True
737                    result.commit_upto = commit_upto
738                    if result.return_code < 0:
739                        raise ValueError('Interrupt')
740
741                # We have the build results, so output the result
742                self._write_result(result, job.keep_outputs, job.work_in_output)
743                self._send_result(result)
744        else:
745            # Just build the currently checked-out build
746            result, request_config = self.run_commit(None, brd, work_dir, True,
747                        self.builder.config_only, True,
748                        self.builder.force_build_failures, job.work_in_output,
749                        job.adjust_cfg)
750            result.commit_upto = 0
751            self._write_result(result, job.keep_outputs, job.work_in_output)
752            self._send_result(result)
753
754    def run(self):
755        """Our thread's run function
756
757        This thread picks a job from the queue, runs it, and then goes to the
758        next job.
759        """
760        while True:
761            job = self.builder.queue.get()
762            try:
763                self.run_job(job)
764            except Exception as exc:
765                print('Thread exception (use -T0 to run without threads):',
766                      exc)
767                self.builder.thread_exceptions.append(exc)
768            self.builder.queue.task_done()
769