builderthread.py 22.0 KB
Newer Older
1
# SPDX-License-Identifier: GPL-2.0+
2 3 4 5 6 7 8
# Copyright (c) 2014 Google, Inc
#

import errno
import glob
import os
import shutil
9
import sys
10 11
import threading

S
Simon Glass 已提交
12 13
from patman import command
from patman import gitutil
14

15 16
RETURN_CODE_RETRY = -1

17
def Mkdir(dirname, parents = False):
18 19 20 21 22 23
    """Make a directory if it doesn't already exist.

    Args:
        dirname: Directory to create
    """
    try:
24 25 26 27
        if parents:
            os.makedirs(dirname)
        else:
            os.mkdir(dirname)
28 29
    except OSError as err:
        if err.errno == errno.EEXIST:
30
            if os.path.realpath('.') == os.path.realpath(dirname):
S
Simon Glass 已提交
31
                print("Cannot create the current working directory '%s'!" % dirname)
32
                sys.exit(1)
33 34 35 36 37 38 39 40 41
            pass
        else:
            raise

class BuilderJob:
    """Holds information about a job to be performed by a thread

    Members:
        board: Board object to build
42 43 44
        commits: List of Commit objects to build
        keep_outputs: True to save build output files
        step: 1 to process every commit, n to process every nth commit
45 46
        work_in_output: Use the output directory as the work directory and
            don't write to a separate output directory.
47 48 49 50
    """
    def __init__(self):
        self.board = None
        self.commits = []
51 52
        self.keep_outputs = False
        self.step = 1
53
        self.work_in_output = False
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92


class ResultThread(threading.Thread):
    """This thread processes results from builder threads.

    It simply passes the results on to the builder. There is only one
    result thread, and this helps to serialise the build output.
    """
    def __init__(self, builder):
        """Set up a new result thread

        Args:
            builder: Builder which will be sent each result
        """
        threading.Thread.__init__(self)
        self.builder = builder

    def run(self):
        """Called to start up the result thread.

        We collect the next result job and pass it on to the build.
        """
        while True:
            result = self.builder.out_queue.get()
            self.builder.ProcessResult(result)
            self.builder.out_queue.task_done()


class BuilderThread(threading.Thread):
    """This thread builds U-Boot for a particular board.

    An input queue provides each new job. We run 'make' to build U-Boot
    and then pass the results on to the output queue.

    Members:
        builder: The builder which contains information we might need
        thread_num: Our thread number (0-n-1), used to decide on a
                temporary directory
    """
S
Simon Glass 已提交
93
    def __init__(self, builder, thread_num, mrproper, per_board_out_dir):
94 95 96 97
        """Set up a new builder thread"""
        threading.Thread.__init__(self)
        self.builder = builder
        self.thread_num = thread_num
S
Simon Glass 已提交
98
        self.mrproper = mrproper
99
        self.per_board_out_dir = per_board_out_dir
100 101 102 103 104 105 106 107 108 109 110

    def Make(self, commit, brd, stage, cwd, *args, **kwargs):
        """Run 'make' on a particular commit and board.

        The source code will already be checked out, so the 'commit'
        argument is only for information.

        Args:
            commit: Commit object that is being built
            brd: Board object that is being built
            stage: Stage of the build. Valid stages are:
111
                        mrproper - can be called to clean source
112 113 114 115 116 117 118 119 120 121 122
                        config - called to configure for a board
                        build - the main make invocation - it does the build
            args: A list of arguments to pass to 'make'
            kwargs: A list of keyword arguments to pass to command.RunPipe()

        Returns:
            CommandResult object
        """
        return self.builder.do_make(commit, brd, stage, cwd, *args,
                **kwargs)

123
    def RunCommit(self, commit_upto, brd, work_dir, do_config, config_only,
124
                  force_build, force_build_failures, work_in_output):
