xref: /rk3399_rockchip-uboot/tools/buildman/builderthread.py (revision 40f11fce7c686f2b51ae109c4085fb5988c5631c)
1# Copyright (c) 2014 Google, Inc
2#
3# SPDX-License-Identifier:      GPL-2.0+
4#
5
6import errno
7import glob
8import os
9import shutil
10import threading
11
12import command
13import gitutil
14
15def Mkdir(dirname, parents = False):
16    """Make a directory if it doesn't already exist.
17
18    Args:
19        dirname: Directory to create
20    """
21    try:
22        if parents:
23            os.makedirs(dirname)
24        else:
25            os.mkdir(dirname)
26    except OSError as err:
27        if err.errno == errno.EEXIST:
28            pass
29        else:
30            raise
31
32class BuilderJob:
33    """Holds information about a job to be performed by a thread
34
35    Members:
36        board: Board object to build
37        commits: List of commit options to build.
38    """
39    def __init__(self):
40        self.board = None
41        self.commits = []
42
43
44class ResultThread(threading.Thread):
45    """This thread processes results from builder threads.
46
47    It simply passes the results on to the builder. There is only one
48    result thread, and this helps to serialise the build output.
49    """
50    def __init__(self, builder):
51        """Set up a new result thread
52
53        Args:
54            builder: Builder which will be sent each result
55        """
56        threading.Thread.__init__(self)
57        self.builder = builder
58
59    def run(self):
60        """Called to start up the result thread.
61
62        We collect the next result job and pass it on to the build.
63        """
64        while True:
65            result = self.builder.out_queue.get()
66            self.builder.ProcessResult(result)
67            self.builder.out_queue.task_done()
68
69
70class BuilderThread(threading.Thread):
71    """This thread builds U-Boot for a particular board.
72
73    An input queue provides each new job. We run 'make' to build U-Boot
74    and then pass the results on to the output queue.
75
76    Members:
77        builder: The builder which contains information we might need
78        thread_num: Our thread number (0-n-1), used to decide on a
79                temporary directory
80    """
81    def __init__(self, builder, thread_num):
82        """Set up a new builder thread"""
83        threading.Thread.__init__(self)
84        self.builder = builder
85        self.thread_num = thread_num
86
87    def Make(self, commit, brd, stage, cwd, *args, **kwargs):
88        """Run 'make' on a particular commit and board.
89
90        The source code will already be checked out, so the 'commit'
91        argument is only for information.
92
93        Args:
94            commit: Commit object that is being built
95            brd: Board object that is being built
96            stage: Stage of the build. Valid stages are:
97                        mrproper - can be called to clean source
98                        config - called to configure for a board
99                        build - the main make invocation - it does the build
100            args: A list of arguments to pass to 'make'
101            kwargs: A list of keyword arguments to pass to command.RunPipe()
102
103        Returns:
104            CommandResult object
105        """
106        return self.builder.do_make(commit, brd, stage, cwd, *args,
107                **kwargs)
108
109    def RunCommit(self, commit_upto, brd, work_dir, do_config, force_build,
110                  force_build_failures):
111        """Build a particular commit.
112
113        If the build is already done, and we are not forcing a build, we skip
114        the build and just return the previously-saved results.
115
116        Args:
117            commit_upto: Commit number to build (0...n-1)
118            brd: Board object to build
119            work_dir: Directory to which the source will be checked out
120            do_config: True to run a make <board>_defconfig on the source
121            force_build: Force a build even if one was previously done
122            force_build_failures: Force a bulid if the previous result showed
123                failure
124
125        Returns:
126            tuple containing:
127                - CommandResult object containing the results of the build
128                - boolean indicating whether 'make config' is still needed
129        """
130        # Create a default result - it will be overwritte by the call to
131        # self.Make() below, in the event that we do a build.
132        result = command.CommandResult()
133        result.return_code = 0
134        if self.builder.in_tree:
135            out_dir = work_dir
136        else:
137            out_dir = os.path.join(work_dir, 'build')
138
139        # Check if the job was already completed last time
140        done_file = self.builder.GetDoneFile(commit_upto, brd.target)
141        result.already_done = os.path.exists(done_file)
142        will_build = (force_build or force_build_failures or
143            not result.already_done)
144        if result.already_done:
145            # Get the return code from that build and use it
146            with open(done_file, 'r') as fd:
147                result.return_code = int(fd.readline())
148            if will_build:
149                err_file = self.builder.GetErrFile(commit_upto, brd.target)
150                if os.path.exists(err_file) and os.stat(err_file).st_size:
151                    result.stderr = 'bad'
152                elif not force_build:
153                    # The build passed, so no need to build it again
154                    will_build = False
155
156        if will_build:
157            # We are going to have to build it. First, get a toolchain
158            if not self.toolchain:
159                try:
160                    self.toolchain = self.builder.toolchains.Select(brd.arch)
161                except ValueError as err:
162                    result.return_code = 10
163                    result.stdout = ''
164                    result.stderr = str(err)
165                    # TODO(sjg@chromium.org): This gets swallowed, but needs
166                    # to be reported.
167
168            if self.toolchain:
169                # Checkout the right commit
170                if self.builder.commits:
171                    commit = self.builder.commits[commit_upto]
172                    if self.builder.checkout:
173                        git_dir = os.path.join(work_dir, '.git')
174                        gitutil.Checkout(commit.hash, git_dir, work_dir,
175                                         force=True)
176                else:
177                    commit = 'current'
178
179                # Set up the environment and command line
180                env = self.toolchain.MakeEnvironment(self.builder.full_path)
181                Mkdir(out_dir)
182                args = []
183                cwd = work_dir
184                src_dir = os.path.realpath(work_dir)
185                if not self.builder.in_tree:
186                    if commit_upto is None:
187                        # In this case we are building in the original source
188                        # directory (i.e. the current directory where buildman
189                        # is invoked. The output directory is set to this
190                        # thread's selected work directory.
191                        #
192                        # Symlinks can confuse U-Boot's Makefile since
193                        # we may use '..' in our path, so remove them.
194                        work_dir = os.path.realpath(work_dir)
195                        args.append('O=%s/build' % work_dir)
196                        cwd = None
197                        src_dir = os.getcwd()
198                    else:
199                        args.append('O=build')
200                if not self.builder.verbose_build:
201                    args.append('-s')
202                if self.builder.num_jobs is not None:
203                    args.extend(['-j', str(self.builder.num_jobs)])
204                config_args = ['%s_defconfig' % brd.target]
205                config_out = ''
206                args.extend(self.builder.toolchains.GetMakeArguments(brd))
207
208                # If we need to reconfigure, do that now
209                if do_config:
210                    result = self.Make(commit, brd, 'mrproper', cwd,
211                            'mrproper', *args, env=env)
212                    config_out = result.combined
213                    result = self.Make(commit, brd, 'config', cwd,
214                            *(args + config_args), env=env)
215                    config_out += result.combined
216                    do_config = False   # No need to configure next time
217                if result.return_code == 0:
218                    result = self.Make(commit, brd, 'build', cwd, *args,
219                            env=env)
220                result.stderr = result.stderr.replace(src_dir + '/', '')
221                if self.builder.verbose_build:
222                    result.stdout = config_out + result.stdout
223            else:
224                result.return_code = 1
225                result.stderr = 'No tool chain for %s\n' % brd.arch
226            result.already_done = False
227
228        result.toolchain = self.toolchain
229        result.brd = brd
230        result.commit_upto = commit_upto
231        result.out_dir = out_dir
232        return result, do_config
233
234    def _WriteResult(self, result, keep_outputs):
235        """Write a built result to the output directory.
236
237        Args:
238            result: CommandResult object containing result to write
239            keep_outputs: True to store the output binaries, False
240                to delete them
241        """
242        # Fatal error
243        if result.return_code < 0:
244            return
245
246        # Aborted?
247        if result.stderr and 'No child processes' in result.stderr:
248            return
249
250        if result.already_done:
251            return
252
253        # Write the output and stderr
254        output_dir = self.builder._GetOutputDir(result.commit_upto)
255        Mkdir(output_dir)
256        build_dir = self.builder.GetBuildDir(result.commit_upto,
257                result.brd.target)
258        Mkdir(build_dir)
259
260        outfile = os.path.join(build_dir, 'log')
261        with open(outfile, 'w') as fd:
262            if result.stdout:
263                fd.write(result.stdout)
264
265        errfile = self.builder.GetErrFile(result.commit_upto,
266                result.brd.target)
267        if result.stderr:
268            with open(errfile, 'w') as fd:
269                fd.write(result.stderr)
270        elif os.path.exists(errfile):
271            os.remove(errfile)
272
273        if result.toolchain:
274            # Write the build result and toolchain information.
275            done_file = self.builder.GetDoneFile(result.commit_upto,
276                    result.brd.target)
277            with open(done_file, 'w') as fd:
278                fd.write('%s' % result.return_code)
279            with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
280                print >>fd, 'gcc', result.toolchain.gcc
281                print >>fd, 'path', result.toolchain.path
282                print >>fd, 'cross', result.toolchain.cross
283                print >>fd, 'arch', result.toolchain.arch
284                fd.write('%s' % result.return_code)
285
286            with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
287                print >>fd, 'gcc', result.toolchain.gcc
288                print >>fd, 'path', result.toolchain.path
289
290            # Write out the image and function size information and an objdump
291            env = result.toolchain.MakeEnvironment(self.builder.full_path)
292            lines = []
293            for fname in ['u-boot', 'spl/u-boot-spl']:
294                cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
295                nm_result = command.RunPipe([cmd], capture=True,
296                        capture_stderr=True, cwd=result.out_dir,
297                        raise_on_error=False, env=env)
298                if nm_result.stdout:
299                    nm = self.builder.GetFuncSizesFile(result.commit_upto,
300                                    result.brd.target, fname)
301                    with open(nm, 'w') as fd:
302                        print >>fd, nm_result.stdout,
303
304                cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
305                dump_result = command.RunPipe([cmd], capture=True,
306                        capture_stderr=True, cwd=result.out_dir,
307                        raise_on_error=False, env=env)
308                rodata_size = ''
309                if dump_result.stdout:
310                    objdump = self.builder.GetObjdumpFile(result.commit_upto,
311                                    result.brd.target, fname)
312                    with open(objdump, 'w') as fd:
313                        print >>fd, dump_result.stdout,
314                    for line in dump_result.stdout.splitlines():
315                        fields = line.split()
316                        if len(fields) > 5 and fields[1] == '.rodata':
317                            rodata_size = fields[2]
318
319                cmd = ['%ssize' % self.toolchain.cross, fname]
320                size_result = command.RunPipe([cmd], capture=True,
321                        capture_stderr=True, cwd=result.out_dir,
322                        raise_on_error=False, env=env)
323                if size_result.stdout:
324                    lines.append(size_result.stdout.splitlines()[1] + ' ' +
325                                 rodata_size)
326
327            # Write out the image sizes file. This is similar to the output
328            # of binutil's 'size' utility, but it omits the header line and
329            # adds an additional hex value at the end of each line for the
330            # rodata size
331            if len(lines):
332                sizes = self.builder.GetSizesFile(result.commit_upto,
333                                result.brd.target)
334                with open(sizes, 'w') as fd:
335                    print >>fd, '\n'.join(lines)
336
337        # Now write the actual build output
338        if keep_outputs:
339            patterns = ['u-boot', '*.bin', 'u-boot.dtb', '*.map', '*.img',
340                        'include/autoconf.mk', 'spl/u-boot-spl',
341                        'spl/u-boot-spl.bin']
342            for pattern in patterns:
343                file_list = glob.glob(os.path.join(result.out_dir, pattern))
344                for fname in file_list:
345                    shutil.copy(fname, build_dir)
346
347
348    def RunJob(self, job):
349        """Run a single job
350
351        A job consists of a building a list of commits for a particular board.
352
353        Args:
354            job: Job to build
355        """
356        brd = job.board
357        work_dir = self.builder.GetThreadDir(self.thread_num)
358        self.toolchain = None
359        if job.commits:
360            # Run 'make board_defconfig' on the first commit
361            do_config = True
362            commit_upto  = 0
363            force_build = False
364            for commit_upto in range(0, len(job.commits), job.step):
365                result, request_config = self.RunCommit(commit_upto, brd,
366                        work_dir, do_config,
367                        force_build or self.builder.force_build,
368                        self.builder.force_build_failures)
369                failed = result.return_code or result.stderr
370                did_config = do_config
371                if failed and not do_config:
372                    # If our incremental build failed, try building again
373                    # with a reconfig.
374                    if self.builder.force_config_on_failure:
375                        result, request_config = self.RunCommit(commit_upto,
376                            brd, work_dir, True, True, False)
377                        did_config = True
378                if not self.builder.force_reconfig:
379                    do_config = request_config
380
381                # If we built that commit, then config is done. But if we got
382                # an warning, reconfig next time to force it to build the same
383                # files that created warnings this time. Otherwise an
384                # incremental build may not build the same file, and we will
385                # think that the warning has gone away.
386                # We could avoid this by using -Werror everywhere...
387                # For errors, the problem doesn't happen, since presumably
388                # the build stopped and didn't generate output, so will retry
389                # that file next time. So we could detect warnings and deal
390                # with them specially here. For now, we just reconfigure if
391                # anything goes work.
392                # Of course this is substantially slower if there are build
393                # errors/warnings (e.g. 2-3x slower even if only 10% of builds
394                # have problems).
395                if (failed and not result.already_done and not did_config and
396                        self.builder.force_config_on_failure):
397                    # If this build failed, try the next one with a
398                    # reconfigure.
399                    # Sometimes if the board_config.h file changes it can mess
400                    # with dependencies, and we get:
401                    # make: *** No rule to make target `include/autoconf.mk',
402                    #     needed by `depend'.
403                    do_config = True
404                    force_build = True
405                else:
406                    force_build = False
407                    if self.builder.force_config_on_failure:
408                        if failed:
409                            do_config = True
410                    result.commit_upto = commit_upto
411                    if result.return_code < 0:
412                        raise ValueError('Interrupt')
413
414                # We have the build results, so output the result
415                self._WriteResult(result, job.keep_outputs)
416                self.builder.out_queue.put(result)
417        else:
418            # Just build the currently checked-out build
419            result, request_config = self.RunCommit(None, brd, work_dir, True,
420                        True, self.builder.force_build_failures)
421            result.commit_upto = 0
422            self._WriteResult(result, job.keep_outputs)
423            self.builder.out_queue.put(result)
424
425    def run(self):
426        """Our thread's run function
427
428        This thread picks a job from the queue, runs it, and then goes to the
429        next job.
430        """
431        alive = True
432        while True:
433            job = self.builder.queue.get()
434            if self.builder.active and alive:
435                self.RunJob(job)
436            '''
437            try:
438                if self.builder.active and alive:
439                    self.RunJob(job)
440            except Exception as err:
441                alive = False
442                print err
443            '''
444            self.builder.queue.task_done()
445