| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # Copyright 2019 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Runs a tryjob/tryjobs after updating the packages.""" |
| |
| from __future__ import print_function |
| |
| import argparse |
| import datetime |
| import json |
| import os |
| import subprocess |
| |
| import chroot |
| import failure_modes |
| import get_llvm_hash |
| import update_chromeos_llvm_hash |
| |
| VALID_CQ_TRYBOTS = ['llvm', 'llvm-next', 'llvm-tot'] |
| |
| |
| def GetCommandLineArgs(): |
| """Parses the command line for the command line arguments. |
| |
| Returns: |
| The log level to use when retrieving the LLVM hash or google3 LLVM version, |
| the chroot path to use for executing chroot commands, |
| a list of a package or packages to update their LLVM next hash, |
| and the LLVM version to use when retrieving the LLVM hash. |
| """ |
| |
| # Default path to the chroot if a path is not specified. |
| cros_root = os.path.expanduser('~') |
| cros_root = os.path.join(cros_root, 'chromiumos') |
| |
| # Create parser and add optional command-line arguments. |
| parser = argparse.ArgumentParser( |
| description='Update an LLVM hash of packages and run tests.') |
| |
| # Add argument for other change lists that want to run alongside the tryjob |
| # which has a change list of updating a package's git hash. |
| parser.add_argument( |
| '--extra_change_lists', |
| type=int, |
| nargs='+', |
| default=[], |
| help='change lists that would like to be run alongside the change list ' |
| 'of updating the packages') |
| |
| # Add argument for a specific chroot path. |
| parser.add_argument( |
| '--chroot_path', |
| default=cros_root, |
| help='the path to the chroot (default: %(default)s)') |
| |
| # Add argument to choose between llvm and llvm-next. |
| parser.add_argument( |
| '--is_llvm_next', |
| action='store_true', |
| help='which llvm hash to update. Update LLVM_NEXT_HASH if specified. ' |
| 'Otherwise, update LLVM_HASH') |
| |
| # Add argument for the absolute path to the file that contains information on |
| # the previous tested svn version. |
| parser.add_argument( |
| '--last_tested', |
| help='the absolute path to the file that contains the last tested ' |
| 'arguments.') |
| |
| # Add argument for the LLVM version to use. |
| parser.add_argument( |
| '--llvm_version', |
| type=get_llvm_hash.is_svn_option, |
| required=True, |
| help='which git hash of LLVM to find ' |
| '{google3, ToT, <svn_version>} ' |
| '(default: finds the git hash of the google3 LLVM ' |
| 'version)') |
| |
| # Add argument to add reviewers for the created CL. |
| parser.add_argument( |
| '--reviewers', |
| nargs='+', |
| default=[], |
| help='The reviewers for the package update changelist') |
| |
| # Add argument for whether to display command contents to `stdout`. |
| parser.add_argument( |
| '--verbose', |
| action='store_true', |
| help='display contents of a command to the terminal ' |
| '(default: %(default)s)') |
| |
| subparsers = parser.add_subparsers(dest='subparser_name') |
| subparser_names = [] |
| # Testing with the tryjobs. |
| tryjob_subparser = subparsers.add_parser('tryjobs') |
| subparser_names.append('tryjobs') |
| tryjob_subparser.add_argument( |
| '--builders', |
| required=True, |
| nargs='+', |
| default=[], |
| help='builders to use for the tryjob testing') |
| |
| # Add argument for custom options for the tryjob. |
| tryjob_subparser.add_argument( |
| '--options', |
| required=False, |
| nargs='+', |
| default=[], |
| help='options to use for the tryjob testing') |
| |
| # Testing with the recipe builders |
| recipe_subparser = subparsers.add_parser('recipe') |
| subparser_names.append('recipe') |
| recipe_subparser.add_argument( |
| '--options', |
| required=False, |
| nargs='+', |
| default=[], |
| help='options passed to the recipe builders') |
| |
| recipe_subparser.add_argument( |
| '--builders', |
| required=True, |
| nargs='+', |
| default=[], |
| help='recipe builders to launch') |
| |
| # Testing with CQ. |
| cq_subparser = subparsers.add_parser('cq') |
| subparser_names.append('cq') |
| |
| # Add argument for specify a cq trybot to test along with other cq builders |
| # e.g. llvm, llvm-next or llvm-tot |
| cq_subparser.add_argument( |
| '--cq_trybot', |
| choices=VALID_CQ_TRYBOTS, |
| help='include the trybot to test together with other cq builders ' |
| 'available: %(choices)s') |
| |
| args_output = parser.parse_args() |
| |
| if args_output.subparser_name not in subparser_names: |
| parser.error('one of %s must be specified' % subparser_names) |
| |
| return args_output |
| |
| |
| def UnchangedSinceLastRun(last_tested_file, arg_dict): |
| """Gets the arguments used for last run |
| |
| Args: |
| last_tested_file: The absolute path to the file that contains the |
| arguments for the last run. |
| arg_dict: The arguments used for this run. |
| |
| Returns: |
| Return true if the arguments used for last run exist and are the |
| same as the arguments used for this run. Otherwise return false. |
| """ |
| |
| if not last_tested_file: |
| return False |
| |
| # Get the last tested svn version if the file exists. |
| last_arg_dict = None |
| try: |
| with open(last_tested_file) as f: |
| last_arg_dict = json.load(f) |
| |
| except (IOError, ValueError): |
| return False |
| |
| return arg_dict == last_arg_dict |
| |
| |
| def AddReviewers(cl, reviewers, chroot_path): |
| """Add reviewers for the created CL.""" |
| |
| gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') |
| for reviewer in reviewers: |
| cmd = [gerrit_abs_path, 'reviewers', str(cl), reviewer] |
| |
| subprocess.check_output(cmd) |
| |
| |
| def AddLinksToCL(tests, cl, chroot_path): |
| """Adds the test link(s) to the CL as a comment.""" |
| |
| # NOTE: Invoking `cros_sdk` does not make each tryjob link appear on its own |
| # line, so invoking the `gerrit` command directly instead of using `cros_sdk` |
| # to do it for us. |
| # |
| # FIXME: Need to figure out why `cros_sdk` does not add each tryjob link as a |
| # newline. |
| gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') |
| |
| links = ['Started the following tests:'] |
| links.extend(test['link'] for test in tests) |
| |
| add_message_cmd = [gerrit_abs_path, 'message', str(cl), '\n'.join(links)] |
| |
| subprocess.check_output(add_message_cmd) |
| |
| |
| # Testing with tryjobs |
| def GetCurrentTimeInUTC(): |
| """Returns the current time via `datetime.datetime.utcnow()`.""" |
| return datetime.datetime.utcnow() |
| |
| |
| def GetTryJobCommand(change_list, extra_change_lists, options, builder): |
| """Constructs the 'tryjob' command. |
| |
| Args: |
| change_list: The CL obtained from updating the packages. |
| extra_change_lists: Extra change lists that would like to be run alongside |
| the change list of updating the packages. |
| options: Options to be passed into the tryjob command. |
| builder: The builder to be passed into the tryjob command. |
| |
| Returns: |
| The 'tryjob' command with the change list of updating the packages and |
| any extra information that was passed into the command line. |
| """ |
| |
| tryjob_cmd = ['cros', 'tryjob', '--yes', '--json', '-g', '%d' % change_list] |
| |
| if extra_change_lists: |
| for extra_cl in extra_change_lists: |
| tryjob_cmd.extend(['-g', '%d' % extra_cl]) |
| |
| if options: |
| tryjob_cmd.extend('--%s' % option for option in options) |
| |
| tryjob_cmd.append(builder) |
| |
| return tryjob_cmd |
| |
| |
| def RunTryJobs(cl_number, extra_change_lists, options, builders, chroot_path): |
| """Runs a tryjob/tryjobs. |
| |
| Args: |
| cl_number: The CL created by updating the packages. |
| extra_change_lists: Any extra change lists that would run alongside the CL |
| that was created by updating the packages ('cl_number'). |
| options: Any options to be passed into the 'tryjob' command. |
| builders: All the builders to run the 'tryjob' with. |
| chroot_path: The absolute path to the chroot. |
| |
| Returns: |
| A list that contains stdout contents of each tryjob, where stdout is |
| information (a hashmap) about the tryjob. The hashmap also contains stderr |
| if there was an error when running a tryjob. |
| |
| Raises: |
| ValueError: Failed to submit a tryjob. |
| """ |
| |
| # Contains the results of each builder. |
| tests = [] |
| |
| # Run tryjobs with the change list number obtained from updating the |
| # packages and append additional changes lists and options obtained from the |
| # command line. |
| for builder in builders: |
| cmd = GetTryJobCommand(cl_number, extra_change_lists, options, builder) |
| |
| out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8') |
| |
| test_output = json.loads(out) |
| |
| buildbucket_id = int(test_output[0]['id']) |
| |
| tests.append({ |
| 'launch_time': str(GetCurrentTimeInUTC()), |
| 'link': 'http://ci.chromium.org/b/%s' % buildbucket_id, |
| 'buildbucket_id': buildbucket_id, |
| 'extra_cls': extra_change_lists, |
| 'options': options, |
| 'builder': [builder] |
| }) |
| |
| AddLinksToCL(tests, cl_number, chroot_path) |
| |
| return tests |
| |
| |
| def StartRecipeBuilders(cl_number, extra_change_lists, options, builders, |
| chroot_path): |
| """Launch recipe builders. |
| |
| Args: |
| cl_number: The CL created by updating the packages. |
| extra_change_lists: Any extra change lists that would run alongside the CL |
| that was created by updating the packages ('cl_number'). |
| options: Any options to be passed into the 'tryjob' command. |
| builders: All the builders to run the 'tryjob' with. |
| chroot_path: The absolute path to the chroot. |
| |
| Returns: |
| A list that contains stdout contents of each builder, where stdout is |
| information (a hashmap) about the tryjob. The hashmap also contains stderr |
| if there was an error when running a tryjob. |
| |
| Raises: |
| ValueError: Failed to start a builder. |
| """ |
| |
| # Contains the results of each builder. |
| tests = [] |
| |
| # Launch a builders with the change list number obtained from updating the |
| # packages and append additional changes lists and options obtained from the |
| # command line. |
| for builder in builders: |
| cmd = ['bb', 'add', '-json'] |
| |
| if cl_number: |
| cmd.extend(['-cl', 'crrev.com/c/%d' % cl_number]) |
| |
| if extra_change_lists: |
| for cl in extra_change_lists: |
| cmd.extend(['-cl', 'crrev.com/c/%d' % cl]) |
| |
| if options: |
| cmd.extend(options) |
| |
| cmd.append(builder) |
| |
| out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8') |
| |
| test_output = json.loads(out) |
| |
| tests.append({ |
| 'launch_time': test_output['createTime'], |
| 'link': 'http://ci.chromium.org/b/%s' % test_output['id'], |
| 'buildbucket_id': test_output['id'], |
| 'extra_cls': extra_change_lists, |
| 'options': options, |
| 'builder': [builder] |
| }) |
| |
| AddLinksToCL(tests, cl_number, chroot_path) |
| |
| return tests |
| |
| |
| # Testing with CQ |
| def GetCQDependString(dependent_cls): |
| """Get CQ dependency string e.g. `Cq-Depend: chromium:MM, chromium:NN`.""" |
| |
| if not dependent_cls: |
| return None |
| |
| # Cq-Depend must start a new paragraph prefixed with "Cq-Depend". |
| return '\nCq-Depend: ' + ', '.join(('chromium:%s' % i) for i in dependent_cls) |
| |
| |
| def GetCQIncludeTrybotsString(trybot): |
| """Get Cq-Include-Trybots string, for more llvm testings""" |
| |
| if not trybot: |
| return None |
| |
| if trybot not in VALID_CQ_TRYBOTS: |
| raise ValueError('%s is not a valid llvm trybot' % trybot) |
| |
| # Cq-Include-Trybots must start a new paragraph prefixed |
| # with "Cq-Include-Trybots". |
| return '\nCq-Include-Trybots:chromeos/cq:cq-%s-orchestrator' % trybot |
| |
| |
| def StartCQDryRun(cl, dependent_cls, chroot_path): |
| """Start CQ dry run for the changelist and dependencies.""" |
| |
| gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') |
| |
| cl_list = [cl] |
| cl_list.extend(dependent_cls) |
| |
| for changes in cl_list: |
| cq_dry_run_cmd = [gerrit_abs_path, 'label-cq', str(changes), '1'] |
| |
| subprocess.check_output(cq_dry_run_cmd) |
| |
| |
| def main(): |
| """Updates the packages' LLVM hash and run tests. |
| |
| Raises: |
| AssertionError: The script was run inside the chroot. |
| """ |
| |
| chroot.VerifyOutsideChroot() |
| |
| args_output = GetCommandLineArgs() |
| |
| update_packages = [ |
| 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx', |
| 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind' |
| ] |
| |
| patch_metadata_file = 'PATCHES.json' |
| |
| svn_option = args_output.llvm_version |
| |
| git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption( |
| svn_option) |
| |
| # There is no need to run tryjobs when all the key parameters remain unchanged |
| # from last time. |
| |
| # If --last_tested is specified, check if the current run has the same |
| # arguments last time --last_tested is used. |
| if args_output.last_tested: |
| chroot_file_paths = chroot.GetChrootEbuildPaths(args_output.chroot_path, |
| update_packages) |
| arg_dict = { |
| 'svn_version': svn_version, |
| 'ebuilds': chroot_file_paths, |
| 'extra_cls': args_output.extra_change_lists, |
| } |
| if args_output.subparser_name in ('tryjobs', 'recipe'): |
| arg_dict['builders'] = args_output.builders |
| arg_dict['tryjob_options'] = args_output.options |
| if UnchangedSinceLastRun(args_output.last_tested, arg_dict): |
| print('svn version (%d) matches the last tested svn version in %s' % |
| (svn_version, args_output.last_tested)) |
| return |
| |
| llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current |
| if args_output.is_llvm_next: |
| llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next |
| update_chromeos_llvm_hash.verbose = args_output.verbose |
| extra_commit_msg = None |
| if args_output.subparser_name == 'cq': |
| cq_depend_msg = GetCQDependString(args_output.extra_change_lists) |
| if cq_depend_msg: |
| extra_commit_msg = cq_depend_msg |
| cq_trybot_msg = GetCQIncludeTrybotsString(args_output.cq_trybot) |
| if cq_trybot_msg: |
| extra_commit_msg += cq_trybot_msg |
| |
| change_list = update_chromeos_llvm_hash.UpdatePackages( |
| update_packages, |
| llvm_variant, |
| git_hash, |
| svn_version, |
| args_output.chroot_path, |
| patch_metadata_file, |
| failure_modes.FailureModes.DISABLE_PATCHES, |
| svn_option, |
| extra_commit_msg=extra_commit_msg) |
| |
| AddReviewers(change_list.cl_number, args_output.reviewers, |
| args_output.chroot_path) |
| |
| print('Successfully updated packages to %d' % svn_version) |
| print('Gerrit URL: %s' % change_list.url) |
| print('Change list number: %d' % change_list.cl_number) |
| |
| if args_output.subparser_name == 'tryjobs': |
| tests = RunTryJobs(change_list.cl_number, args_output.extra_change_lists, |
| args_output.options, args_output.builders, |
| args_output.chroot_path) |
| print('Tests:') |
| for test in tests: |
| print(test) |
| elif args_output.subparser_name == 'recipe': |
| tests = StartRecipeBuilders(change_list.cl_number, |
| args_output.extra_change_lists, |
| args_output.options, args_output.builders, |
| args_output.chroot_path) |
| print('Tests:') |
| for test in tests: |
| print(test) |
| |
| else: |
| StartCQDryRun(change_list.cl_number, args_output.extra_change_lists, |
| args_output.chroot_path) |
| |
| # If --last_tested is specified, record the arguments used |
| if args_output.last_tested: |
| with open(args_output.last_tested, 'w') as f: |
| json.dump(arg_dict, f, indent=2) |
| |
| |
| if __name__ == '__main__': |
| main() |