125 126 127 128 129 130 131 132 133 134
        """Build a particular commit.

        If the build is already done, and we are not forcing a build, we skip
        the build and just return the previously-saved results.

        Args:
            commit_upto: Commit number to build (0...n-1)
            brd: Board object to build
            work_dir: Directory to which the source will be checked out
            do_config: True to run a make <board>_defconfig on the source
135
            config_only: Only configure the source, do not build it
136 137 138
            force_build: Force a build even if one was previously done
            force_build_failures: Force a bulid if the previous result showed
                failure
139 140
            work_in_output: Use the output directory as the work directory and
                don't write to a separate output directory.
141 142 143 144 145 146 147 148 149 150

        Returns:
            tuple containing:
                - CommandResult object containing the results of the build
                - boolean indicating whether 'make config' is still needed
        """
        # Create a default result - it will be overwritte by the call to
        # self.Make() below, in the event that we do a build.
        result = command.CommandResult()
        result.return_code = 0
151
        if work_in_output or self.builder.in_tree:
152 153
            out_dir = work_dir
        else:
154 155 156 157 158
            if self.per_board_out_dir:
                out_rel_dir = os.path.join('..', brd.target)
            else:
                out_rel_dir = 'build'
            out_dir = os.path.join(work_dir, out_rel_dir)
159 160 161 162 163 164

        # Check if the job was already completed last time
        done_file = self.builder.GetDoneFile(commit_upto, brd.target)
        result.already_done = os.path.exists(done_file)
        will_build = (force_build or force_build_failures or
            not result.already_done)
165
        if result.already_done:
166 167
            # Get the return code from that build and use it
            with open(done_file, 'r') as fd:
168 169 170 171 172 173
                try:
                    result.return_code = int(fd.readline())
                except ValueError:
                    # The file may be empty due to running out of disk space.
                    # Try a rebuild
                    result.return_code = RETURN_CODE_RETRY
174 175 176 177 178

            # Check the signal that the build needs to be retried
            if result.return_code == RETURN_CODE_RETRY:
                will_build = True
            elif will_build:
179 180 181 182 183 184
                err_file = self.builder.GetErrFile(commit_upto, brd.target)
                if os.path.exists(err_file) and os.stat(err_file).st_size:
                    result.stderr = 'bad'
                elif not force_build:
                    # The build passed, so no need to build it again
                    will_build = False
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209

        if will_build:
            # We are going to have to build it. First, get a toolchain
            if not self.toolchain:
                try:
                    self.toolchain = self.builder.toolchains.Select(brd.arch)
                except ValueError as err:
                    result.return_code = 10
                    result.stdout = ''
                    result.stderr = str(err)
                    # TODO(sjg@chromium.org): This gets swallowed, but needs
                    # to be reported.

            if self.toolchain:
                # Checkout the right commit
                if self.builder.commits:
                    commit = self.builder.commits[commit_upto]
                    if self.builder.checkout:
                        git_dir = os.path.join(work_dir, '.git')
                        gitutil.Checkout(commit.hash, git_dir, work_dir,
                                         force=True)
                else:
                    commit = 'current'

                # Set up the environment and command line
210
                env = self.toolchain.MakeEnvironment(self.builder.full_path)
211 212 213
                Mkdir(out_dir)
                args = []
                cwd = work_dir
214
                src_dir = os.path.realpath(work_dir)
215 216 217 218 219 220 221 222 223
                if not self.builder.in_tree:
                    if commit_upto is None:
                        # In this case we are building in the original source
                        # directory (i.e. the current directory where buildman
                        # is invoked. The output directory is set to this
                        # thread's selected work directory.
                        #
                        # Symlinks can confuse U-Boot's Makefile since
                        # we may use '..' in our path, so remove them.
224 225
                        out_dir = os.path.realpath(out_dir)
                        args.append('O=%s' % out_dir)
226
                        cwd = None
227
                        src_dir = os.getcwd()
228
                    else:
229
                        args.append('O=%s' % out_rel_dir)
230 231 232
                if self.builder.verbose_build:
                    args.append('V=1')
                else:
233
                    args.append('-s')
