]> git.sur5r.net Git - u-boot/blob - tools/buildman/builderthread.py
buildman: Show 'make' command line when -V is used
[u-boot] / tools / buildman / builderthread.py
1 # Copyright (c) 2014 Google, Inc
2 #
3 # SPDX-License-Identifier:      GPL-2.0+
4 #
5
6 import errno
7 import glob
8 import os
9 import shutil
10 import threading
11
12 import command
13 import gitutil
14
15 def 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
32 class 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
44 class 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
70 class 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()