1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2020 The ChromiumOS Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Get an upstream patch to LLVM's PATCHES.json.""" 8 9import argparse 10import dataclasses 11from datetime import datetime 12import json 13import logging 14import os 15from pathlib import Path 16import shlex 17import subprocess 18import sys 19import typing as t 20 21import chroot 22import get_llvm_hash 23import git 24import git_llvm_rev 25import patch_utils 26import update_chromeos_llvm_hash 27 28 29__DOC_EPILOGUE = """ 30Example Usage: 31 get_upstream_patch --chroot_path ~/chromiumos --platform chromiumos \ 32--sha 1234567 --sha 890abdc 33""" 34 35 36class CherrypickError(ValueError): 37 """A ValueError that highlights the cherry-pick has been seen before""" 38 39 40class CherrypickVersionError(ValueError): 41 """A ValueError that highlights the cherry-pick is before the start_sha""" 42 43 44class PatchApplicationError(ValueError): 45 """A ValueError indicating that a test patch application was unsuccessful""" 46 47 48def validate_patch_application( 49 llvm_dir: Path, svn_version: int, patches_json_fp: Path, patch_props 50): 51 52 start_sha = get_llvm_hash.GetGitHashFrom(llvm_dir, svn_version) 53 subprocess.run(["git", "-C", llvm_dir, "checkout", start_sha], check=True) 54 55 predecessor_apply_results = patch_utils.apply_all_from_json( 56 svn_version, llvm_dir, patches_json_fp, continue_on_failure=True 57 ) 58 59 if predecessor_apply_results.failed_patches: 60 logging.error("Failed to apply patches from PATCHES.json:") 61 for p in predecessor_apply_results.failed_patches: 62 logging.error(f"Patch title: {p.title()}") 63 raise PatchApplicationError("Failed to apply patch from PATCHES.json") 64 65 patch_entry = patch_utils.PatchEntry.from_dict( 66 patches_json_fp.parent, patch_props 67 ) 68 test_apply_result = patch_entry.test_apply(Path(llvm_dir)) 69 70 if not test_apply_result: 71 logging.error("Could not apply requested patch") 72 logging.error(test_apply_result.failure_info()) 73 raise PatchApplicationError( 74 f'Failed to apply patch: {patch_props["metadata"]["title"]}' 75 ) 76 77 78def add_patch( 79 patches_json_path: str, 80 patches_dir: str, 81 relative_patches_dir: str, 82 start_version: git_llvm_rev.Rev, 83 llvm_dir: str, 84 rev: t.Union[git_llvm_rev.Rev, str], 85 sha: str, 86 package: str, 87 platforms: t.List[str], 88): 89 """Gets the start and end intervals in 'json_file'. 90 91 Args: 92 patches_json_path: The absolute path to PATCHES.json. 93 patches_dir: The aboslute path to the directory patches are in. 94 relative_patches_dir: The relative path to PATCHES.json. 95 start_version: The base LLVM revision this patch applies to. 96 llvm_dir: The path to LLVM checkout. 97 rev: An LLVM revision (git_llvm_rev.Rev) for a cherrypicking, or a 98 differential revision (str) otherwise. 99 sha: The LLVM git sha that corresponds to the patch. For differential 100 revisions, the git sha from the local commit created by 'arc patch' 101 is used. 102 package: The LLVM project name this patch applies to. 103 platforms: List of platforms this patch applies to. 104 105 Raises: 106 CherrypickError: A ValueError that highlights the cherry-pick has been 107 seen before. 108 CherrypickRangeError: A ValueError that's raised when the given patch 109 is from before the start_sha. 110 """ 111 112 is_cherrypick = isinstance(rev, git_llvm_rev.Rev) 113 if is_cherrypick: 114 file_name = f"{sha}.patch" 115 else: 116 file_name = f"{rev}.patch" 117 rel_patch_path = os.path.join(relative_patches_dir, file_name) 118 119 # Check that we haven't grabbed a patch range that's nonsensical. 120 end_vers = rev.number if isinstance(rev, git_llvm_rev.Rev) else None 121 if end_vers is not None and end_vers <= start_version.number: 122 raise CherrypickVersionError( 123 f"`until` version {end_vers} is earlier or equal to" 124 f" `from` version {start_version.number} for patch" 125 f" {rel_patch_path}" 126 ) 127 128 with open(patches_json_path, encoding="utf-8") as f: 129 patches_json = json.load(f) 130 131 for p in patches_json: 132 rel_path = p["rel_patch_path"] 133 if rel_path == rel_patch_path: 134 raise CherrypickError( 135 f"Patch at {rel_path} already exists in PATCHES.json" 136 ) 137 if is_cherrypick: 138 if sha in rel_path: 139 logging.warning( 140 "Similarly-named patch already exists in PATCHES.json: %r", 141 rel_path, 142 ) 143 144 with open(os.path.join(patches_dir, file_name), "wb") as f: 145 cmd = ["git", "show", sha] 146 # Only apply the part of the patch that belongs to this package, expect 147 # LLVM. This is because some packages are built with LLVM ebuild on X86 but 148 # not on the other architectures. e.g. compiler-rt. Therefore always apply 149 # the entire patch to LLVM ebuild as a workaround. 150 if package != "llvm": 151 cmd.append(package_to_project(package)) 152 subprocess.check_call(cmd, stdout=f, cwd=llvm_dir) 153 154 commit_subject = subprocess.check_output( 155 ["git", "log", "-n1", "--format=%s", sha], 156 cwd=llvm_dir, 157 encoding="utf-8", 158 ) 159 patch_props = { 160 "rel_patch_path": rel_patch_path, 161 "metadata": { 162 "title": commit_subject.strip(), 163 "info": [], 164 }, 165 "platforms": sorted(platforms), 166 "version_range": { 167 "from": start_version.number, 168 "until": end_vers, 169 }, 170 } 171 172 with patch_utils.git_clean_context(Path(llvm_dir)): 173 validate_patch_application( 174 Path(llvm_dir), 175 start_version.number, 176 Path(patches_json_path), 177 patch_props, 178 ) 179 180 patches_json.append(patch_props) 181 182 temp_file = patches_json_path + ".tmp" 183 with open(temp_file, "w", encoding="utf-8") as f: 184 json.dump( 185 patches_json, f, indent=4, separators=(",", ": "), sort_keys=True 186 ) 187 f.write("\n") 188 os.rename(temp_file, patches_json_path) 189 190 191def parse_ebuild_for_assignment(ebuild_path: str, var_name: str) -> str: 192 # '_pre' filters the LLVM 9.0 ebuild, which we never want to target, from 193 # this list. 194 candidates = [ 195 x 196 for x in os.listdir(ebuild_path) 197 if x.endswith(".ebuild") and "_pre" in x 198 ] 199 200 if not candidates: 201 raise ValueError("No ebuilds found under %r" % ebuild_path) 202 203 ebuild = os.path.join(ebuild_path, max(candidates)) 204 with open(ebuild, encoding="utf-8") as f: 205 var_name_eq = var_name + "=" 206 for orig_line in f: 207 if not orig_line.startswith(var_name_eq): 208 continue 209 210 # We shouldn't see much variety here, so do the simplest thing possible. 211 line = orig_line[len(var_name_eq) :] 212 # Remove comments 213 line = line.split("#")[0] 214 # Remove quotes 215 line = shlex.split(line) 216 if len(line) != 1: 217 raise ValueError( 218 "Expected exactly one quoted value in %r" % orig_line 219 ) 220 return line[0].strip() 221 222 raise ValueError("No %s= line found in %r" % (var_name, ebuild)) 223 224 225# Resolves a git ref (or similar) to a LLVM SHA. 226def resolve_llvm_ref(llvm_dir: str, sha: str) -> str: 227 return subprocess.check_output( 228 ["git", "rev-parse", sha], 229 encoding="utf-8", 230 cwd=llvm_dir, 231 ).strip() 232 233 234# Get the package name of an LLVM project 235def project_to_package(project: str) -> str: 236 if project == "libunwind": 237 return "llvm-libunwind" 238 return project 239 240 241# Get the LLVM project name of a package 242def package_to_project(package: str) -> str: 243 if package == "llvm-libunwind": 244 return "libunwind" 245 return package 246 247 248# Get the LLVM projects change in the specifed sha 249def get_package_names(sha: str, llvm_dir: str) -> list: 250 paths = subprocess.check_output( 251 ["git", "show", "--name-only", "--format=", sha], 252 cwd=llvm_dir, 253 encoding="utf-8", 254 ).splitlines() 255 # Some LLVM projects are built by LLVM ebuild on X86, so always apply the 256 # patch to LLVM ebuild 257 packages = {"llvm"} 258 # Detect if there are more packages to apply the patch to 259 for path in paths: 260 package = project_to_package(path.split("/")[0]) 261 if package in ("compiler-rt", "libcxx", "libcxxabi", "llvm-libunwind"): 262 packages.add(package) 263 packages = list(sorted(packages)) 264 return packages 265 266 267def create_patch_for_packages( 268 packages: t.List[str], 269 symlinks: t.List[str], 270 start_rev: git_llvm_rev.Rev, 271 rev: t.Union[git_llvm_rev.Rev, str], 272 sha: str, 273 llvm_dir: str, 274 platforms: t.List[str], 275): 276 """Create a patch and add its metadata for each package""" 277 for package, symlink in zip(packages, symlinks): 278 symlink_dir = os.path.dirname(symlink) 279 patches_json_path = os.path.join(symlink_dir, "files/PATCHES.json") 280 relative_patches_dir = "cherry" if package == "llvm" else "" 281 patches_dir = os.path.join(symlink_dir, "files", relative_patches_dir) 282 logging.info("Getting %s (%s) into %s", rev, sha, package) 283 add_patch( 284 patches_json_path, 285 patches_dir, 286 relative_patches_dir, 287 start_rev, 288 llvm_dir, 289 rev, 290 sha, 291 package, 292 platforms=platforms, 293 ) 294 295 296def make_cl( 297 symlinks_to_uprev: t.List[str], 298 llvm_symlink_dir: str, 299 branch: str, 300 commit_messages: t.List[str], 301 reviewers: t.Optional[t.List[str]], 302 cc: t.Optional[t.List[str]], 303): 304 symlinks_to_uprev = sorted(set(symlinks_to_uprev)) 305 for symlink in symlinks_to_uprev: 306 update_chromeos_llvm_hash.UprevEbuildSymlink(symlink) 307 subprocess.check_output( 308 ["git", "add", "--all"], cwd=os.path.dirname(symlink) 309 ) 310 git.UploadChanges(llvm_symlink_dir, branch, commit_messages, reviewers, cc) 311 git.DeleteBranch(llvm_symlink_dir, branch) 312 313 314def resolve_symbolic_sha(start_sha: str, llvm_symlink_dir: str) -> str: 315 if start_sha == "llvm": 316 return parse_ebuild_for_assignment(llvm_symlink_dir, "LLVM_HASH") 317 318 if start_sha == "llvm-next": 319 return parse_ebuild_for_assignment(llvm_symlink_dir, "LLVM_NEXT_HASH") 320 321 return start_sha 322 323 324def find_patches_and_make_cl( 325 chroot_path: str, 326 patches: t.List[str], 327 start_rev: git_llvm_rev.Rev, 328 llvm_config: git_llvm_rev.LLVMConfig, 329 llvm_symlink_dir: str, 330 create_cl: bool, 331 skip_dependencies: bool, 332 reviewers: t.Optional[t.List[str]], 333 cc: t.Optional[t.List[str]], 334 platforms: t.List[str], 335): 336 337 converted_patches = [ 338 _convert_patch(llvm_config, skip_dependencies, p) for p in patches 339 ] 340 potential_duplicates = _get_duplicate_shas(converted_patches) 341 if potential_duplicates: 342 err_msg = "\n".join( 343 f"{a.patch} == {b.patch}" for a, b in potential_duplicates 344 ) 345 raise RuntimeError(f"Found Duplicate SHAs:\n{err_msg}") 346 347 # CL Related variables, only used if `create_cl` 348 symlinks_to_uprev = [] 349 commit_messages = [ 350 "llvm: get patches from upstream\n", 351 ] 352 branch = f'get-upstream-{datetime.now().strftime("%Y%m%d%H%M%S%f")}' 353 354 if create_cl: 355 git.CreateBranch(llvm_symlink_dir, branch) 356 357 for parsed_patch in converted_patches: 358 # Find out the llvm projects changed in this commit 359 packages = get_package_names(parsed_patch.sha, llvm_config.dir) 360 # Find out the ebuild symlinks of the corresponding ChromeOS packages 361 symlinks = chroot.GetChrootEbuildPaths( 362 chroot_path, 363 [ 364 "sys-devel/llvm" if package == "llvm" else "sys-libs/" + package 365 for package in packages 366 ], 367 ) 368 symlinks = chroot.ConvertChrootPathsToAbsolutePaths( 369 chroot_path, symlinks 370 ) 371 # Create a local patch for all the affected llvm projects 372 create_patch_for_packages( 373 packages, 374 symlinks, 375 start_rev, 376 parsed_patch.rev, 377 parsed_patch.sha, 378 llvm_config.dir, 379 platforms=platforms, 380 ) 381 if create_cl: 382 symlinks_to_uprev.extend(symlinks) 383 384 commit_messages.extend( 385 [ 386 parsed_patch.git_msg(), 387 subprocess.check_output( 388 ["git", "log", "-n1", "--oneline", parsed_patch.sha], 389 cwd=llvm_config.dir, 390 encoding="utf-8", 391 ), 392 ] 393 ) 394 395 if parsed_patch.is_differential: 396 subprocess.check_output( 397 ["git", "reset", "--hard", "HEAD^"], cwd=llvm_config.dir 398 ) 399 400 if create_cl: 401 make_cl( 402 symlinks_to_uprev, 403 llvm_symlink_dir, 404 branch, 405 commit_messages, 406 reviewers, 407 cc, 408 ) 409 410 411@dataclasses.dataclass(frozen=True) 412class ParsedPatch: 413 """Class to keep track of bundled patch info.""" 414 415 patch: str 416 sha: str 417 is_differential: bool 418 rev: t.Union[git_llvm_rev.Rev, str] 419 420 def git_msg(self) -> str: 421 if self.is_differential: 422 return f"\n\nreviews.llvm.org/{self.patch}\n" 423 return f"\n\nreviews.llvm.org/rG{self.sha}\n" 424 425 426def _convert_patch( 427 llvm_config: git_llvm_rev.LLVMConfig, skip_dependencies: bool, patch: str 428) -> ParsedPatch: 429 """Extract git revision info from a patch. 430 431 Args: 432 llvm_config: LLVM configuration object. 433 skip_dependencies: Pass --skip-dependecies for to `arc` 434 patch: A single patch referent string. 435 436 Returns: 437 A [ParsedPatch] object. 438 """ 439 440 # git hash should only have lower-case letters 441 is_differential = patch.startswith("D") 442 if is_differential: 443 subprocess.check_output( 444 [ 445 "arc", 446 "patch", 447 "--nobranch", 448 "--skip-dependencies" if skip_dependencies else "--revision", 449 patch, 450 ], 451 cwd=llvm_config.dir, 452 ) 453 sha = resolve_llvm_ref(llvm_config.dir, "HEAD") 454 rev = patch 455 else: 456 sha = resolve_llvm_ref(llvm_config.dir, patch) 457 rev = git_llvm_rev.translate_sha_to_rev(llvm_config, sha) 458 return ParsedPatch( 459 patch=patch, sha=sha, rev=rev, is_differential=is_differential 460 ) 461 462 463def _get_duplicate_shas( 464 patches: t.List[ParsedPatch], 465) -> t.List[t.Tuple[ParsedPatch, ParsedPatch]]: 466 """Return a list of Patches which have duplicate SHA's""" 467 return [ 468 (left, right) 469 for i, left in enumerate(patches) 470 for right in patches[i + 1 :] 471 if left.sha == right.sha 472 ] 473 474 475def get_from_upstream( 476 chroot_path: str, 477 create_cl: bool, 478 start_sha: str, 479 patches: t.List[str], 480 platforms: t.List[str], 481 skip_dependencies: bool = False, 482 reviewers: t.List[str] = None, 483 cc: t.List[str] = None, 484): 485 llvm_symlink = chroot.ConvertChrootPathsToAbsolutePaths( 486 chroot_path, 487 chroot.GetChrootEbuildPaths(chroot_path, ["sys-devel/llvm"]), 488 )[0] 489 llvm_symlink_dir = os.path.dirname(llvm_symlink) 490 491 git_status = subprocess.check_output( 492 ["git", "status", "-s"], cwd=llvm_symlink_dir, encoding="utf-8" 493 ) 494 495 if git_status: 496 error_path = os.path.dirname(os.path.dirname(llvm_symlink_dir)) 497 raise ValueError(f"Uncommited changes detected in {error_path}") 498 499 start_sha = resolve_symbolic_sha(start_sha, llvm_symlink_dir) 500 logging.info("Base llvm hash == %s", start_sha) 501 502 llvm_config = git_llvm_rev.LLVMConfig( 503 remote="origin", dir=get_llvm_hash.GetAndUpdateLLVMProjectInLLVMTools() 504 ) 505 start_sha = resolve_llvm_ref(llvm_config.dir, start_sha) 506 507 find_patches_and_make_cl( 508 chroot_path=chroot_path, 509 patches=patches, 510 platforms=platforms, 511 start_rev=git_llvm_rev.translate_sha_to_rev(llvm_config, start_sha), 512 llvm_config=llvm_config, 513 llvm_symlink_dir=llvm_symlink_dir, 514 create_cl=create_cl, 515 skip_dependencies=skip_dependencies, 516 reviewers=reviewers, 517 cc=cc, 518 ) 519 logging.info("Complete.") 520 521 522def main(): 523 chroot.VerifyOutsideChroot() 524 logging.basicConfig( 525 format="%(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: %(message)s", 526 level=logging.INFO, 527 ) 528 529 parser = argparse.ArgumentParser( 530 description=__doc__, 531 formatter_class=argparse.RawDescriptionHelpFormatter, 532 epilog=__DOC_EPILOGUE, 533 ) 534 parser.add_argument( 535 "--chroot_path", 536 default=os.path.join(os.path.expanduser("~"), "chromiumos"), 537 help="the path to the chroot (default: %(default)s)", 538 ) 539 parser.add_argument( 540 "--start_sha", 541 default="llvm-next", 542 help="LLVM SHA that the patch should start applying at. You can specify " 543 '"llvm" or "llvm-next", as well. Defaults to %(default)s.', 544 ) 545 parser.add_argument( 546 "--sha", 547 action="append", 548 default=[], 549 help="The LLVM git SHA to cherry-pick.", 550 ) 551 parser.add_argument( 552 "--differential", 553 action="append", 554 default=[], 555 help="The LLVM differential revision to apply. Example: D1234." 556 " Cannot be used for changes already merged upstream; use --sha" 557 " instead for those.", 558 ) 559 parser.add_argument( 560 "--platform", 561 action="append", 562 required=True, 563 help="Apply this patch to the give platform. Common options include " 564 '"chromiumos" and "android". Can be specified multiple times to ' 565 "apply to multiple platforms", 566 ) 567 parser.add_argument( 568 "--create_cl", 569 action="store_true", 570 help="Automatically create a CL if specified", 571 ) 572 parser.add_argument( 573 "--skip_dependencies", 574 action="store_true", 575 help="Skips a LLVM differential revision's dependencies. Only valid " 576 "when --differential appears exactly once.", 577 ) 578 args = parser.parse_args() 579 580 if not (args.sha or args.differential): 581 parser.error("--sha or --differential required") 582 583 if args.skip_dependencies and len(args.differential) != 1: 584 parser.error( 585 "--skip_dependencies is only valid when there's exactly one " 586 "supplied differential" 587 ) 588 589 get_from_upstream( 590 chroot_path=args.chroot_path, 591 create_cl=args.create_cl, 592 start_sha=args.start_sha, 593 patches=args.sha + args.differential, 594 skip_dependencies=args.skip_dependencies, 595 platforms=args.platform, 596 ) 597 598 599if __name__ == "__main__": 600 sys.exit(main()) 601