234 235
                if self.builder.num_jobs is not None:
                    args.extend(['-j', str(self.builder.num_jobs)])
236 237
                if self.builder.warnings_as_errors:
                    args.append('KCFLAGS=-Werror')
238 239 240
                config_args = ['%s_defconfig' % brd.target]
                config_out = ''
                args.extend(self.builder.toolchains.GetMakeArguments(brd))
241
                args.extend(self.toolchain.MakeArgs())
242 243 244

                # If we need to reconfigure, do that now
                if do_config:
245
                    config_out = ''
S
Simon Glass 已提交
246
                    if self.mrproper:
247 248 249
                        result = self.Make(commit, brd, 'mrproper', cwd,
                                'mrproper', *args, env=env)
                        config_out += result.combined
250 251
                    result = self.Make(commit, brd, 'config', cwd,
                            *(args + config_args), env=env)
252
                    config_out += result.combined
253 254
                    do_config = False   # No need to configure next time
                if result.return_code == 0:
255
                    if config_only:
256
                        args.append('cfg')
257 258
                    result = self.Make(commit, brd, 'build', cwd, *args,
                            env=env)
259
                result.stderr = result.stderr.replace(src_dir + '/', '')
260 261
                if self.builder.verbose_build:
                    result.stdout = config_out + result.stdout
262 263 264 265 266 267 268 269 270 271 272
            else:
                result.return_code = 1
                result.stderr = 'No tool chain for %s\n' % brd.arch
            result.already_done = False

        result.toolchain = self.toolchain
        result.brd = brd
        result.commit_upto = commit_upto
        result.out_dir = out_dir
        return result, do_config

273
    def _WriteResult(self, result, keep_outputs, work_in_output):
274 275 276 277 278 279
        """Write a built result to the output directory.

        Args:
            result: CommandResult object containing result to write
            keep_outputs: True to store the output binaries, False
                to delete them
280 281
            work_in_output: Use the output directory as the work directory and
                don't write to a separate output directory.
282 283 284 285 286
        """
        # Fatal error
        if result.return_code < 0:
            return

287 288 289 290
        # If we think this might have been aborted with Ctrl-C, record the
        # failure but not that we are 'done' with this board. A retry may fix
        # it.
        maybe_aborted =  result.stderr and 'No child processes' in result.stderr
291 292 293 294 295 296 297 298 299 300 301 302 303 304

        if result.already_done:
            return

        # Write the output and stderr
        output_dir = self.builder._GetOutputDir(result.commit_upto)
        Mkdir(output_dir)
        build_dir = self.builder.GetBuildDir(result.commit_upto,
                result.brd.target)
        Mkdir(build_dir)

        outfile = os.path.join(build_dir, 'log')
        with open(outfile, 'w') as fd:
            if result.stdout:
S
Simon Glass 已提交
305
                fd.write(result.stdout)
306 307 308 309 310

        errfile = self.builder.GetErrFile(result.commit_upto,
                result.brd.target)
        if result.stderr:
            with open(errfile, 'w') as fd:
S
Simon Glass 已提交
311
                fd.write(result.stderr)
312 313 314 315 316 317 318 319
        elif os.path.exists(errfile):
            os.remove(errfile)

        if result.toolchain:
            # Write the build result and toolchain information.
            done_file = self.builder.GetDoneFile(result.commit_upto,
                    result.brd.target)
            with open(done_file, 'w') as fd:
320 321 322 323 324
                if maybe_aborted:
                    # Special code to indicate we need to retry
                    fd.write('%s' % RETURN_CODE_RETRY)
                else:
                    fd.write('%s' % result.return_code)
325
            with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
S
Simon Glass 已提交
326 327 328 329
                print('gcc', result.toolchain.gcc, file=fd)
                print('path', result.toolchain.path, file=fd)
                print('cross', result.toolchain.cross, file=fd)
                print('arch', result.toolchain.arch, file=fd)
330 331 332
                fd.write('%s' % result.return_code)

            # Write out the image and function size information and an objdump
