1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2024 The ChromiumOS Authors 3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 4*760c253cSXin Li# found in the LICENSE file. 5*760c253cSXin Li 6*760c253cSXin Li"""Get patches from a patch source, and integrate them into ChromiumOS. 7*760c253cSXin Li 8*760c253cSXin LiExample Usage: 9*760c253cSXin Li # Apply a Pull request. 10*760c253cSXin Li $ get_patch.py -s HEAD p:74791 11*760c253cSXin Li # Apply several patches. 12*760c253cSXin Li $ get_patch.py -s 82e851a407c5 p:74791 47413bb27 13*760c253cSXin Li # Use another llvm-project dir. 14*760c253cSXin Li $ get_patch.py -s HEAD -l ~/llvm-project 47413bb27 15*760c253cSXin Li""" 16*760c253cSXin Li 17*760c253cSXin Liimport argparse 18*760c253cSXin Liimport dataclasses 19*760c253cSXin Liimport json 20*760c253cSXin Liimport logging 21*760c253cSXin Lifrom pathlib import Path 22*760c253cSXin Liimport random 23*760c253cSXin Liimport re 24*760c253cSXin Liimport subprocess 25*760c253cSXin Liimport tempfile 26*760c253cSXin Liimport textwrap 27*760c253cSXin Lifrom typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union 28*760c253cSXin Lifrom urllib import request 29*760c253cSXin Li 30*760c253cSXin Liimport atomic_write_file 31*760c253cSXin Liimport git_llvm_rev 32*760c253cSXin Liimport patch_utils 33*760c253cSXin Li 34*760c253cSXin Li 35*760c253cSXin LiCHROMIUMOS_OVERLAY_PATH = Path("src/third_party/chromiumos-overlay") 36*760c253cSXin LiLLVM_PKG_PATH = CHROMIUMOS_OVERLAY_PATH / "sys-devel/llvm" 37*760c253cSXin LiCOMPILER_RT_PKG_PATH = CHROMIUMOS_OVERLAY_PATH / "sys-libs/compiler-rt" 38*760c253cSXin LiLIBCXX_PKG_PATH = CHROMIUMOS_OVERLAY_PATH / "sys-libs/libcxx" 39*760c253cSXin LiLIBUNWIND_PKG_PATH = CHROMIUMOS_OVERLAY_PATH / "sys-libs/llvm-libunwind" 40*760c253cSXin LiSCUDO_PKG_PATH = CHROMIUMOS_OVERLAY_PATH / "sys-libs/scudo" 41*760c253cSXin LiLLDB_PKG_PATH = CHROMIUMOS_OVERLAY_PATH / "dev-util/lldb-server" 42*760c253cSXin Li 43*760c253cSXin LiLLVM_PROJECT_PATH = Path("src/third_party/llvm-project") 44*760c253cSXin LiPATCH_METADATA_FILENAME = "PATCHES.json" 45*760c253cSXin Li 46*760c253cSXin Li 47*760c253cSXin Liclass CherrypickError(ValueError): 48*760c253cSXin Li """ValueError for a cherry-pick has been seen before.""" 49*760c253cSXin Li 50*760c253cSXin Li 51*760c253cSXin Liclass CherrypickVersionError(ValueError): 52*760c253cSXin Li """ValueError that highlights the cherry-pick is before the start_ref.""" 53*760c253cSXin Li 54*760c253cSXin Li 55*760c253cSXin Li@dataclasses.dataclass 56*760c253cSXin Liclass LLVMGitRef: 57*760c253cSXin Li """Represents an LLVM git ref.""" 58*760c253cSXin Li 59*760c253cSXin Li git_ref: str 60*760c253cSXin Li _rev: Optional[git_llvm_rev.Rev] = None # Used for caching 61*760c253cSXin Li 62*760c253cSXin Li @classmethod 63*760c253cSXin Li def from_rev(cls, llvm_dir: Path, rev: git_llvm_rev.Rev) -> "LLVMGitRef": 64*760c253cSXin Li return cls( 65*760c253cSXin Li git_llvm_rev.translate_rev_to_sha( 66*760c253cSXin Li git_llvm_rev.LLVMConfig("origin", llvm_dir), rev 67*760c253cSXin Li ), 68*760c253cSXin Li _rev=rev, 69*760c253cSXin Li ) 70*760c253cSXin Li 71*760c253cSXin Li def to_rev(self, llvm_dir: Path) -> git_llvm_rev.Rev: 72*760c253cSXin Li if self._rev: 73*760c253cSXin Li return self._rev 74*760c253cSXin Li self._rev = git_llvm_rev.translate_sha_to_rev( 75*760c253cSXin Li git_llvm_rev.LLVMConfig("origin", llvm_dir), 76*760c253cSXin Li self.git_ref, 77*760c253cSXin Li ) 78*760c253cSXin Li return self._rev 79*760c253cSXin Li 80*760c253cSXin Li 81*760c253cSXin Li@dataclasses.dataclass(frozen=True) 82*760c253cSXin Liclass LLVMPullRequest: 83*760c253cSXin Li """Represents an upstream GitHub Pull Request number.""" 84*760c253cSXin Li 85*760c253cSXin Li number: int 86*760c253cSXin Li 87*760c253cSXin Li 88*760c253cSXin Li@dataclasses.dataclass 89*760c253cSXin Liclass PatchContext: 90*760c253cSXin Li """Represents the state of the chromiumos source during patching.""" 91*760c253cSXin Li 92*760c253cSXin Li llvm_project_dir: Path 93*760c253cSXin Li chromiumos_root: Path 94*760c253cSXin Li start_ref: LLVMGitRef 95*760c253cSXin Li platforms: Iterable[str] 96*760c253cSXin Li dry_run: bool = False 97*760c253cSXin Li 98*760c253cSXin Li def apply_patches( 99*760c253cSXin Li self, patch_source: Union[LLVMGitRef, LLVMPullRequest] 100*760c253cSXin Li ) -> None: 101*760c253cSXin Li """Create .patch files and add them to PATCHES.json. 102*760c253cSXin Li 103*760c253cSXin Li Post: 104*760c253cSXin Li Unless self.dry_run is True, writes the patch contents to 105*760c253cSXin Li the respective <pkg>/files/ workdir for each applicable 106*760c253cSXin Li patch, and the JSON files are updated with the new entries. 107*760c253cSXin Li 108*760c253cSXin Li Raises: 109*760c253cSXin Li TypeError: If the patch_source is not a 110*760c253cSXin Li LLVMGitRef or LLVMPullRequest. 111*760c253cSXin Li """ 112*760c253cSXin Li new_patch_entries = self.make_patches(patch_source) 113*760c253cSXin Li self.apply_entries_to_json(new_patch_entries) 114*760c253cSXin Li 115*760c253cSXin Li def apply_entries_to_json( 116*760c253cSXin Li self, 117*760c253cSXin Li new_patch_entries: Iterable[patch_utils.PatchEntry], 118*760c253cSXin Li ) -> None: 119*760c253cSXin Li """Add some PatchEntries to the appropriate PATCHES.json.""" 120*760c253cSXin Li workdir_mappings: Dict[Path, List[patch_utils.PatchEntry]] = {} 121*760c253cSXin Li for pe in new_patch_entries: 122*760c253cSXin Li workdir_mappings[pe.workdir] = workdir_mappings.get( 123*760c253cSXin Li pe.workdir, [] 124*760c253cSXin Li ) + [pe] 125*760c253cSXin Li for workdir, pes in workdir_mappings.items(): 126*760c253cSXin Li patches_json_file = workdir / PATCH_METADATA_FILENAME 127*760c253cSXin Li with patches_json_file.open(encoding="utf-8") as f: 128*760c253cSXin Li orig_contents = f.read() 129*760c253cSXin Li old_patch_entries = patch_utils.json_str_to_patch_entries( 130*760c253cSXin Li workdir, orig_contents 131*760c253cSXin Li ) 132*760c253cSXin Li indent_len = patch_utils.predict_indent(orig_contents.splitlines()) 133*760c253cSXin Li if not self.dry_run: 134*760c253cSXin Li with atomic_write_file.atomic_write( 135*760c253cSXin Li patches_json_file, encoding="utf-8" 136*760c253cSXin Li ) as f: 137*760c253cSXin Li json.dump( 138*760c253cSXin Li [pe.to_dict() for pe in old_patch_entries + pes], 139*760c253cSXin Li f, 140*760c253cSXin Li indent=indent_len, 141*760c253cSXin Li ) 142*760c253cSXin Li f.write("\n") 143*760c253cSXin Li 144*760c253cSXin Li def make_patches( 145*760c253cSXin Li self, patch_source: Union[LLVMGitRef, LLVMPullRequest] 146*760c253cSXin Li ) -> List[patch_utils.PatchEntry]: 147*760c253cSXin Li """Create PatchEntries for a given LLVM change and returns them. 148*760c253cSXin Li 149*760c253cSXin Li Returns: 150*760c253cSXin Li A list of PatchEntries representing the patches for each 151*760c253cSXin Li package for the given patch_source. 152*760c253cSXin Li 153*760c253cSXin Li Post: 154*760c253cSXin Li Unless self.dry_run is True, writes the patch contents to 155*760c253cSXin Li the respective <pkg>/files/ workdir for each applicable 156*760c253cSXin Li patch. 157*760c253cSXin Li 158*760c253cSXin Li Raises: 159*760c253cSXin Li TypeError: If the patch_source is not a 160*760c253cSXin Li LLVMGitRef or LLVMPullRequest. 161*760c253cSXin Li """ 162*760c253cSXin Li 163*760c253cSXin Li # This is just a dispatch method to the actual methods. 164*760c253cSXin Li if isinstance(patch_source, LLVMGitRef): 165*760c253cSXin Li return self._make_patches_from_git_ref(patch_source) 166*760c253cSXin Li if isinstance(patch_source, LLVMPullRequest): 167*760c253cSXin Li return self._make_patches_from_pr(patch_source) 168*760c253cSXin Li raise TypeError( 169*760c253cSXin Li f"patch_source was invalid type {type(patch_source).__name__}" 170*760c253cSXin Li ) 171*760c253cSXin Li 172*760c253cSXin Li def _make_patches_from_git_ref( 173*760c253cSXin Li self, 174*760c253cSXin Li patch_source: LLVMGitRef, 175*760c253cSXin Li ) -> List[patch_utils.PatchEntry]: 176*760c253cSXin Li packages = get_changed_packages( 177*760c253cSXin Li self.llvm_project_dir, patch_source.git_ref 178*760c253cSXin Li ) 179*760c253cSXin Li new_patch_entries: List[patch_utils.PatchEntry] = [] 180*760c253cSXin Li for workdir in self._workdirs_for_packages(packages): 181*760c253cSXin Li rel_patch_path = f"cherry/{patch_source.git_ref}.patch" 182*760c253cSXin Li if (workdir / "cherry").is_dir(): 183*760c253cSXin Li rel_patch_path = f"cherry/{patch_source.git_ref}.patch" 184*760c253cSXin Li else: 185*760c253cSXin Li # Some packages don't have a cherry directory. 186*760c253cSXin Li rel_patch_path = f"{patch_source.git_ref}.patch" 187*760c253cSXin Li if not self._is_valid_patch_range(self.start_ref, patch_source): 188*760c253cSXin Li raise CherrypickVersionError( 189*760c253cSXin Li f"'from' ref {self.start_ref} is later or" 190*760c253cSXin Li f" same as than 'until' ref {patch_source}" 191*760c253cSXin Li ) 192*760c253cSXin Li pe = patch_utils.PatchEntry( 193*760c253cSXin Li workdir=workdir, 194*760c253cSXin Li metadata={ 195*760c253cSXin Li "title": get_commit_subj( 196*760c253cSXin Li self.llvm_project_dir, patch_source.git_ref 197*760c253cSXin Li ), 198*760c253cSXin Li "info": [], 199*760c253cSXin Li }, 200*760c253cSXin Li platforms=list(self.platforms), 201*760c253cSXin Li rel_patch_path=rel_patch_path, 202*760c253cSXin Li version_range={ 203*760c253cSXin Li "from": self.start_ref.to_rev(self.llvm_project_dir).number, 204*760c253cSXin Li "until": patch_source.to_rev(self.llvm_project_dir).number, 205*760c253cSXin Li }, 206*760c253cSXin Li ) 207*760c253cSXin Li # Before we actually do any modifications, check if the patch is 208*760c253cSXin Li # already applied. 209*760c253cSXin Li if self.is_patch_applied(pe): 210*760c253cSXin Li raise CherrypickError( 211*760c253cSXin Li f"Patch at {pe.rel_patch_path}" 212*760c253cSXin Li " already exists in PATCHES.json" 213*760c253cSXin Li ) 214*760c253cSXin Li contents = git_format_patch( 215*760c253cSXin Li self.llvm_project_dir, 216*760c253cSXin Li patch_source.git_ref, 217*760c253cSXin Li ) 218*760c253cSXin Li if not self.dry_run: 219*760c253cSXin Li _write_patch(pe.title(), contents, pe.patch_path()) 220*760c253cSXin Li new_patch_entries.append(pe) 221*760c253cSXin Li return new_patch_entries 222*760c253cSXin Li 223*760c253cSXin Li def _make_patches_from_pr( 224*760c253cSXin Li self, patch_source: LLVMPullRequest 225*760c253cSXin Li ) -> List[patch_utils.PatchEntry]: 226*760c253cSXin Li json_response = get_llvm_github_pull(patch_source.number) 227*760c253cSXin Li github_ctx = GitHubPRContext(json_response, self.llvm_project_dir) 228*760c253cSXin Li rel_patch_path = f"{github_ctx.full_title_cleaned}.patch" 229*760c253cSXin Li contents, packages = github_ctx.git_squash_chain_patch() 230*760c253cSXin Li new_patch_entries = [] 231*760c253cSXin Li for workdir in self._workdirs_for_packages(packages): 232*760c253cSXin Li pe = patch_utils.PatchEntry( 233*760c253cSXin Li workdir=workdir, 234*760c253cSXin Li metadata={ 235*760c253cSXin Li "title": github_ctx.full_title, 236*760c253cSXin Li "info": [], 237*760c253cSXin Li }, 238*760c253cSXin Li rel_patch_path=rel_patch_path, 239*760c253cSXin Li platforms=list(self.platforms), 240*760c253cSXin Li version_range={ 241*760c253cSXin Li "from": self.start_ref.to_rev(self.llvm_project_dir).number, 242*760c253cSXin Li "until": None, 243*760c253cSXin Li }, 244*760c253cSXin Li ) 245*760c253cSXin Li # Before we actually do any modifications, check if the patch is 246*760c253cSXin Li # already applied. 247*760c253cSXin Li if self.is_patch_applied(pe): 248*760c253cSXin Li raise CherrypickError( 249*760c253cSXin Li f"Patch at {pe.rel_patch_path}" 250*760c253cSXin Li " already exists in PATCHES.json" 251*760c253cSXin Li ) 252*760c253cSXin Li if not self.dry_run: 253*760c253cSXin Li _write_patch(pe.title(), contents, pe.patch_path()) 254*760c253cSXin Li new_patch_entries.append(pe) 255*760c253cSXin Li return new_patch_entries 256*760c253cSXin Li 257*760c253cSXin Li def _workdirs_for_packages(self, packages: Iterable[Path]) -> List[Path]: 258*760c253cSXin Li return [self.chromiumos_root / pkg / "files" for pkg in packages] 259*760c253cSXin Li 260*760c253cSXin Li def is_patch_applied(self, to_check: patch_utils.PatchEntry) -> bool: 261*760c253cSXin Li """Return True if the patch is applied in PATCHES.json.""" 262*760c253cSXin Li patches_json_file = to_check.workdir / PATCH_METADATA_FILENAME 263*760c253cSXin Li with patches_json_file.open(encoding="utf-8") as f: 264*760c253cSXin Li patch_entries = patch_utils.json_to_patch_entries( 265*760c253cSXin Li to_check.workdir, f 266*760c253cSXin Li ) 267*760c253cSXin Li return any( 268*760c253cSXin Li p.rel_patch_path == to_check.rel_patch_path for p in patch_entries 269*760c253cSXin Li ) 270*760c253cSXin Li 271*760c253cSXin Li def _is_valid_patch_range( 272*760c253cSXin Li self, from_ref: LLVMGitRef, to_ref: LLVMGitRef 273*760c253cSXin Li ) -> bool: 274*760c253cSXin Li return ( 275*760c253cSXin Li from_ref.to_rev(self.llvm_project_dir).number 276*760c253cSXin Li < to_ref.to_rev(self.llvm_project_dir).number 277*760c253cSXin Li ) 278*760c253cSXin Li 279*760c253cSXin Li 280*760c253cSXin Lidef get_commit_subj(git_root_dir: Path, ref: str) -> str: 281*760c253cSXin Li """Return a given commit's subject.""" 282*760c253cSXin Li logging.debug("Getting commit subject for %s", ref) 283*760c253cSXin Li subj = subprocess.run( 284*760c253cSXin Li ["git", "show", "-s", "--format=%s", ref], 285*760c253cSXin Li cwd=git_root_dir, 286*760c253cSXin Li encoding="utf-8", 287*760c253cSXin Li stdout=subprocess.PIPE, 288*760c253cSXin Li check=True, 289*760c253cSXin Li ).stdout.strip() 290*760c253cSXin Li logging.debug(" -> %s", subj) 291*760c253cSXin Li return subj 292*760c253cSXin Li 293*760c253cSXin Li 294*760c253cSXin Lidef git_format_patch(git_root_dir: Path, ref: str) -> str: 295*760c253cSXin Li """Format a patch for a single git ref. 296*760c253cSXin Li 297*760c253cSXin Li Args: 298*760c253cSXin Li git_root_dir: Root directory for a given local git repository. 299*760c253cSXin Li ref: Git ref to make a patch for. 300*760c253cSXin Li 301*760c253cSXin Li Returns: 302*760c253cSXin Li The patch file contents. 303*760c253cSXin Li """ 304*760c253cSXin Li logging.debug("Formatting patch for %s^..%s", ref, ref) 305*760c253cSXin Li proc = subprocess.run( 306*760c253cSXin Li ["git", "format-patch", "--stdout", f"{ref}^..{ref}"], 307*760c253cSXin Li cwd=git_root_dir, 308*760c253cSXin Li encoding="utf-8", 309*760c253cSXin Li stdout=subprocess.PIPE, 310*760c253cSXin Li check=True, 311*760c253cSXin Li ) 312*760c253cSXin Li contents = proc.stdout.strip() 313*760c253cSXin Li if not contents: 314*760c253cSXin Li raise ValueError(f"No git diff between {ref}^..{ref}") 315*760c253cSXin Li logging.debug("Patch diff is %d lines long", contents.count("\n")) 316*760c253cSXin Li return contents 317*760c253cSXin Li 318*760c253cSXin Li 319*760c253cSXin Lidef get_llvm_github_pull(pull_number: int) -> Dict[str, Any]: 320*760c253cSXin Li """Get information about an LLVM pull request. 321*760c253cSXin Li 322*760c253cSXin Li Returns: 323*760c253cSXin Li A dictionary containing the JSON response from GitHub. 324*760c253cSXin Li 325*760c253cSXin Li Raises: 326*760c253cSXin Li RuntimeError when the network response is not OK. 327*760c253cSXin Li """ 328*760c253cSXin Li 329*760c253cSXin Li pull_url = ( 330*760c253cSXin Li f"https://api.github.com/repos/llvm/llvm-project/pulls/{pull_number}" 331*760c253cSXin Li ) 332*760c253cSXin Li # TODO(ajordanr): If we are ever allowed to use the 'requests' library 333*760c253cSXin Li # we should move to that instead of urllib. 334*760c253cSXin Li req = request.Request( 335*760c253cSXin Li url=pull_url, 336*760c253cSXin Li headers={ 337*760c253cSXin Li "X-GitHub-Api-Version": "2022-11-28", 338*760c253cSXin Li "Accept": "application/vnd.github+json", 339*760c253cSXin Li }, 340*760c253cSXin Li ) 341*760c253cSXin Li with request.urlopen(req) as f: 342*760c253cSXin Li if f.status >= 400: 343*760c253cSXin Li raise RuntimeError( 344*760c253cSXin Li f"GitHub response was not OK: {f.status} {f.reason}" 345*760c253cSXin Li ) 346*760c253cSXin Li response = f.read().decode("utf-8") 347*760c253cSXin Li return json.loads(response) 348*760c253cSXin Li 349*760c253cSXin Li 350*760c253cSXin Liclass GitHubPRContext: 351*760c253cSXin Li """Metadata and pathing context for a GitHub pull request checkout.""" 352*760c253cSXin Li 353*760c253cSXin Li def __init__( 354*760c253cSXin Li self, 355*760c253cSXin Li response: Dict[str, Any], 356*760c253cSXin Li llvm_project_dir: Path, 357*760c253cSXin Li ) -> None: 358*760c253cSXin Li """Create a GitHubPRContext from a GitHub pulls api call. 359*760c253cSXin Li 360*760c253cSXin Li Args: 361*760c253cSXin Li response: A dictionary formed from the JSON sent by 362*760c253cSXin Li the github pulls API endpoint. 363*760c253cSXin Li llvm_project_dir: Path to llvm-project git directory. 364*760c253cSXin Li """ 365*760c253cSXin Li try: 366*760c253cSXin Li self.clone_url = response["head"]["repo"]["clone_url"] 367*760c253cSXin Li self._title = response["title"] 368*760c253cSXin Li self.body = response["body"] 369*760c253cSXin Li self.base_ref = response["base"]["sha"] 370*760c253cSXin Li self.head_ref = response["head"]["sha"] 371*760c253cSXin Li self.llvm_project_dir = llvm_project_dir 372*760c253cSXin Li self.number = int(response["number"]) 373*760c253cSXin Li self._fetched = False 374*760c253cSXin Li except (ValueError, KeyError): 375*760c253cSXin Li logging.error("Failed to parse GitHub response:\n%s", response) 376*760c253cSXin Li raise 377*760c253cSXin Li 378*760c253cSXin Li @property 379*760c253cSXin Li def full_title(self) -> str: 380*760c253cSXin Li return f"[PR{self.number}] {self._title}" 381*760c253cSXin Li 382*760c253cSXin Li @property 383*760c253cSXin Li def full_title_cleaned(self) -> str: 384*760c253cSXin Li return re.sub(r"\W", "-", self.full_title) 385*760c253cSXin Li 386*760c253cSXin Li def git_squash_chain_patch(self) -> Tuple[str, Set[Path]]: 387*760c253cSXin Li """Replicate a squashed merge commit as a patch file. 388*760c253cSXin Li 389*760c253cSXin Li Args: 390*760c253cSXin Li git_root_dir: Root directory for a given local git repository 391*760c253cSXin Li which contains the base_ref. 392*760c253cSXin Li output: File path to write the patch to. 393*760c253cSXin Li 394*760c253cSXin Li Returns: 395*760c253cSXin Li The patch file contents. 396*760c253cSXin Li """ 397*760c253cSXin Li self._fetch() 398*760c253cSXin Li idx = random.randint(0, 2**32) 399*760c253cSXin Li tmpbranch_name = f"squash-branch-{idx}" 400*760c253cSXin Li 401*760c253cSXin Li with tempfile.TemporaryDirectory() as dir_str: 402*760c253cSXin Li worktree_parent_dir = Path(dir_str) 403*760c253cSXin Li commit_message_file = worktree_parent_dir / "commit_message" 404*760c253cSXin Li # Need this separate from the commit message, otherwise the 405*760c253cSXin Li # dir will be non-empty. 406*760c253cSXin Li worktree_dir = worktree_parent_dir / "worktree" 407*760c253cSXin Li with commit_message_file.open("w", encoding="utf-8") as f: 408*760c253cSXin Li f.write(self.full_title) 409*760c253cSXin Li f.write("\n\n") 410*760c253cSXin Li f.write( 411*760c253cSXin Li "\n".join( 412*760c253cSXin Li textwrap.wrap( 413*760c253cSXin Li self.body, width=72, replace_whitespace=False 414*760c253cSXin Li ) 415*760c253cSXin Li ) 416*760c253cSXin Li ) 417*760c253cSXin Li f.write("\n") 418*760c253cSXin Li 419*760c253cSXin Li logging.debug("Base ref: %s", self.base_ref) 420*760c253cSXin Li logging.debug("Head ref: %s", self.head_ref) 421*760c253cSXin Li logging.debug( 422*760c253cSXin Li "Creating worktree at '%s' with branch '%s'", 423*760c253cSXin Li worktree_dir, 424*760c253cSXin Li tmpbranch_name, 425*760c253cSXin Li ) 426*760c253cSXin Li self._run( 427*760c253cSXin Li [ 428*760c253cSXin Li "git", 429*760c253cSXin Li "worktree", 430*760c253cSXin Li "add", 431*760c253cSXin Li "-b", 432*760c253cSXin Li tmpbranch_name, 433*760c253cSXin Li worktree_dir, 434*760c253cSXin Li self.base_ref, 435*760c253cSXin Li ], 436*760c253cSXin Li self.llvm_project_dir, 437*760c253cSXin Li ) 438*760c253cSXin Li try: 439*760c253cSXin Li self._run( 440*760c253cSXin Li ["git", "merge", "--squash", self.head_ref], worktree_dir 441*760c253cSXin Li ) 442*760c253cSXin Li self._run( 443*760c253cSXin Li [ 444*760c253cSXin Li "git", 445*760c253cSXin Li "commit", 446*760c253cSXin Li "-a", 447*760c253cSXin Li "-F", 448*760c253cSXin Li commit_message_file, 449*760c253cSXin Li ], 450*760c253cSXin Li worktree_dir, 451*760c253cSXin Li ) 452*760c253cSXin Li changed_packages = get_changed_packages( 453*760c253cSXin Li worktree_dir, (self.base_ref, "HEAD") 454*760c253cSXin Li ) 455*760c253cSXin Li patch_contents = git_format_patch(worktree_dir, "HEAD") 456*760c253cSXin Li finally: 457*760c253cSXin Li logging.debug( 458*760c253cSXin Li "Cleaning up worktree and deleting branch %s", 459*760c253cSXin Li tmpbranch_name, 460*760c253cSXin Li ) 461*760c253cSXin Li self._run( 462*760c253cSXin Li ["git", "worktree", "remove", worktree_dir], 463*760c253cSXin Li self.llvm_project_dir, 464*760c253cSXin Li ) 465*760c253cSXin Li self._run( 466*760c253cSXin Li ["git", "branch", "-D", tmpbranch_name], 467*760c253cSXin Li self.llvm_project_dir, 468*760c253cSXin Li ) 469*760c253cSXin Li return (patch_contents, changed_packages) 470*760c253cSXin Li 471*760c253cSXin Li def _fetch(self) -> None: 472*760c253cSXin Li if not self._fetched: 473*760c253cSXin Li logging.debug( 474*760c253cSXin Li "Fetching from %s and setting FETCH_HEAD to %s", 475*760c253cSXin Li self.clone_url, 476*760c253cSXin Li self.head_ref, 477*760c253cSXin Li ) 478*760c253cSXin Li self._run( 479*760c253cSXin Li ["git", "fetch", self.clone_url, self.head_ref], 480*760c253cSXin Li cwd=self.llvm_project_dir, 481*760c253cSXin Li ) 482*760c253cSXin Li self._fetched = True 483*760c253cSXin Li 484*760c253cSXin Li @staticmethod 485*760c253cSXin Li def _run( 486*760c253cSXin Li cmd: List[Union[str, Path]], 487*760c253cSXin Li cwd: Path, 488*760c253cSXin Li stdin: int = subprocess.DEVNULL, 489*760c253cSXin Li ) -> subprocess.CompletedProcess: 490*760c253cSXin Li """Helper for subprocess.run.""" 491*760c253cSXin Li return subprocess.run( 492*760c253cSXin Li cmd, 493*760c253cSXin Li cwd=cwd, 494*760c253cSXin Li stdin=stdin, 495*760c253cSXin Li stdout=subprocess.PIPE, 496*760c253cSXin Li encoding="utf-8", 497*760c253cSXin Li check=True, 498*760c253cSXin Li ) 499*760c253cSXin Li 500*760c253cSXin Li 501*760c253cSXin Lidef get_changed_packages( 502*760c253cSXin Li llvm_project_dir: Path, ref: Union[str, Tuple[str, str]] 503*760c253cSXin Li) -> Set[Path]: 504*760c253cSXin Li """Returns package paths which changed over a given ref. 505*760c253cSXin Li 506*760c253cSXin Li Args: 507*760c253cSXin Li llvm_project_dir: Path to llvm-project 508*760c253cSXin Li ref: Git ref to check diff of. If set to a tuple, compares the diff 509*760c253cSXin Li between the first and second ref. 510*760c253cSXin Li 511*760c253cSXin Li Returns: 512*760c253cSXin Li A set of package paths which were changed. 513*760c253cSXin Li """ 514*760c253cSXin Li if isinstance(ref, tuple): 515*760c253cSXin Li ref_from, ref_to = ref 516*760c253cSXin Li elif isinstance(ref, str): 517*760c253cSXin Li ref_from = ref + "^" 518*760c253cSXin Li ref_to = ref 519*760c253cSXin Li else: 520*760c253cSXin Li raise TypeError(f"ref was {type(ref)}; need a tuple or a string") 521*760c253cSXin Li 522*760c253cSXin Li logging.debug("Getting git diff between %s..%s", ref_from, ref_to) 523*760c253cSXin Li proc = subprocess.run( 524*760c253cSXin Li ["git", "diff", "--name-only", f"{ref_from}..{ref_to}"], 525*760c253cSXin Li check=True, 526*760c253cSXin Li encoding="utf-8", 527*760c253cSXin Li stdout=subprocess.PIPE, 528*760c253cSXin Li cwd=llvm_project_dir, 529*760c253cSXin Li ) 530*760c253cSXin Li changed_paths = proc.stdout.splitlines() 531*760c253cSXin Li logging.debug("Found %d changed files", len(changed_paths)) 532*760c253cSXin Li # Some LLVM projects are built by LLVM ebuild on x86, so always apply the 533*760c253cSXin Li # patch to LLVM ebuild 534*760c253cSXin Li packages = {LLVM_PKG_PATH} 535*760c253cSXin Li for changed_path in changed_paths: 536*760c253cSXin Li if changed_path.startswith("compiler-rt"): 537*760c253cSXin Li packages.add(COMPILER_RT_PKG_PATH) 538*760c253cSXin Li if "scudo" in changed_path: 539*760c253cSXin Li packages.add(SCUDO_PKG_PATH) 540*760c253cSXin Li elif changed_path.startswith("libunwind"): 541*760c253cSXin Li packages.add(LIBUNWIND_PKG_PATH) 542*760c253cSXin Li elif changed_path.startswith("libcxx") or changed_path.startswith( 543*760c253cSXin Li "libcxxabi" 544*760c253cSXin Li ): 545*760c253cSXin Li packages.add(LIBCXX_PKG_PATH) 546*760c253cSXin Li elif changed_path.startswith("lldb"): 547*760c253cSXin Li packages.add(LLDB_PKG_PATH) 548*760c253cSXin Li return packages 549*760c253cSXin Li 550*760c253cSXin Li 551*760c253cSXin Lidef _has_repo_child(path: Path) -> bool: 552*760c253cSXin Li """Check if a given directory has a repo child. 553*760c253cSXin Li 554*760c253cSXin Li Useful for checking if a directory has a chromiumos source tree. 555*760c253cSXin Li """ 556*760c253cSXin Li child_maybe = path / ".repo" 557*760c253cSXin Li return path.is_dir() and child_maybe.is_dir() 558*760c253cSXin Li 559*760c253cSXin Li 560*760c253cSXin Lidef _autodetect_chromiumos_root( 561*760c253cSXin Li parent: Optional[Path] = None, 562*760c253cSXin Li) -> Optional[Path]: 563*760c253cSXin Li """Find the root of the chromiumos source tree from the current workdir. 564*760c253cSXin Li 565*760c253cSXin Li Returns: 566*760c253cSXin Li The root directory of the current chromiumos source tree. 567*760c253cSXin Li If the current working directory is not within a chromiumos source 568*760c253cSXin Li tree, then this returns None. 569*760c253cSXin Li """ 570*760c253cSXin Li if parent is None: 571*760c253cSXin Li parent = Path.cwd() 572*760c253cSXin Li if parent.resolve() == Path.root: 573*760c253cSXin Li return None 574*760c253cSXin Li if _has_repo_child(parent): 575*760c253cSXin Li return parent 576*760c253cSXin Li return _autodetect_chromiumos_root(parent.parent) 577*760c253cSXin Li 578*760c253cSXin Li 579*760c253cSXin Lidef _write_patch(title: str, contents: str, path: Path) -> None: 580*760c253cSXin Li """Actually write the patch contents to a file.""" 581*760c253cSXin Li # This is mostly separated for mocking. 582*760c253cSXin Li logging.info("Writing patch '%s' to '%s'", title, path) 583*760c253cSXin Li path.write_text(contents, encoding="utf-8") 584*760c253cSXin Li 585*760c253cSXin Li 586*760c253cSXin Lidef validate_patch_args( 587*760c253cSXin Li positional_args: List[str], 588*760c253cSXin Li) -> List[Union[LLVMGitRef, LLVMPullRequest]]: 589*760c253cSXin Li """Checks that each ref_or_pr_num is in a valid format.""" 590*760c253cSXin Li patch_sources = [] 591*760c253cSXin Li for arg in positional_args: 592*760c253cSXin Li patch_source: Union[LLVMGitRef, LLVMPullRequest] 593*760c253cSXin Li if arg.startswith("p:"): 594*760c253cSXin Li try: 595*760c253cSXin Li pull_request_num = int(arg.lstrip("p:")) 596*760c253cSXin Li except ValueError as e: 597*760c253cSXin Li raise ValueError( 598*760c253cSXin Li f"GitHub Pull Request '{arg}' was not in the format of" 599*760c253cSXin Li f" 'p:NNNN': {e}" 600*760c253cSXin Li ) 601*760c253cSXin Li logging.info("Patching remote GitHub PR '%s'", pull_request_num) 602*760c253cSXin Li patch_source = LLVMPullRequest(pull_request_num) 603*760c253cSXin Li else: 604*760c253cSXin Li logging.info("Patching local ref '%s'", arg) 605*760c253cSXin Li patch_source = LLVMGitRef(arg) 606*760c253cSXin Li patch_sources.append(patch_source) 607*760c253cSXin Li return patch_sources 608*760c253cSXin Li 609*760c253cSXin Li 610*760c253cSXin Lidef parse_args() -> argparse.Namespace: 611*760c253cSXin Li """Parse CLI arguments for this script.""" 612*760c253cSXin Li 613*760c253cSXin Li parser = argparse.ArgumentParser( 614*760c253cSXin Li "get_patch", 615*760c253cSXin Li description=__doc__, 616*760c253cSXin Li formatter_class=argparse.RawDescriptionHelpFormatter, 617*760c253cSXin Li ) 618*760c253cSXin Li parser.add_argument( 619*760c253cSXin Li "-c", 620*760c253cSXin Li "--chromiumos-root", 621*760c253cSXin Li help="""Path to the chromiumos source tree root. 622*760c253cSXin Li Tries to autodetect if not passed. 623*760c253cSXin Li """, 624*760c253cSXin Li ) 625*760c253cSXin Li parser.add_argument( 626*760c253cSXin Li "-l", 627*760c253cSXin Li "--llvm", 628*760c253cSXin Li help="""Path to the llvm dir. 629*760c253cSXin Li Tries to autodetect from chromiumos root if not passed. 630*760c253cSXin Li """, 631*760c253cSXin Li ) 632*760c253cSXin Li parser.add_argument( 633*760c253cSXin Li "-s", 634*760c253cSXin Li "--start-ref", 635*760c253cSXin Li default="HEAD", 636*760c253cSXin Li help="""The starting ref for which to apply patches. 637*760c253cSXin Li """, 638*760c253cSXin Li ) 639*760c253cSXin Li parser.add_argument( 640*760c253cSXin Li "-p", 641*760c253cSXin Li "--platform", 642*760c253cSXin Li action="append", 643*760c253cSXin Li help="""Apply this patch to the give platform. Common options include 644*760c253cSXin Li 'chromiumos' and 'android'. Can be specified multiple times to 645*760c253cSXin Li apply to multiple platforms. If not passed, platform is set to 646*760c253cSXin Li 'chromiumos'. 647*760c253cSXin Li """, 648*760c253cSXin Li ) 649*760c253cSXin Li parser.add_argument( 650*760c253cSXin Li "--dry-run", 651*760c253cSXin Li action="store_true", 652*760c253cSXin Li help="Run normally, but don't make any changes. Read-only mode.", 653*760c253cSXin Li ) 654*760c253cSXin Li parser.add_argument( 655*760c253cSXin Li "-v", 656*760c253cSXin Li "--verbose", 657*760c253cSXin Li action="store_true", 658*760c253cSXin Li help="Enable verbose logging.", 659*760c253cSXin Li ) 660*760c253cSXin Li parser.add_argument( 661*760c253cSXin Li "ref_or_pr_num", 662*760c253cSXin Li nargs="+", 663*760c253cSXin Li help="""Git ref or GitHub PR number to make patches. 664*760c253cSXin Li To patch a GitHub PR, use the syntax p:NNNN (e.g. 'p:123456'). 665*760c253cSXin Li """, 666*760c253cSXin Li type=str, 667*760c253cSXin Li ) 668*760c253cSXin Li args = parser.parse_args() 669*760c253cSXin Li 670*760c253cSXin Li logging.basicConfig( 671*760c253cSXin Li format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: " 672*760c253cSXin Li "%(message)s", 673*760c253cSXin Li level=logging.DEBUG if args.verbose else logging.INFO, 674*760c253cSXin Li ) 675*760c253cSXin Li 676*760c253cSXin Li args.patch_sources = validate_patch_args(args.ref_or_pr_num) 677*760c253cSXin Li if args.chromiumos_root: 678*760c253cSXin Li if not _has_repo_child(args.chromiumos_root): 679*760c253cSXin Li parser.error("chromiumos root directly passed but has no .repo") 680*760c253cSXin Li logging.debug("chromiumos root directly passed; found and verified") 681*760c253cSXin Li elif tmp := _autodetect_chromiumos_root(): 682*760c253cSXin Li logging.debug("chromiumos root autodetected; found and verified") 683*760c253cSXin Li args.chromiumos_root = tmp 684*760c253cSXin Li else: 685*760c253cSXin Li parser.error( 686*760c253cSXin Li "Could not autodetect chromiumos root. Use '-c' to pass the " 687*760c253cSXin Li "chromiumos root path directly." 688*760c253cSXin Li ) 689*760c253cSXin Li 690*760c253cSXin Li if not args.llvm: 691*760c253cSXin Li if (args.chromiumos_root / LLVM_PROJECT_PATH).is_dir(): 692*760c253cSXin Li args.llvm = args.chromiumos_root / LLVM_PROJECT_PATH 693*760c253cSXin Li else: 694*760c253cSXin Li parser.error( 695*760c253cSXin Li "Could not autodetect llvm-project dir. Use '-l' to pass the " 696*760c253cSXin Li "llvm-project directly" 697*760c253cSXin Li ) 698*760c253cSXin Li return args 699*760c253cSXin Li 700*760c253cSXin Li 701*760c253cSXin Lidef main() -> None: 702*760c253cSXin Li """Entry point for the program.""" 703*760c253cSXin Li 704*760c253cSXin Li args = parse_args() 705*760c253cSXin Li 706*760c253cSXin Li # For the vast majority of cases, we'll only want to set platform to 707*760c253cSXin Li # ["chromiumos"], so let's make that the default. 708*760c253cSXin Li platforms: List[str] = args.platform if args.platform else ["chromiumos"] 709*760c253cSXin Li 710*760c253cSXin Li ctx = PatchContext( 711*760c253cSXin Li chromiumos_root=args.chromiumos_root, 712*760c253cSXin Li llvm_project_dir=args.llvm, 713*760c253cSXin Li start_ref=LLVMGitRef(args.start_ref), 714*760c253cSXin Li platforms=platforms, 715*760c253cSXin Li dry_run=args.dry_run, 716*760c253cSXin Li ) 717*760c253cSXin Li for patch_source in args.patch_sources: 718*760c253cSXin Li ctx.apply_patches(patch_source) 719*760c253cSXin Li 720*760c253cSXin Li 721*760c253cSXin Liif __name__ == "__main__": 722*760c253cSXin Li main() 723