1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2013 The Chromium OS Authors.
5 """Control module for buildman
7 This holds the main control logic for buildman, when not running tests.
11 import multiprocessing
18 from buildman import boards
19 from buildman import bsettings
20 from buildman import cfgutil
21 from buildman import toolchain
22 from buildman.builder import Builder
23 from patman import gitutil
24 from patman import patchstream
25 from u_boot_pylib import command
26 from u_boot_pylib import terminal
27 from u_boot_pylib import tools
28 from u_boot_pylib.terminal import print_clear, tprint
32 # Space-separated list of buildman process IDs currently running jobs
33 RUNNING_FNAME = f'buildmanq.{getpass.getuser()}'
35 # Lock file for access to RUNNING_FILE
36 LOCK_FNAME = f'{RUNNING_FNAME}.lock'
38 # Wait time for access to lock (seconds)
41 # Wait time to start running
44 def get_plural(count):
45 """Returns a plural 's' if count is not 1"""
46 return 's' if count != 1 else ''
49 def count_build_commits(commits, step):
50 """Calculate the number of commits to be built
53 commits (list of Commit): Commits to build or None
54 step (int): Step value for commits, typically 1
57 Number of commits that will be built
61 return (count + step - 1) // step
65 def get_action_summary(is_summary, commit_count, selected, threads, jobs):
66 """Return a string summarising the intended action.
69 is_summary (bool): True if this is a summary (otherwise it is building)
70 commits (list): List of commits being built
71 selected (list of Board): List of Board objects that are marked
72 step (int): Step increment through commits
73 threads (int): Number of processor threads being used
74 jobs (int): Number of jobs to build at once
80 commit_str = f'{commit_count} commit{get_plural(commit_count)}'
82 commit_str = 'current source'
83 msg = (f"{'Summary of' if is_summary else 'Building'} "
84 f'{commit_str} for {len(selected)} boards')
85 msg += (f' ({threads} thread{get_plural(threads)}, '
86 f'{jobs} job{get_plural(jobs)} per thread)')
89 # pylint: disable=R0913
90 def show_actions(series, why_selected, boards_selected, output_dir,
91 board_warnings, step, threads, jobs, verbose):
92 """Display a list of actions that we would take, if not a dry run.
96 why_selected: Dictionary where each key is a buildman argument
97 provided by the user, and the value is the list of boards
98 brought in by that argument. For example, 'arm' might bring
99 in 400 boards, so in this case the key would be 'arm' and
100 the value would be a list of board names.
101 boards_selected: Dict of selected boards, key is target name,
102 value is Board object
103 output_dir (str): Output directory for builder
104 board_warnings: List of warnings obtained from board selected
105 step (int): Step increment through commits
106 threads (int): Number of processor threads being used
107 jobs (int): Number of jobs to build at once
108 verbose (bool): True to indicate why each board was selected
110 col = terminal.Color()
111 print('Dry run, so not doing much. But I would do this:')
114 commits = series.commits
117 print(get_action_summary(False, count_build_commits(commits, step),
118 boards_selected, threads, jobs))
119 print(f'Build directory: {output_dir}')
121 for upto in range(0, len(series.commits), step):
122 commit = series.commits[upto]
123 print(' ', col.build(col.YELLOW, commit.hash[:8], bright=False), end=' ')
124 print(commit.subject)
126 for arg in why_selected:
127 # When -x is used, only the 'all' member exists
128 if arg != 'all' or len(why_selected) == 1:
129 print(arg, f': {len(why_selected[arg])} boards')
131 print(f" {' '.join(why_selected[arg])}")
132 print('Total boards to build for each '
133 f"commit: {len(why_selected['all'])}\n")
135 for warning in board_warnings:
136 print(col.build(col.YELLOW, warning))
138 def show_toolchain_prefix(brds, toolchains):
139 """Show information about a the tool chain used by one or more boards
141 The function checks that all boards use the same toolchain, then prints
142 the correct value for CROSS_COMPILE.
145 boards: Boards object containing selected boards
146 toolchains: Toolchains object containing available toolchains
149 None on success, string error message otherwise
151 board_selected = brds.get_selected_dict()
153 for brd in board_selected.values():
154 tc_set.add(toolchains.Select(brd.arch))
156 sys.exit('Supplied boards must share one toolchain')
157 tchain = tc_set.pop()
158 print(tchain.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
161 """Show information about a the architecture used by one or more boards
163 The function checks that all boards use the same architecture, then prints
164 the correct value for ARCH.
167 boards: Boards object containing selected boards
170 None on success, string error message otherwise
172 board_selected = brds.get_selected_dict()
174 for brd in board_selected.values():
175 arch_set.add(brd.arch)
176 if len(arch_set) != 1:
177 sys.exit('Supplied boards must share one arch')
178 print(arch_set.pop())
180 def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
181 """Figure out whether to allow external blobs
183 Uses the allow-missing setting and the provided arguments to decide whether
184 missing external blobs should be allowed
187 opt_allow (bool): True if --allow-missing flag is set
188 opt_no_allow (bool): True if --no-allow-missing flag is set
189 num_selected (int): Number of selected board
190 has_branch (bool): True if a git branch (to build) has been provided
193 bool: True to allow missing external blobs, False to produce an error if
194 external blobs are used
196 allow_missing = False
197 am_setting = bsettings.get_global_item_value('allow-missing')
199 if am_setting == 'always':
201 if 'multiple' in am_setting and num_selected > 1:
203 if 'branch' in am_setting and has_branch:
209 allow_missing = False
213 def count_commits(branch, count, col, git_dir):
214 """Could the number of commits in the branch/ranch being built
217 branch (str): Name of branch to build, or None if none
218 count (int): Number of commits to build, or -1 for all
219 col (Terminal.Color): Color object to use
220 git_dir (str): Git directory to use, e.g. './.git'
224 Number of commits being built
225 True if the 'branch' string contains a range rather than a simple
228 has_range = branch and '..' in branch
234 count, msg = gitutil.count_commits_in_range(git_dir, branch)
236 count, msg = gitutil.count_commits_in_branch(git_dir, branch)
238 sys.exit(col.build(col.RED, msg))
240 sys.exit(col.build(col.RED,
241 f"Range '{branch}' has no commits"))
243 print(col.build(col.YELLOW, msg))
244 count += 1 # Build upstream commit also
247 msg = (f"No commits found to process in branch '{branch}': "
248 "set branch's upstream or use -c flag")
249 sys.exit(col.build(col.RED, msg))
250 return count, has_range
253 def determine_series(selected, col, git_dir, count, branch, work_in_output):
254 """Determine the series which is to be built, if any
256 If there is a series, the commits in that series are numbered by setting
257 their sequence value (starting from 0). This is used by tests.
260 selected (list of Board): List of Board objects that are marked
262 col (Terminal.Color): Color object to use
263 git_dir (str): Git directory to use, e.g. './.git'
264 count (int): Number of commits in branch
265 branch (str): Name of branch to build, or None if none
266 work_in_output (bool): True to work in the output directory
269 Series: Series to build, or None for none
271 Read the metadata from the commits. First look at the upstream commit,
272 then the ones in the branch. We would like to do something like
273 upstream/master~..branch but that isn't possible if upstream/master is
274 a merge commit (it will list all the commits that form part of the
277 Conflicting tags are not a problem for buildman, since it does not use
278 them. For example, Series-version is not useful for buildman. On the
279 other hand conflicting tags will cause an error. So allow later tags
280 to overwrite earlier ones by setting allow_overwrite=True
283 # Work out how many commits to build. We want to build everything on the
284 # branch. We also build the upstream commit as a control so we can see
285 # problems introduced by the first commit on the branch.
286 count, has_range = count_commits(branch, count, col, git_dir)
288 if len(selected) != 1:
289 sys.exit(col.build(col.RED,
290 '-w can only be used with a single board'))
292 sys.exit(col.build(col.RED,
293 '-w can only be used with a single commit'))
300 range_expr = gitutil.get_range_in_branch(git_dir, branch)
301 upstream_commit = gitutil.get_upstream(git_dir, branch)
302 series = patchstream.get_metadata_for_list(upstream_commit,
303 git_dir, 1, series=None, allow_overwrite=True)
305 series = patchstream.get_metadata_for_list(range_expr,
306 git_dir, None, series, allow_overwrite=True)
309 series = patchstream.get_metadata_for_list(branch,
310 git_dir, count, series=None, allow_overwrite=True)
312 # Number the commits for test purposes
313 for i, commit in enumerate(series.commits):
320 def do_fetch_arch(toolchains, col, fetch_arch):
321 """Handle the --fetch-arch option
324 toolchains (Toolchains): Tool chains to use
325 col (terminal.Color): Color object to build
326 fetch_arch (str): Argument passed to the --fetch-arch option
329 int: Return code for buildman
331 if fetch_arch == 'list':
332 sorted_list = toolchains.ListArchs()
335 f"Available architectures: {' '.join(sorted_list)}\n"))
338 if fetch_arch == 'all':
339 fetch_arch = ','.join(toolchains.ListArchs())
340 print(col.build(col.CYAN,
341 f'\nDownloading toolchains: {fetch_arch}'))
342 for arch in fetch_arch.split(','):
344 ret = toolchains.FetchAndInstall(arch)
350 def get_toolchains(toolchains, col, override_toolchain, fetch_arch,
351 list_tool_chains, verbose):
352 """Get toolchains object to use
355 toolchains (Toolchains or None): Toolchains to use. If None, then a
356 Toolchains object will be created and scanned
357 col (Terminal.Color): Color object
358 override_toolchain (str or None): Override value for toolchain, or None
359 fetch_arch (bool): True to fetch the toolchain for the architectures
360 list_tool_chains (bool): True to list all tool chains
361 verbose (bool): True for verbose output when listing toolchains
365 int: Operation completed and buildman should exit with exit code
366 Toolchains: Toolchains object to use
368 no_toolchains = toolchains is None
370 toolchains = toolchain.Toolchains(override_toolchain)
373 return do_fetch_arch(toolchains, col, fetch_arch)
376 toolchains.GetSettings()
377 toolchains.Scan(list_tool_chains and verbose)
385 def get_boards_obj(output_dir, regen_board_list, maintainer_check, full_check,
387 """Object the Boards object to use
389 Creates the output directory and ensures there is a boards.cfg file, then
393 output_dir (str): Output directory to use
394 regen_board_list (bool): True to just regenerate the board list
395 maintainer_check (bool): True to just run a maintainer check
396 full_check (bool): True to just run a full check of Kconfig and
398 threads (int or None): Number of threads to use to create boards file
399 verbose (bool): False to suppress output from boards-file generation
403 int: Operation completed and buildman should exit with exit code
404 Boards: Boards object to use
406 brds = boards.Boards()
407 nr_cpus = threads or multiprocessing.cpu_count()
408 if maintainer_check or full_check:
409 warnings = brds.build_board_list(jobs=nr_cpus,
410 warn_targets=full_check)[1]
412 for warn in warnings:
413 print(warn, file=sys.stderr)
417 if not os.path.exists(output_dir):
418 os.makedirs(output_dir)
419 board_file = os.path.join(output_dir, 'boards.cfg')
420 if regen_board_list and regen_board_list != '-':
421 board_file = regen_board_list
423 okay = brds.ensure_board_list(board_file, nr_cpus, force=regen_board_list,
426 return 0 if okay else 2
427 brds.read_boards(board_file)
431 def determine_boards(brds, args, col, opt_boards, exclude_list):
432 """Determine which boards to build
434 Each element of args and exclude can refer to a board name, arch or SoC
437 brds (Boards): Boards object
438 args (list of str): Arguments describing boards to build
439 col (Terminal.Color): Color object
440 opt_boards (list of str): Specific boards to build, or None for all
441 exclude_list (list of str): Arguments describing boards to exclude
445 list of Board: List of Board objects that are marked selected
446 why_selected: Dictionary where each key is a buildman argument
447 provided by the user, and the value is the list of boards
448 brought in by that argument. For example, 'arm' might bring
449 in 400 boards, so in this case the key would be 'arm' and
450 the value would be a list of board names.
451 board_warnings: List of warnings obtained from board selected
455 for arg in exclude_list:
456 exclude += arg.split(',')
459 requested_boards = []
460 for brd in opt_boards:
461 requested_boards += brd.split(',')
463 requested_boards = None
464 why_selected, board_warnings = brds.select_boards(args, exclude,
466 selected = brds.get_selected()
468 sys.exit(col.build(col.RED, 'No matching boards found'))
469 return selected, why_selected, board_warnings
472 def adjust_args(args, series, selected):
473 """Adjust arguments according to various constraints
475 Updates verbose, show_errors, threads, jobs and step
478 args (Namespace): Namespace object to adjust
479 series (Series): Series being built / summarised
480 selected (list of Board): List of Board objects that are marked
482 if not series and not args.dry_run:
485 args.show_errors = True
487 # By default we have one thread per CPU. But if there are not enough jobs
488 # we can have fewer threads and use a high '-j' value for make.
489 if args.threads is None:
490 args.threads = min(multiprocessing.cpu_count(), len(selected))
492 args.jobs = max(1, (multiprocessing.cpu_count() +
493 len(selected) - 1) // len(selected))
496 args.step = len(series.commits) - 1
498 # We can't show function sizes without board details at present
500 args.show_detail = True
503 def setup_output_dir(output_dir, work_in_output, branch, no_subdirs, col,
505 """Set up the output directory
508 output_dir (str): Output directory provided by the user, or None if none
509 work_in_output (bool): True to work in the output directory
510 branch (str): Name of branch to build, or None if none
511 no_subdirs (bool): True to put the output in the top-level output dir
512 clean_dir: Used for tests only, indicates that the existing output_dir
513 should be removed before starting the build
516 str: Updated output directory pathname
520 sys.exit(col.build(col.RED, '-w requires that you specify -o'))
522 if branch and not no_subdirs:
523 # As a special case allow the board directory to be placed in the
524 # output directory itself rather than any subdirectory.
525 dirname = branch.replace('/', '_')
526 output_dir = os.path.join(output_dir, dirname)
527 if clean_dir and os.path.exists(output_dir):
528 shutil.rmtree(output_dir)
532 def run_builder(builder, commits, board_selected, args):
533 """Run the builder or show the summary
536 commits (list of Commit): List of commits being built, None if no branch
537 boards_selected (dict): Dict of selected boards:
540 args (Namespace): Namespace to use
543 int: Return code for buildman
545 gnu_make = command.output(os.path.join(args.git,
546 'scripts/show-gnu-make'), raise_on_error=False).rstrip()
548 sys.exit('GNU Make not found')
549 builder.gnu_make = gnu_make
552 commit_count = count_build_commits(commits, args.step)
553 tprint(get_action_summary(args.summary, commit_count, board_selected,
554 args.threads, args.jobs))
556 builder.set_display_options(
557 args.show_errors, args.show_sizes, args.show_detail, args.show_bloat,
558 args.list_error_boards, args.show_config, args.show_environment,
559 args.filter_dtb_warnings, args.filter_migration_warnings, args.ide)
561 builder.show_summary(commits, board_selected)
563 fail, warned, excs = builder.build_boards(
564 commits, board_selected, args.keep_outputs, args.verbose)
569 if warned and not args.ignore_warnings:
574 def calc_adjust_cfg(adjust_cfg, reproducible_builds):
575 """Calculate the value to use for adjust_cfg
578 adjust_cfg (list of str): List of configuration changes. See cfgutil for
580 reproducible_builds (bool): True to adjust the configuration to get
584 adjust_cfg (list of str): List of configuration changes
586 adjust_cfg = cfgutil.convert_list_to_dict(adjust_cfg)
588 # Drop LOCALVERSION_AUTO since it changes the version string on every commit
589 if reproducible_builds:
590 # If these are mentioned, leave the local version alone
591 if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
592 print('Not dropping LOCALVERSION_AUTO for reproducible build')
594 adjust_cfg['LOCALVERSION_AUTO'] = '~'
598 def read_procs(tmpdir=tempfile.gettempdir()):
599 """Read the list of running buildman processes
601 If the list is corrupted, returns an empty list
604 tmpdir (str): Temporary directory to use (for testing only)
606 running_fname = os.path.join(tmpdir, RUNNING_FNAME)
608 if os.path.exists(running_fname):
609 items = tools.read_file(running_fname, binary=False).split()
611 procs = [int(x) for x in items]
612 except ValueError: # Handle invalid format
618 """Check for existence of a unix PID
620 https://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid-in-python
623 pid (int): PID to check
626 True if it exists, else False
636 def write_procs(procs, tmpdir=tempfile.gettempdir()):
637 """Write the list of running buildman processes
640 tmpdir (str): Temporary directory to use (for testing only)
642 running_fname = os.path.join(tmpdir, RUNNING_FNAME)
643 tools.write_file(running_fname, ' '.join([str(p) for p in procs]),
646 # Allow another user to access the file
647 os.chmod(running_fname, 0o666)
649 def wait_for_process_limit(limit, tmpdir=tempfile.gettempdir(),
651 """Wait until the number of buildman processes drops to the limit
653 This uses FileLock to protect a 'running' file, which contains a list of
654 PIDs of running buildman processes. The number of PIDs in the file indicates
655 the number of running processes.
657 When buildman starts up, it calls this function to wait until it is OK to
660 On exit, no attempt is made to remove the PID from the file, since other
661 buildman processes will notice that the PID is no-longer valid, and ignore
664 Two timeouts are provided:
665 LOCK_WAIT_S: length of time to wait for the lock; if this occurs, the
666 lock is busted / removed before trying again
667 RUN_WAIT_S: length of time to wait to be allowed to run; if this occurs,
668 the build starts, with the PID being added to the file.
671 limit (int): Maximum number of buildman processes, including this one;
673 tmpdir (str): Temporary directory to use (for testing only)
674 pid (int): Current process ID (for testing only)
676 from filelock import Timeout, FileLock
678 running_fname = os.path.join(tmpdir, RUNNING_FNAME)
679 lock_fname = os.path.join(tmpdir, LOCK_FNAME)
680 lock = FileLock(lock_fname)
682 # Allow another user to access the file
683 col = terminal.Color()
684 tprint('Waiting for other buildman processes...', newline=False,
688 deadline = time.time() + RUN_WAIT_S
691 with lock.acquire(timeout=LOCK_WAIT_S):
692 os.chmod(lock_fname, 0o666)
693 procs = read_procs(tmpdir)
695 # Drop PIDs which are not running
696 procs = list(filter(check_pid, procs))
698 # If we haven't hit the limit, add ourself
699 if len(procs) < limit:
700 tprint('done...', newline=False)
702 if time.time() >= deadline:
703 tprint('timeout...', newline=False)
706 write_procs(procs + [pid], tmpdir)
710 tprint('failed to get lock: busting...', newline=False)
711 os.remove(lock_fname)
714 tprint('starting build', newline=False)
717 def do_buildman(args, toolchains=None, make_func=None, brds=None,
718 clean_dir=False, test_thread_exceptions=False):
719 """The main control code for buildman
722 args: ArgumentParser object
723 args: Command line arguments (list of strings)
724 toolchains: Toolchains to use - this should be a Toolchains()
725 object. If None, then it will be created and scanned
726 make_func: Make function to use for the builder. This is called
727 to execute 'make'. If this is None, the normal function
728 will be used, which calls the 'make' tool with suitable
729 arguments. This setting is useful for tests.
730 brds: Boards() object to use, containing a list of available
731 boards. If this is None it will be created and scanned.
732 clean_dir: Used for tests only, indicates that the existing output_dir
733 should be removed before starting the build
734 test_thread_exceptions: Uses for tests only, True to make the threads
735 raise an exception instead of reporting their result. This simulates
736 a failure in the code somewhere
738 # Used so testing can obtain the builder: pylint: disable=W0603
742 col = terminal.Color()
744 git_dir = os.path.join(args.git, '.git')
746 toolchains = get_toolchains(toolchains, col, args.override_toolchain,
747 args.fetch_arch, args.list_tool_chains,
749 if isinstance(toolchains, int):
752 output_dir = setup_output_dir(
753 args.output_dir, args.work_in_output, args.branch,
754 args.no_subdirs, col, clean_dir)
756 # Work out what subset of the boards we are building
758 brds = get_boards_obj(output_dir, args.regen_board_list,
759 args.maintainer_check, args.full_check,
760 args.threads, args.verbose and
761 not args.print_arch and not args.print_prefix)
762 if isinstance(brds, int):
765 selected, why_selected, board_warnings = determine_boards(
766 brds, args.terms, col, args.boards, args.exclude)
768 if args.print_prefix:
769 show_toolchain_prefix(brds, toolchains)
776 series = determine_series(selected, col, git_dir, args.count,
777 args.branch, args.work_in_output)
779 adjust_args(args, series, selected)
781 # For a dry run, just show our actions as a sanity check
783 show_actions(series, why_selected, selected, output_dir, board_warnings,
784 args.step, args.threads, args.jobs,
788 # Create a new builder with the selected args
789 builder = Builder(toolchains, output_dir, git_dir,
790 args.threads, args.jobs, checkout=True,
791 show_unknown=args.show_unknown, step=args.step,
792 no_subdirs=args.no_subdirs, full_path=args.full_path,
793 verbose_build=args.verbose_build,
794 mrproper=args.mrproper,
795 fallback_mrproper=args.fallback_mrproper,
796 per_board_out_dir=args.per_board_out_dir,
797 config_only=args.config_only,
798 squash_config_y=not args.preserve_config_y,
799 warnings_as_errors=args.warnings_as_errors,
800 work_in_output=args.work_in_output,
801 test_thread_exceptions=test_thread_exceptions,
802 adjust_cfg=calc_adjust_cfg(args.adjust_cfg,
803 args.reproducible_builds),
804 allow_missing=get_allow_missing(args.allow_missing,
805 args.no_allow_missing,
806 len(selected), args.branch),
808 reproducible_builds=args.reproducible_builds,
809 force_build = args.force_build,
810 force_build_failures = args.force_build_failures,
811 force_reconfig = args.force_reconfig, in_tree = args.in_tree,
812 force_config_on_failure=not args.quick, make_func=make_func)
814 TEST_BUILDER = builder
816 if args.process_limit:
817 wait_for_process_limit(args.process_limit)
819 return run_builder(builder, series.commits if series else None,
820 brds.get_selected_dict(), args)