333
            env = result.toolchain.MakeEnvironment(self.builder.full_path)
334
            with open(os.path.join(build_dir, 'out-env'), 'w') as fd:
335
                for var in sorted(env.keys()):
S
Simon Glass 已提交
336
                    print('%s="%s"' % (var, env[var]), file=fd)
337 338 339 340 341 342 343 344 345 346
            lines = []
            for fname in ['u-boot', 'spl/u-boot-spl']:
                cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
                nm_result = command.RunPipe([cmd], capture=True,
                        capture_stderr=True, cwd=result.out_dir,
                        raise_on_error=False, env=env)
                if nm_result.stdout:
                    nm = self.builder.GetFuncSizesFile(result.commit_upto,
                                    result.brd.target, fname)
                    with open(nm, 'w') as fd:
S
Simon Glass 已提交
347
                        print(nm_result.stdout, end=' ', file=fd)
348 349 350 351 352 353 354 355 356 357

                cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
                dump_result = command.RunPipe([cmd], capture=True,
                        capture_stderr=True, cwd=result.out_dir,
                        raise_on_error=False, env=env)
                rodata_size = ''
                if dump_result.stdout:
                    objdump = self.builder.GetObjdumpFile(result.commit_upto,
                                    result.brd.target, fname)
                    with open(objdump, 'w') as fd:
S
Simon Glass 已提交
358
                        print(dump_result.stdout, end=' ', file=fd)
359 360 361 362 363 364 365 366 367 368 369 370 371
                    for line in dump_result.stdout.splitlines():
                        fields = line.split()
                        if len(fields) > 5 and fields[1] == '.rodata':
                            rodata_size = fields[2]

                cmd = ['%ssize' % self.toolchain.cross, fname]
                size_result = command.RunPipe([cmd], capture=True,
                        capture_stderr=True, cwd=result.out_dir,
                        raise_on_error=False, env=env)
                if size_result.stdout:
                    lines.append(size_result.stdout.splitlines()[1] + ' ' +
                                 rodata_size)

372 373 374 375 376 377 378 379
            # Extract the environment from U-Boot and dump it out
            cmd = ['%sobjcopy' % self.toolchain.cross, '-O', 'binary',
                   '-j', '.rodata.default_environment',
                   'env/built-in.o', 'uboot.env']
            command.RunPipe([cmd], capture=True,
                            capture_stderr=True, cwd=result.out_dir,
                            raise_on_error=False, env=env)
            ubootenv = os.path.join(result.out_dir, 'uboot.env')
380 381
            if not work_in_output:
                self.CopyFiles(result.out_dir, build_dir, '', ['uboot.env'])
382

383 384 385 386 387 388 389 390
            # Write out the image sizes file. This is similar to the output
            # of binutil's 'size' utility, but it omits the header line and
            # adds an additional hex value at the end of each line for the
            # rodata size
            if len(lines):
                sizes = self.builder.GetSizesFile(result.commit_upto,
                                result.brd.target)
                with open(sizes, 'w') as fd:
S
Simon Glass 已提交
391
                    print('\n'.join(lines), file=fd)
392

393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
        if not work_in_output:
            # Write out the configuration files, with a special case for SPL
            for dirname in ['', 'spl', 'tpl']:
                self.CopyFiles(
                    result.out_dir, build_dir, dirname,
                    ['u-boot.cfg', 'spl/u-boot-spl.cfg', 'tpl/u-boot-tpl.cfg',
                     '.config', 'include/autoconf.mk',
                     'include/generated/autoconf.h'])

            # Now write the actual build output
            if keep_outputs:
                self.CopyFiles(
                    result.out_dir, build_dir, '',
                    ['u-boot*', '*.bin', '*.map', '*.img', 'MLO', 'SPL',
                     'include/autoconf.mk', 'spl/u-boot-spl*'])
