1#!/usr/bin/env python3 2# Copyright 2019 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Runs a tryjob/tryjobs after updating the packages.""" 7 8import argparse 9import datetime 10import json 11import os 12from pathlib import Path 13import subprocess 14from typing import Any, Dict, Iterable, List, Optional, Union 15 16import chroot 17import failure_modes 18import get_llvm_hash 19import update_chromeos_llvm_hash 20 21 22VALID_CQ_TRYBOTS = ("llvm", "llvm-next") 23 24 25def GetCommandLineArgs() -> argparse.Namespace: 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 30 version, 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 44 # Add argument for other change lists that want to run alongside the tryjob 45 # which has a change list of updating a package's git hash. 46 parser.add_argument( 47 "--extra_change_lists", 48 type=int, 49 nargs="+", 50 default=[], 51 help="change lists that would like to be run alongside the change list " 52 "of updating the packages", 53 ) 54 55 # Add argument for a specific chroot path. 56 parser.add_argument( 57 "--chromeos_path", 58 default=cros_root, 59 help="the path to the ChromeOS tree (default: %(default)s)", 60 ) 61 62 # Add argument for a specific chroot path. 63 parser.add_argument( 64 "--chroot_name", 65 default="chroot", 66 help=""" 67 the name of the chroot to use in the CrOS checkout. Defaults to 68 'chroot'. 69 """, 70 ) 71 72 parser.add_argument( 73 "--chroot_out", 74 help=""" 75 the name of the chroot to use in the CrOS checkout. Defaults to 76 'out' if the chroot's name is 'chroot'; otherwise, defaults to 77 '${chroot_name}_out'. 78 """, 79 ) 80 81 # Add argument to choose between llvm and llvm-next. 82 parser.add_argument( 83 "--is_llvm_next", 84 action="store_true", 85 help="which llvm hash to update. Update LLVM_NEXT_HASH if specified. " 86 "Otherwise, update LLVM_HASH", 87 ) 88 89 # Add argument for the absolute path to the file that contains information 90 # on the previous tested svn version. 91 parser.add_argument( 92 "--last_tested", 93 help="the absolute path to the file that contains the last tested " 94 "arguments.", 95 ) 96 97 # Add argument for the LLVM version to use. 98 parser.add_argument( 99 "--llvm_version", 100 type=get_llvm_hash.IsSvnOption, 101 required=True, 102 help="which git hash of LLVM to find " 103 "{google3, ToT, <svn_version>} " 104 "(default: finds the git hash of the google3 LLVM " 105 "version)", 106 ) 107 108 # Add argument to add reviewers for the created CL. 109 parser.add_argument( 110 "--reviewers", 111 nargs="+", 112 default=[], 113 help="The reviewers for the package update changelist", 114 ) 115 116 subparsers = parser.add_subparsers(dest="subparser_name") 117 subparser_names = [] 118 # Testing with the tryjobs. 119 tryjob_subparser = subparsers.add_parser("tryjobs") 120 subparser_names.append("tryjobs") 121 tryjob_subparser.add_argument( 122 "--builders", 123 required=True, 124 nargs="+", 125 default=[], 126 help="builders to use for the tryjob testing", 127 ) 128 129 # Add argument for custom options for the tryjob. 130 tryjob_subparser.add_argument( 131 "--options", 132 required=False, 133 nargs="+", 134 default=[], 135 help="options to use for the tryjob testing", 136 ) 137 138 # Testing with the recipe builders 139 recipe_subparser = subparsers.add_parser("recipe") 140 subparser_names.append("recipe") 141 recipe_subparser.add_argument( 142 "--options", 143 required=False, 144 nargs="+", 145 default=[], 146 help="options passed to the recipe builders", 147 ) 148 149 recipe_subparser.add_argument( 150 "--builders", 151 required=True, 152 nargs="+", 153 default=[], 154 help="recipe builders to launch", 155 ) 156 157 # Testing with CQ. 158 cq_subparser = subparsers.add_parser("cq") 159 subparser_names.append("cq") 160 161 # Add argument for specify a cq trybot to test along with other cq builders 162 # e.g. llvm, llvm-next or llvm-tot 163 cq_subparser.add_argument( 164 "--cq_trybot", 165 choices=VALID_CQ_TRYBOTS, 166 help="include the trybot to test together with other cq builders " 167 "available: %(choices)s", 168 ) 169 170 args_output = parser.parse_args() 171 172 if args_output.subparser_name not in subparser_names: 173 parser.error("one of %s must be specified" % subparser_names) 174 175 if not args_output.chroot_out: 176 chroot_name = args_output.chroot_name 177 if chroot_name == "chroot": 178 out = "out" 179 else: 180 out = f"{chroot_name}_out" 181 args_output.chroot_out = out 182 183 return args_output 184 185 186def UnchangedSinceLastRun( 187 last_tested_file: Optional[Union[Path, str]], 188 arg_dict: Dict, 189) -> bool: 190 """Gets the arguments used for last run 191 192 Args: 193 last_tested_file: The absolute path to the file that contains the 194 arguments for the last run. 195 arg_dict: The arguments used for this run. 196 197 Returns: 198 Return true if the arguments used for last run exist and are the 199 same as the arguments used for this run. Otherwise return false. 200 """ 201 202 if not last_tested_file: 203 return False 204 205 # Get the last tested svn version if the file exists. 206 last_arg_dict = None 207 try: 208 with open(last_tested_file, encoding="utf-8") as f: 209 last_arg_dict = json.load(f) 210 211 except (IOError, ValueError): 212 return False 213 214 return arg_dict == last_arg_dict 215 216 217def AddReviewers( 218 cl: int, 219 reviewers: Iterable[str], 220 chromeos_path: Union[Path, str], 221) -> None: 222 """Add reviewers for the created CL. 223 224 Args: 225 cl: The CL number to add reviewers to. 226 reviewers: Email addresses of reviewers to add. 227 chromeos_path: The absolute path to the chromeos tree. 228 """ 229 230 gerrit_abs_path = os.path.join(chromeos_path, "chromite/bin/gerrit") 231 for reviewer in reviewers: 232 cmd = [gerrit_abs_path, "reviewers", str(cl), reviewer] 233 234 subprocess.check_output(cmd) 235 236 237def AddLinksToCL( 238 tests: Iterable[Dict[str, Any]], 239 cl: int, 240 chromeos_path: Union[Path, str], 241) -> None: 242 """Adds the test link(s) to the CL as a comment. 243 244 Args: 245 tests: Links to the tests. 246 cl: The number of the CL to add the test links to. 247 chromeos_path: Absolute path to the chromeos tree. 248 """ 249 250 # NOTE: Invoking `cros_sdk` does not make each tryjob link appear on its 251 # own line, so invoking the `gerrit` command directly instead of using 252 # `cros_sdk` to do it for us. 253 # 254 # FIXME: Need to figure out why `cros_sdk` does not add each tryjob link as 255 # a newline. 256 gerrit_abs_path = os.path.join(chromeos_path, "chromite/bin/gerrit") 257 258 links = ["Started the following tests:"] 259 links.extend(test["link"] for test in tests) 260 261 add_message_cmd = [gerrit_abs_path, "message", str(cl), "\n".join(links)] 262 263 subprocess.check_output(add_message_cmd) 264 265 266# Testing with tryjobs 267def GetCurrentTimeInUTC() -> datetime.datetime: 268 """Returns the current time via `datetime.datetime.utcnow()`.""" 269 return datetime.datetime.utcnow() 270 271 272def GetTryJobCommand( 273 change_list: int, 274 extra_change_lists: Iterable[int], 275 options: Iterable[str], 276 builder: str, 277) -> List[str]: 278 """Constructs the 'tryjob' command. 279 280 Args: 281 change_list: The CL obtained from updating the packages. 282 extra_change_lists: Extra change lists that would like to be run 283 alongside the change list of updating the packages. 284 options: Options to be passed into the tryjob command. 285 builder: The builder to be passed into the tryjob command. 286 287 Returns: 288 The 'tryjob' command with the change list of updating the packages and 289 any extra information that was passed into the command line. 290 """ 291 292 tryjob_cmd = ["cros", "tryjob", "--yes", "--json", "-g", "%d" % change_list] 293 294 if extra_change_lists: 295 for extra_cl in extra_change_lists: 296 tryjob_cmd.extend(["-g", "%d" % extra_cl]) 297 298 if options: 299 tryjob_cmd.extend("--%s" % option for option in options) 300 301 tryjob_cmd.append(builder) 302 303 return tryjob_cmd 304 305 306def RunTryJobs( 307 cl_number: int, 308 extra_change_lists: List[int], 309 options: List[str], 310 builders: Iterable[str], 311 chromeos_path: Union[Path, str], 312) -> List[Dict]: 313 """Runs a tryjob/tryjobs. 314 315 Args: 316 cl_number: The CL created by updating the packages. 317 extra_change_lists: Any extra change lists that would run alongside the 318 CL that was created by updating the packages ('cl_number'). 319 options: Any options to be passed into the 'tryjob' command. 320 builders: All the builders to run the 'tryjob' with. 321 chromeos_path: The absolute path to the chromeos tree. 322 323 Returns: 324 A list that contains stdout contents of each tryjob, where stdout is 325 information (a hashmap) about the tryjob. The hashmap also contains 326 stderr if there was an error when running a tryjob. 327 328 Raises: 329 ValueError: Failed to submit a tryjob. 330 """ 331 332 # Contains the results of each builder. 333 tests = [] 334 335 # Run tryjobs with the change list number obtained from updating the 336 # packages and append additional changes lists and options obtained from the 337 # command line. 338 for builder in builders: 339 cmd = GetTryJobCommand(cl_number, extra_change_lists, options, builder) 340 341 out = subprocess.check_output(cmd, cwd=chromeos_path, encoding="utf-8") 342 343 test_output = json.loads(out) 344 345 buildbucket_id = int(test_output[0]["id"]) 346 347 tests.append( 348 { 349 "launch_time": str(GetCurrentTimeInUTC()), 350 "link": "http://ci.chromium.org/b/%s" % buildbucket_id, 351 "buildbucket_id": buildbucket_id, 352 "extra_cls": extra_change_lists, 353 "options": options, 354 "builder": [builder], 355 } 356 ) 357 358 AddLinksToCL(tests, cl_number, chromeos_path) 359 360 return tests 361 362 363def StartRecipeBuilders( 364 cl_number: int, 365 extra_change_lists: List[int], 366 options: List[str], 367 builders: List[str], 368 chromeos_path: Union[Path, str], 369) -> List[Dict]: 370 """Launch recipe builders. 371 372 Args: 373 cl_number: The CL created by updating the packages. 374 extra_change_lists: Any extra change lists that would run alongside the 375 CL that was created by updating the packages ('cl_number'). 376 options: Any options to be passed into the 'tryjob' command. 377 builders: All the builders to run the 'tryjob' with. 378 chromeos_path: The absolute path to the chromeos tree. 379 380 Returns: 381 A list that contains stdout contents of each builder, where stdout is 382 information (a hashmap) about the tryjob. The hashmap also contains 383 stderr if there was an error when running a tryjob. 384 385 Raises: 386 ValueError: Failed to start a builder. 387 """ 388 389 # Contains the results of each builder. 390 tests = [] 391 392 # Launch a builders with the change list number obtained from updating the 393 # packages and append additional changes lists and options obtained from the 394 # command line. 395 for builder in builders: 396 cmd = ["bb", "add", "-json"] 397 398 if cl_number: 399 cmd.extend(["-cl", "crrev.com/c/%d" % cl_number]) 400 401 if extra_change_lists: 402 for cl in extra_change_lists: 403 cmd.extend(["-cl", "crrev.com/c/%d" % cl]) 404 405 if options: 406 cmd.extend(options) 407 408 cmd.append(builder) 409 410 out = subprocess.check_output(cmd, cwd=chromeos_path, encoding="utf-8") 411 412 test_output = json.loads(out) 413 414 tests.append( 415 { 416 "launch_time": test_output["createTime"], 417 "link": "http://ci.chromium.org/b/%s" % test_output["id"], 418 "buildbucket_id": test_output["id"], 419 "extra_cls": extra_change_lists, 420 "options": options, 421 "builder": [builder], 422 } 423 ) 424 425 AddLinksToCL(tests, cl_number, chromeos_path) 426 427 return tests 428 429 430# Testing with CQ 431def GetCQDependString(dependent_cls: List[int]) -> Optional[str]: 432 """Get CQ dependency string e.g. `Cq-Depend: chromium:MM, chromium:NN`.""" 433 434 if not dependent_cls: 435 return None 436 437 # Cq-Depend must start a new paragraph prefixed with "Cq-Depend". 438 return "Cq-Depend: " + ", ".join(f"chromium:{x}" for x in dependent_cls) 439 440 441def GetCQIncludeTrybotsString(trybot: Optional[str]) -> Optional[str]: 442 """Get Cq-Include-Trybots string, for more llvm testings""" 443 444 if not trybot: 445 return None 446 447 if trybot not in VALID_CQ_TRYBOTS: 448 raise ValueError("%s is not a valid llvm trybot" % trybot) 449 450 # Cq-Include-Trybots must start a new paragraph prefixed 451 # with "Cq-Include-Trybots". 452 return "Cq-Include-Trybots:chromeos/cq:cq-%s-orchestrator" % trybot 453 454 455def StartCQDryRun( 456 cl: int, 457 dependent_cls: List[int], 458 chromeos_path: Union[Path, str], 459) -> None: 460 """Start CQ dry run for the changelist and dependencies.""" 461 462 gerrit_abs_path = os.path.join(chromeos_path, "chromite/bin/gerrit") 463 464 cl_list = [cl] 465 cl_list.extend(dependent_cls) 466 467 for changes in cl_list: 468 cq_dry_run_cmd = [gerrit_abs_path, "label-cq", str(changes), "1"] 469 470 subprocess.check_output(cq_dry_run_cmd) 471 472 473def main(): 474 """Updates the packages' LLVM hash and run tests. 475 476 Raises: 477 AssertionError: The script was run inside the chroot. 478 """ 479 480 chroot.VerifyOutsideChroot() 481 482 args_output = GetCommandLineArgs() 483 484 chroot.VerifyChromeOSRoot(args_output.chromeos_path) 485 486 svn_option = args_output.llvm_version 487 488 git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption( 489 svn_option 490 ) 491 492 # There is no need to run tryjobs when all the key parameters remain 493 # unchanged from last time. 494 495 # If --last_tested is specified, check if the current run has the same 496 # arguments last time --last_tested is used. 497 if args_output.last_tested: 498 chroot_file_paths = chroot.GetChrootEbuildPaths( 499 args_output.chromeos_path, 500 update_chromeos_llvm_hash.DEFAULT_PACKAGES, 501 args_output.chroot_name, 502 args_output.chroot_out, 503 ) 504 arg_dict = { 505 "svn_version": svn_version, 506 "ebuilds": chroot_file_paths, 507 "extra_cls": args_output.extra_change_lists, 508 } 509 if args_output.subparser_name in ("tryjobs", "recipe"): 510 arg_dict["builders"] = args_output.builders 511 arg_dict["tryjob_options"] = args_output.options 512 if UnchangedSinceLastRun(args_output.last_tested, arg_dict): 513 print( 514 "svn version (%d) matches the last tested svn version in %s" 515 % (svn_version, args_output.last_tested) 516 ) 517 return 518 519 llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current 520 if args_output.is_llvm_next: 521 llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next 522 523 extra_commit_msg_lines = [] 524 if args_output.subparser_name == "cq": 525 footers = [] 526 cq_depend_msg = GetCQDependString(args_output.extra_change_lists) 527 if cq_depend_msg: 528 footers.append(cq_depend_msg) 529 cq_trybot_msg = GetCQIncludeTrybotsString(args_output.cq_trybot) 530 if cq_trybot_msg: 531 footers.append(cq_trybot_msg) 532 533 # We want a single blank line before any of these, so Git properly 534 # interprets them as a footer. 535 if footers: 536 extra_commit_msg_lines.append("") 537 extra_commit_msg_lines += footers 538 539 change_list = update_chromeos_llvm_hash.UpdatePackages( 540 packages=update_chromeos_llvm_hash.DEFAULT_PACKAGES, 541 manifest_packages=[], 542 llvm_variant=llvm_variant, 543 git_hash=git_hash, 544 svn_version=svn_version, 545 chroot_opts=update_chromeos_llvm_hash.ChrootOpts( 546 chromeos_root=Path(args_output.chromeos_path), 547 chroot_name=args_output.chroot_name, 548 out_name=args_output.chroot_out, 549 ), 550 mode=failure_modes.FailureModes.DISABLE_PATCHES, 551 git_hash_source=svn_option, 552 extra_commit_msg_lines=extra_commit_msg_lines, 553 # b/331607705: set WIP on these changes, so the code-review-nudger bot 554 # doesn't ping them. 555 wip=True, 556 ) 557 558 AddReviewers( 559 change_list.cl_number, args_output.reviewers, args_output.chromeos_path 560 ) 561 562 print("Successfully updated packages to %d" % svn_version) 563 print("Gerrit URL: %s" % change_list.url) 564 print("Change list number: %d" % change_list.cl_number) 565 566 if args_output.subparser_name == "tryjobs": 567 tests = RunTryJobs( 568 change_list.cl_number, 569 args_output.extra_change_lists, 570 args_output.options, 571 args_output.builders, 572 args_output.chromeos_path, 573 ) 574 print("Tests:") 575 for test in tests: 576 print(test) 577 elif args_output.subparser_name == "recipe": 578 tests = StartRecipeBuilders( 579 change_list.cl_number, 580 args_output.extra_change_lists, 581 args_output.options, 582 args_output.builders, 583 args_output.chromeos_path, 584 ) 585 print("Tests:") 586 for test in tests: 587 print(test) 588 589 else: 590 StartCQDryRun( 591 change_list.cl_number, 592 args_output.extra_change_lists, 593 args_output.chromeos_path, 594 ) 595 596 # If --last_tested is specified, record the arguments used 597 if args_output.last_tested: 598 with open(args_output.last_tested, "w", encoding="utf-8") as f: 599 json.dump(arg_dict, f, indent=2) 600 601 602if __name__ == "__main__": 603 main() 604