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