S
Simon Glass 已提交
408 409 410

    def CopyFiles(self, out_dir, build_dir, dirname, patterns):
        """Copy files from the build directory to the output.
411

S
Simon Glass 已提交
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
        Args:
            out_dir: Path to output directory containing the files
            build_dir: Place to copy the files
            dirname: Source directory, '' for normal U-Boot, 'spl' for SPL
            patterns: A list of filenames (strings) to copy, each relative
               to the build directory
        """
        for pattern in patterns:
            file_list = glob.glob(os.path.join(out_dir, dirname, pattern))
            for fname in file_list:
                target = os.path.basename(fname)
                if dirname:
                    base, ext = os.path.splitext(target)
                    if ext:
                        target = '%s-%s%s' % (base, dirname, ext)
                shutil.copy(fname, os.path.join(build_dir, target))
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446

    def RunJob(self, job):
        """Run a single job

        A job consists of a building a list of commits for a particular board.

        Args:
            job: Job to build
        """
        brd = job.board
        work_dir = self.builder.GetThreadDir(self.thread_num)
        self.toolchain = None
        if job.commits:
            # Run 'make board_defconfig' on the first commit
            do_config = True
            commit_upto  = 0
            force_build = False
            for commit_upto in range(0, len(job.commits), job.step):
                result, request_config = self.RunCommit(commit_upto, brd,
447
                        work_dir, do_config, self.builder.config_only,
448
                        force_build or self.builder.force_build,
449 450
                        self.builder.force_build_failures,
                        work_in_output=job.work_in_output)
451 452 453 454 455 456 457
                failed = result.return_code or result.stderr
                did_config = do_config
                if failed and not do_config:
                    # If our incremental build failed, try building again
                    # with a reconfig.
                    if self.builder.force_config_on_failure:
                        result, request_config = self.RunCommit(commit_upto,
458 459
                            brd, work_dir, True, False, True, False,
                            work_in_output=job.work_in_output)
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
                        did_config = True
                if not self.builder.force_reconfig:
                    do_config = request_config

                # If we built that commit, then config is done. But if we got
                # an warning, reconfig next time to force it to build the same
                # files that created warnings this time. Otherwise an
                # incremental build may not build the same file, and we will
                # think that the warning has gone away.
                # We could avoid this by using -Werror everywhere...
                # For errors, the problem doesn't happen, since presumably
                # the build stopped and didn't generate output, so will retry
                # that file next time. So we could detect warnings and deal
                # with them specially here. For now, we just reconfigure if
                # anything goes work.
                # Of course this is substantially slower if there are build
                # errors/warnings (e.g. 2-3x slower even if only 10% of builds
                # have problems).
                if (failed and not result.already_done and not did_config and
                        self.builder.force_config_on_failure):
                    # If this build failed, try the next one with a
                    # reconfigure.
                    # Sometimes if the board_config.h file changes it can mess
                    # with dependencies, and we get:
                    # make: *** No rule to make target `include/autoconf.mk',
                    #     needed by `depend'.
                    do_config = True
                    force_build = True
                else:
                    force_build = False
                    if self.builder.force_config_on_failure:
                        if failed:
                            do_config = True
                    result.commit_upto = commit_upto
                    if result.return_code < 0:
                        raise ValueError('Interrupt')

                # We have the build results, so output the result
498
                self._WriteResult(result, job.keep_outputs, job.work_in_output)
499 500 501 502
                self.builder.out_queue.put(result)
        else:
            # Just build the currently checked-out build
            result, request_config = self.RunCommit(None, brd, work_dir, True,
503
                        self.builder.config_only, True,
504 505
                        self.builder.force_build_failures,
                        work_in_output=job.work_in_output)
506
            result.commit_upto = 0
507
            self._WriteResult(result, job.keep_outputs, job.work_in_output)
508 509 510 511 512 513 514 515 516 517
            self.builder.out_queue.put(result)

    def run(self):
        """Our thread's run function

        This thread picks a job from the queue, runs it, and then goes to the
        next job.
        """
        while True:
            job = self.builder.queue.get()
518
            self.RunJob(job)
519
            self.builder.queue.task_done()