1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2019 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Runs a tryjob/tryjobs after updating the packages.""" 8 9from __future__ import print_function 10 11import argparse 12import datetime 13import json 14import os 15import subprocess 16 17import chroot 18import failure_modes 19import get_llvm_hash 20import update_chromeos_llvm_hash 21 22VALID_CQ_TRYBOTS = ['llvm', 'llvm-next', 'llvm-tot'] 23 24 25def GetCommandLineArgs(): 26 """Parses the command line for the command line arguments. 27 28 Returns: 29 The log level to use when retrieving the LLVM hash or google3 LLVM version, 30 the chroot path to use for executing chroot commands, 31 a list of a package or packages to update their LLVM next hash, 32 and the LLVM version to use when retrieving the LLVM hash. 33 """ 34 35 # Default path to the chroot if a path is not specified. 36 cros_root = os.path.expanduser('~') 37 cros_root = os.path.join(cros_root, 'chromiumos') 38 39 # Create parser and add optional command-line arguments. 40 parser = argparse.ArgumentParser( 41 description='Update an LLVM hash of packages and run tests.') 42 43 # Add argument for other change lists that want to run alongside the tryjob 44 # which has a change list of updating a package's git hash. 45 parser.add_argument( 46 '--extra_change_lists', 47 type=int, 48 nargs='+', 49 default=[], 50 help='change lists that would like to be run alongside the change list ' 51 'of updating the packages') 52 53 # Add argument for a specific chroot path. 54 parser.add_argument( 55 '--chroot_path', 56 default=cros_root, 57 help='the path to the chroot (default: %(default)s)') 58 59 # Add argument to choose between llvm and llvm-next. 60 parser.add_argument( 61 '--is_llvm_next', 62 action='store_true', 63 help='which llvm hash to update. Update LLVM_NEXT_HASH if specified. ' 64 'Otherwise, update LLVM_HASH') 65 66 # Add argument for the absolute path to the file that contains information on 67 # the previous tested svn version. 68 parser.add_argument( 69 '--last_tested', 70 help='the absolute path to the file that contains the last tested ' 71 'arguments.') 72 73 # Add argument for the LLVM version to use. 74 parser.add_argument( 75 '--llvm_version', 76 type=get_llvm_hash.is_svn_option, 77 required=True, 78 help='which git hash of LLVM to find ' 79 '{google3, ToT, <svn_version>} ' 80 '(default: finds the git hash of the google3 LLVM ' 81 'version)') 82 83 # Add argument to add reviewers for the created CL. 84 parser.add_argument( 85 '--reviewers', 86 nargs='+', 87 default=[], 88 help='The reviewers for the package update changelist') 89 90 # Add argument for whether to display command contents to `stdout`. 91 parser.add_argument( 92 '--verbose', 93 action='store_true', 94 help='display contents of a command to the terminal ' 95 '(default: %(default)s)') 96 97 subparsers = parser.add_subparsers(dest='subparser_name') 98 subparser_names = [] 99 # Testing with the tryjobs. 100 tryjob_subparser = subparsers.add_parser('tryjobs') 101 subparser_names.append('tryjobs') 102 tryjob_subparser.add_argument( 103 '--builders', 104 required=True, 105 nargs='+', 106 default=[], 107 help='builders to use for the tryjob testing') 108 109 # Add argument for custom options for the tryjob. 110 tryjob_subparser.add_argument( 111 '--options', 112 required=False, 113 nargs='+', 114 default=[], 115 help='options to use for the tryjob testing') 116 117 # Testing with the recipe builders 118 recipe_subparser = subparsers.add_parser('recipe') 119 subparser_names.append('recipe') 120 recipe_subparser.add_argument( 121 '--options', 122 required=False, 123 nargs='+', 124 default=[], 125 help='options passed to the recipe builders') 126 127 recipe_subparser.add_argument( 128 '--builders', 129 required=True, 130 nargs='+', 131 default=[], 132 help='recipe builders to launch') 133 134 # Testing with CQ. 135 cq_subparser = subparsers.add_parser('cq') 136 subparser_names.append('cq') 137 138 # Add argument for specify a cq trybot to test along with other cq builders 139 # e.g. llvm, llvm-next or llvm-tot 140 cq_subparser.add_argument( 141 '--cq_trybot', 142 choices=VALID_CQ_TRYBOTS, 143 help='include the trybot to test together with other cq builders ' 144 'available: %(choices)s') 145 146 args_output = parser.parse_args() 147 148 if args_output.subparser_name not in subparser_names: 149 parser.error('one of %s must be specified' % subparser_names) 150 151 return args_output 152 153 154def UnchangedSinceLastRun(last_tested_file, arg_dict): 155 """Gets the arguments used for last run 156 157 Args: 158 last_tested_file: The absolute path to the file that contains the 159 arguments for the last run. 160 arg_dict: The arguments used for this run. 161 162 Returns: 163 Return true if the arguments used for last run exist and are the 164 same as the arguments used for this run. Otherwise return false. 165 """ 166 167 if not last_tested_file: 168 return False 169 170 # Get the last tested svn version if the file exists. 171 last_arg_dict = None 172 try: 173 with open(last_tested_file) as f: 174 last_arg_dict = json.load(f) 175 176 except (IOError, ValueError): 177 return False 178 179 return arg_dict == last_arg_dict 180 181 182def AddReviewers(cl, reviewers, chroot_path): 183 """Add reviewers for the created CL.""" 184 185 gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') 186 for reviewer in reviewers: 187 cmd = [gerrit_abs_path, 'reviewers', str(cl), reviewer] 188 189 subprocess.check_output(cmd) 190 191 192def AddLinksToCL(tests, cl, chroot_path): 193 """Adds the test link(s) to the CL as a comment.""" 194 195 # NOTE: Invoking `cros_sdk` does not make each tryjob link appear on its own 196 # line, so invoking the `gerrit` command directly instead of using `cros_sdk` 197 # to do it for us. 198 # 199 # FIXME: Need to figure out why `cros_sdk` does not add each tryjob link as a 200 # newline. 201 gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') 202 203 links = ['Started the following tests:'] 204 links.extend(test['link'] for test in tests) 205 206 add_message_cmd = [gerrit_abs_path, 'message', str(cl), '\n'.join(links)] 207 208 subprocess.check_output(add_message_cmd) 209 210 211# Testing with tryjobs 212def GetCurrentTimeInUTC(): 213 """Returns the current time via `datetime.datetime.utcnow()`.""" 214 return datetime.datetime.utcnow() 215 216 217def GetTryJobCommand(change_list, extra_change_lists, options, builder): 218 """Constructs the 'tryjob' command. 219 220 Args: 221 change_list: The CL obtained from updating the packages. 222 extra_change_lists: Extra change lists that would like to be run alongside 223 the change list of updating the packages. 224 options: Options to be passed into the tryjob command. 225 builder: The builder to be passed into the tryjob command. 226 227 Returns: 228 The 'tryjob' command with the change list of updating the packages and 229 any extra information that was passed into the command line. 230 """ 231 232 tryjob_cmd = ['cros', 'tryjob', '--yes', '--json', '-g', '%d' % change_list] 233 234 if extra_change_lists: 235 for extra_cl in extra_change_lists: 236 tryjob_cmd.extend(['-g', '%d' % extra_cl]) 237 238 if options: 239 tryjob_cmd.extend('--%s' % option for option in options) 240 241 tryjob_cmd.append(builder) 242 243 return tryjob_cmd 244 245 246def RunTryJobs(cl_number, extra_change_lists, options, builders, chroot_path): 247 """Runs a tryjob/tryjobs. 248 249 Args: 250 cl_number: The CL created by updating the packages. 251 extra_change_lists: Any extra change lists that would run alongside the CL 252 that was created by updating the packages ('cl_number'). 253 options: Any options to be passed into the 'tryjob' command. 254 builders: All the builders to run the 'tryjob' with. 255 chroot_path: The absolute path to the chroot. 256 257 Returns: 258 A list that contains stdout contents of each tryjob, where stdout is 259 information (a hashmap) about the tryjob. The hashmap also contains stderr 260 if there was an error when running a tryjob. 261 262 Raises: 263 ValueError: Failed to submit a tryjob. 264 """ 265 266 # Contains the results of each builder. 267 tests = [] 268 269 # Run tryjobs with the change list number obtained from updating the 270 # packages and append additional changes lists and options obtained from the 271 # command line. 272 for builder in builders: 273 cmd = GetTryJobCommand(cl_number, extra_change_lists, options, builder) 274 275 out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8') 276 277 test_output = json.loads(out) 278 279 tests.append({ 280 'launch_time': str(GetCurrentTimeInUTC()), 281 'link': str(test_output[0]['url']), 282 'buildbucket_id': int(test_output[0]['buildbucket_id']), 283 'extra_cls': extra_change_lists, 284 'options': options, 285 'builder': [builder] 286 }) 287 288 AddLinksToCL(tests, cl_number, chroot_path) 289 290 return tests 291 292 293def StartRecipeBuilders(cl_number, extra_change_lists, options, builders, 294 chroot_path): 295 """Launch recipe builders. 296 297 Args: 298 cl_number: The CL created by updating the packages. 299 extra_change_lists: Any extra change lists that would run alongside the CL 300 that was created by updating the packages ('cl_number'). 301 options: Any options to be passed into the 'tryjob' command. 302 builders: All the builders to run the 'tryjob' with. 303 chroot_path: The absolute path to the chroot. 304 305 Returns: 306 A list that contains stdout contents of each builder, where stdout is 307 information (a hashmap) about the tryjob. The hashmap also contains stderr 308 if there was an error when running a tryjob. 309 310 Raises: 311 ValueError: Failed to start a builder. 312 """ 313 314 # Contains the results of each builder. 315 tests = [] 316 317 # Launch a builders with the change list number obtained from updating the 318 # packages and append additional changes lists and options obtained from the 319 # command line. 320 for builder in builders: 321 cmd = ['bb', 'add', '-json'] 322 323 if cl_number: 324 cmd.extend(['-cl', 'crrev.com/c/%d' % cl_number]) 325 326 if extra_change_lists: 327 for cl in extra_change_lists: 328 cmd.extend(['-cl', 'crrev.com/c/%d' % cl]) 329 330 if options: 331 cmd.extend(options) 332 333 cmd.append(builder) 334 335 out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8') 336 337 test_output = json.loads(out) 338 339 tests.append({ 340 'launch_time': test_output['createTime'], 341 'link': 'http://ci.chromium.org/b/%s' % test_output['id'], 342 'buildbucket_id': test_output['id'], 343 'extra_cls': extra_change_lists, 344 'options': options, 345 'builder': [builder] 346 }) 347 348 AddLinksToCL(tests, cl_number, chroot_path) 349 350 return tests 351 352 353# Testing with CQ 354def GetCQDependString(dependent_cls): 355 """Get CQ dependency string e.g. `Cq-Depend: chromium:MM, chromium:NN`.""" 356 357 if not dependent_cls: 358 return None 359 360 # Cq-Depend must start a new paragraph prefixed with "Cq-Depend". 361 return '\nCq-Depend: ' + ', '.join(('chromium:%s' % i) for i in dependent_cls) 362 363 364def GetCQIncludeTrybotsString(trybot): 365 """Get Cq-Include-Trybots string, for more llvm testings""" 366 367 if not trybot: 368 return None 369 370 if trybot not in VALID_CQ_TRYBOTS: 371 raise ValueError('%s is not a valid llvm trybot' % trybot) 372 373 # Cq-Include-Trybots must start a new paragraph prefixed 374 # with "Cq-Include-Trybots". 375 return '\nCq-Include-Trybots:chromeos/cq:cq-%s-orchestrator' % trybot 376 377 378def StartCQDryRun(cl, dependent_cls, chroot_path): 379 """Start CQ dry run for the changelist and dependencies.""" 380 381 gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') 382 383 cl_list = [cl] 384 cl_list.extend(dependent_cls) 385 386 for changes in cl_list: 387 cq_dry_run_cmd = [gerrit_abs_path, 'label-cq', str(changes), '1'] 388 389 subprocess.check_output(cq_dry_run_cmd) 390 391 392def main(): 393 """Updates the packages' LLVM hash and run tests. 394 395 Raises: 396 AssertionError: The script was run inside the chroot. 397 """ 398 399 chroot.VerifyOutsideChroot() 400 401 args_output = GetCommandLineArgs() 402 403 update_packages = [ 404 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx', 405 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind' 406 ] 407 408 patch_metadata_file = 'PATCHES.json' 409 410 svn_option = args_output.llvm_version 411 412 git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption( 413 svn_option) 414 415 # There is no need to run tryjobs when all the key parameters remain unchanged 416 # from last time. 417 418 # If --last_tested is specified, check if the current run has the same 419 # arguments last time --last_tested is used. 420 if args_output.last_tested: 421 chroot_file_paths = chroot.GetChrootEbuildPaths(args_output.chroot_path, 422 update_packages) 423 arg_dict = { 424 'svn_version': svn_version, 425 'ebuilds': chroot_file_paths, 426 'extra_cls': args_output.extra_change_lists, 427 } 428 if args_output.subparser_name in ('tryjobs', 'recipe'): 429 arg_dict['builders'] = args_output.builders 430 arg_dict['tryjob_options'] = args_output.options 431 if UnchangedSinceLastRun(args_output.last_tested, arg_dict): 432 print('svn version (%d) matches the last tested svn version in %s' % 433 (svn_version, args_output.last_tested)) 434 return 435 436 llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current 437 if args_output.is_llvm_next: 438 llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next 439 update_chromeos_llvm_hash.verbose = args_output.verbose 440 extra_commit_msg = None 441 if args_output.subparser_name == 'cq': 442 cq_depend_msg = GetCQDependString(args_output.extra_change_lists) 443 if cq_depend_msg: 444 extra_commit_msg = cq_depend_msg 445 cq_trybot_msg = GetCQIncludeTrybotsString(args_output.cq_trybot) 446 if cq_trybot_msg: 447 extra_commit_msg += cq_trybot_msg 448 449 change_list = update_chromeos_llvm_hash.UpdatePackages( 450 update_packages, 451 llvm_variant, 452 git_hash, 453 svn_version, 454 args_output.chroot_path, 455 patch_metadata_file, 456 failure_modes.FailureModes.DISABLE_PATCHES, 457 svn_option, 458 extra_commit_msg=extra_commit_msg) 459 460 AddReviewers(change_list.cl_number, args_output.reviewers, 461 args_output.chroot_path) 462 463 print('Successfully updated packages to %d' % svn_version) 464 print('Gerrit URL: %s' % change_list.url) 465 print('Change list number: %d' % change_list.cl_number) 466 467 if args_output.subparser_name == 'tryjobs': 468 tests = RunTryJobs(change_list.cl_number, args_output.extra_change_lists, 469 args_output.options, args_output.builders, 470 args_output.chroot_path) 471 print('Tests:') 472 for test in tests: 473 print(test) 474 elif args_output.subparser_name == 'recipe': 475 tests = StartRecipeBuilders( 476 change_list.cl_number, args_output.extra_change_lists, 477 args_output.options, args_output.builders, args_output.chroot_path) 478 print('Tests:') 479 for test in tests: 480 print(test) 481 482 else: 483 StartCQDryRun(change_list.cl_number, args_output.extra_change_lists, 484 args_output.chroot_path) 485 486 # If --last_tested is specified, record the arguments used 487 if args_output.last_tested: 488 with open(args_output.last_tested, 'w') as f: 489 json.dump(arg_dict, f, indent=2) 490 491 492if __name__ == '__main__': 493 main() 494