xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/update_chromeos_llvm_hash.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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"""Updates the LLVM hash and uprevs the build of the specified packages.
7
8For each package, a temporary repo is created and the changes are uploaded
9for review.
10"""
11
12import argparse
13import dataclasses
14import enum
15import os
16from pathlib import Path
17import re
18import subprocess
19import textwrap
20from typing import Dict, Iterable, Iterator, List, Optional, Union
21
22import atomic_write_file
23import chroot
24import failure_modes
25import get_llvm_hash
26import git
27import manifest_utils
28import patch_utils
29import subprocess_helpers
30
31
32# Default list of packages to update.
33DEFAULT_PACKAGES = patch_utils.CHROMEOS_PATCHES_JSON_PACKAGES
34
35DEFAULT_MANIFEST_PACKAGES = ["sys-devel/llvm"]
36
37
38# Specify which LLVM hash to update
39class LLVMVariant(enum.Enum):
40    """Represent the LLVM hash in an ebuild file to update."""
41
42    current = "LLVM_HASH"
43    next = "LLVM_NEXT_HASH"
44
45
46@dataclasses.dataclass(frozen=True, eq=True)
47class ChrootOpts:
48    """A class that holds chroot options."""
49
50    chromeos_root: Path
51    chroot_name: str = "chroot"
52    out_name: str = "out"
53
54
55class PortagePackage:
56    """Represents a portage package with location info."""
57
58    def __init__(self, chroot_opts: ChrootOpts, package: str):
59        """Create a new PortagePackage.
60
61        Args:
62            chroot_opts: options that specify the ChromeOS chroot to use.
63            package: "category/package" string.
64        """
65        self.package = package
66        potential_ebuild_path = PortagePackage.find_package_ebuild(
67            chroot_opts, package
68        )
69        if potential_ebuild_path.is_symlink():
70            self.uprev_target: Optional[Path] = potential_ebuild_path.absolute()
71            self.ebuild_path = potential_ebuild_path.resolve()
72        else:
73            # Should have a 9999 ebuild, no uprevs needed.
74            self.uprev_target = None
75            self.ebuild_path = potential_ebuild_path.absolute()
76
77    @staticmethod
78    def find_package_ebuild(chroot_opts: ChrootOpts, package: str) -> Path:
79        """Look up the package's ebuild location."""
80        chromeos_root_str = str(chroot_opts.chromeos_root)
81        ebuild_paths = chroot.GetChrootEbuildPaths(
82            chromeos_root_str,
83            [package],
84            chroot_opts.chroot_name,
85            chroot_opts.out_name,
86        )
87        converted = chroot.ConvertChrootPathsToAbsolutePaths(
88            chromeos_root_str, ebuild_paths
89        )[0]
90        return Path(converted)
91
92    def package_dir(self) -> Path:
93        """Return the package directory."""
94        return self.ebuild_path.parent
95
96    def update(
97        self, llvm_variant: LLVMVariant, git_hash: str, svn_version: int
98    ):
99        """Update the package with the new LLVM git sha and revision.
100
101        Args:
102            llvm_variant: Which LLVM hash to update.
103                Either LLVM_HASH or LLVM_NEXT_HASH.
104            git_hash: Upstream LLVM git hash to update to.
105            svn_version: Matching LLVM revision string for the git_hash.
106        """
107        live_ebuild = self.live_ebuild()
108        if live_ebuild:
109            # Working with a -9999 ebuild package here, no
110            # upreving.
111            UpdateEbuildLLVMHash(
112                live_ebuild, llvm_variant, git_hash, svn_version
113            )
114            return
115        if not self.uprev_target:
116            # We can exit early if we're not working with a live ebuild,
117            # and we don't have something to uprev.
118            raise RuntimeError(
119                "Cannot update: no live ebuild or symlink found"
120                f" for {self.package}"
121            )
122
123        UpdateEbuildLLVMHash(
124            self.ebuild_path, llvm_variant, git_hash, svn_version
125        )
126        if llvm_variant == LLVMVariant.current:
127            UprevEbuildToVersion(str(self.uprev_target), svn_version, git_hash)
128        else:
129            UprevEbuildSymlink(str(self.uprev_target))
130
131    def live_ebuild(self) -> Optional[Path]:
132        """Path to the live ebuild if it exists.
133
134        Returns:
135            The patch to the live ebuild if it exists. None otherwise.
136        """
137        matches = self.package_dir().glob("*-9999.ebuild")
138        return next(matches, None)
139
140
141def defaultCrosRoot() -> Path:
142    """Get default location of chromeos_path.
143
144    The logic assumes that the cros_root is ~/chromiumos, unless llvm_tools is
145    inside of a CrOS checkout, in which case that checkout should be used.
146
147    Returns:
148        The best guess location for the cros checkout.
149    """
150    llvm_tools_path = os.path.realpath(os.path.dirname(__file__))
151    if llvm_tools_path.endswith("src/third_party/toolchain-utils/llvm_tools"):
152        return Path(llvm_tools_path).parent.parent.parent.parent
153    return Path.home() / "chromiumos"
154
155
156def GetCommandLineArgs():
157    """Parses the command line for the optional command line arguments.
158
159    Returns:
160        The log level to use when retrieving the LLVM hash or google3 LLVM
161        version, the chroot path to use for executing chroot commands, a list
162        of a package or packages to update their LLVM next hash, and the LLVM
163        version to use when retrieving the LLVM hash.
164    """
165
166    # Create parser and add optional command-line arguments.
167    parser = argparse.ArgumentParser(
168        description="Updates the build's hash for llvm-next."
169    )
170
171    # Add argument for a specific chroot path.
172    parser.add_argument(
173        "--chromeos_path",
174        type=Path,
175        default=defaultCrosRoot(),
176        help="the path to the chroot (default: %(default)s)",
177    )
178
179    # Add argument for specific builds to uprev and update their llvm-next
180    # hash.
181    parser.add_argument(
182        "--update_packages",
183        default=",".join(DEFAULT_PACKAGES),
184        help="Comma-separated ebuilds to update llvm-next hash for "
185        "(default: %(default)s)",
186    )
187
188    parser.add_argument(
189        "--manifest_packages",
190        default="",
191        help="Comma-separated ebuilds to update manifests for "
192        "(default: %(default)s)",
193    )
194
195    # Add argument for the LLVM hash to update
196    parser.add_argument(
197        "--is_llvm_next",
198        action="store_true",
199        help="which llvm hash to update. If specified, update LLVM_NEXT_HASH. "
200        "Otherwise, update LLVM_HASH",
201    )
202
203    # Add argument for the LLVM version to use.
204    parser.add_argument(
205        "--llvm_version",
206        type=get_llvm_hash.IsSvnOption,
207        required=True,
208        help="which git hash to use. Either a svn revision, or one "
209        f"of {sorted(get_llvm_hash.KNOWN_HASH_SOURCES)}",
210    )
211
212    # Add argument for the mode of the patch management when handling patches.
213    parser.add_argument(
214        "--failure_mode",
215        default=failure_modes.FailureModes.FAIL.value,
216        choices=[
217            failure_modes.FailureModes.FAIL.value,
218            failure_modes.FailureModes.CONTINUE.value,
219            failure_modes.FailureModes.DISABLE_PATCHES.value,
220        ],
221        help="the mode of the patch manager when handling failed patches "
222        "(default: %(default)s)",
223    )
224
225    # Add argument for the patch metadata file.
226    parser.add_argument(
227        "--patch_metadata_file",
228        default="PATCHES.json",
229        help="the .json file that has all the patches and their "
230        "metadata if applicable (default: PATCHES.json inside $FILESDIR)",
231    )
232    parser.add_argument(
233        "--no_repo_manifest",
234        dest="repo_manifest",
235        action="store_false",
236        help="Skip updating the llvm-project revision attribute"
237        " in the internal manifest.",
238    )
239    parser.add_argument(
240        "--no_delete_branch",
241        action="store_true",
242        help="Do not delete the created overlay branch.",
243    )
244    parser.add_argument(
245        "--no_upload_changes",
246        action="store_true",
247        help="Do not upload changes to gerrit.",
248    )
249    parser.add_argument(
250        "--no_patching",
251        action="store_true",
252        help="Do not check or update PATCHES.json.",
253    )
254    # Parse the command line.
255    return parser.parse_args()
256
257
258def UpdateEbuildLLVMHash(
259    ebuild_path: Path,
260    llvm_variant: LLVMVariant,
261    git_hash: str,
262    svn_version: int,
263) -> None:
264    """Updates the LLVM hash in the ebuild.
265
266    The build changes are staged for commit in the temporary repo.
267
268    Args:
269        ebuild_path: The absolute path to the ebuild.
270        llvm_variant: Which LLVM hash to update.
271        git_hash: The new git hash.
272        svn_version: The SVN-style revision number of git_hash.
273
274    Raises:
275        ValueError: Invalid ebuild path provided or failed to stage the commit
276        of the changes or failed to update the LLVM hash.
277    """
278
279    # For each ebuild, read the file in
280    # advance and then create a temporary file
281    # that gets updated with the new LLVM hash
282    # and revision number and then the ebuild file
283    # gets updated to the temporary file.
284    if not os.path.isfile(ebuild_path):
285        raise ValueError(f"Invalid ebuild path provided: {ebuild_path}")
286
287    with open(ebuild_path, encoding="utf-8") as ebuild_file:
288        new_lines = list(
289            ReplaceLLVMHash(ebuild_file, llvm_variant, git_hash, svn_version)
290        )
291    with atomic_write_file.atomic_write(
292        ebuild_path, "w", encoding="utf-8"
293    ) as ebuild_file:
294        ebuild_file.writelines(new_lines)
295    # Stage the changes.
296    subprocess.check_output(
297        ["git", "-C", ebuild_path.parent, "add", ebuild_path]
298    )
299
300
301def ReplaceLLVMHash(
302    ebuild_lines: Iterable[str],
303    llvm_variant: LLVMVariant,
304    git_hash: str,
305    svn_version: int,
306) -> Iterator[str]:
307    """Updates the LLVM git hash.
308
309    Args:
310        ebuild_lines: The contents of the ebuild file.
311        llvm_variant: The LLVM hash to update.
312        git_hash: The new git hash.
313        svn_version: The SVN-style revision number of git_hash.
314
315    Yields:
316        lines of the modified ebuild file
317    """
318    is_updated = False
319    llvm_regex = re.compile(
320        "^" + re.escape(llvm_variant.value) + '="[a-z0-9]+"'
321    )
322    for cur_line in ebuild_lines:
323        if not is_updated and llvm_regex.search(cur_line):
324            # Update the git hash and revision number.
325            cur_line = f'{llvm_variant.value}="{git_hash}" # r{svn_version}\n'
326
327            is_updated = True
328
329        yield cur_line
330
331    if not is_updated:
332        raise ValueError(f"Failed to update {llvm_variant.value}")
333
334
335def UprevEbuildSymlink(symlink: str) -> None:
336    """Uprevs the symlink's revision number.
337
338    Increases the revision number by 1 and stages the change in
339    the temporary repo.
340
341    Args:
342        symlink: The absolute path of an ebuild symlink.
343
344    Raises:
345        ValueError: Failed to uprev the symlink or failed to stage the changes.
346    """
347
348    if not os.path.islink(symlink):
349        raise ValueError(f"Invalid symlink provided: {symlink}")
350
351    new_symlink, is_changed = re.subn(
352        r"r([0-9]+).ebuild",
353        lambda match: "r%s.ebuild" % str(int(match.group(1)) + 1),
354        symlink,
355        count=1,
356    )
357
358    if not is_changed:
359        raise ValueError("Failed to uprev the symlink.")
360
361    # rename the symlink
362    subprocess.check_output(
363        ["git", "-C", os.path.dirname(symlink), "mv", symlink, new_symlink]
364    )
365
366
367def UprevEbuildToVersion(symlink: str, svn_version: int, git_hash: str) -> None:
368    """Uprevs the ebuild's revision number.
369
370    Increases the revision number by 1 and stages the change in
371    the temporary repo.
372
373    Args:
374        symlink: The absolute path of an ebuild symlink.
375        svn_version: The SVN-style revision number of git_hash.
376        git_hash: The new git hash.
377
378    Raises:
379        ValueError: Failed to uprev the ebuild or failed to stage the changes.
380        AssertionError: No llvm version provided for an LLVM uprev
381    """
382
383    if not os.path.islink(symlink):
384        raise ValueError(f"Invalid symlink provided: {symlink}")
385
386    ebuild = os.path.realpath(symlink)
387    llvm_major_version = get_llvm_hash.GetLLVMMajorVersion(git_hash)
388    # llvm
389    package = os.path.basename(os.path.dirname(symlink))
390    if not package:
391        raise ValueError("Tried to uprev an unknown package")
392    if package == "llvm":
393        new_ebuild, is_changed = re.subn(
394            r"(\d+)\.(\d+)_pre([0-9]+)(_p[0-9]+)?",
395            "%s.\\2_pre%s"
396            % (
397                llvm_major_version,
398                str(svn_version),
399            ),
400            ebuild,
401            count=1,
402        )
403    # any other package
404    else:
405        new_ebuild, is_changed = re.subn(
406            r"(\d+)\.(\d+)_pre([0-9]+)",
407            "%s.\\2_pre%s" % (llvm_major_version, str(svn_version)),
408            ebuild,
409            count=1,
410        )
411
412    if not is_changed:  # failed to increment the revision number
413        raise ValueError("Failed to uprev the ebuild.")
414
415    symlink_dir = os.path.dirname(symlink)
416
417    # Rename the ebuild
418    subprocess.check_output(
419        ["git", "-C", symlink_dir, "mv", ebuild, new_ebuild]
420    )
421
422    # Create a symlink of the renamed ebuild
423    new_symlink = new_ebuild[: -len(".ebuild")] + "-r1.ebuild"
424    subprocess.check_output(["ln", "-s", "-r", new_ebuild, new_symlink])
425    subprocess.check_output(["git", "-C", symlink_dir, "add", new_symlink])
426    # Remove the old symlink
427    subprocess.check_output(["git", "-C", symlink_dir, "rm", symlink])
428
429
430def RemovePatchesFromFilesDir(patches: Iterable[str]) -> None:
431    """Removes the patches from $FILESDIR of a package.
432
433    Args:
434        patches: A list of absolute paths of patches to remove
435
436    Raises:
437        ValueError: Failed to remove a patch in $FILESDIR.
438    """
439
440    for patch in patches:
441        subprocess.check_output(
442            ["git", "-C", os.path.dirname(patch), "rm", "-f", patch]
443        )
444
445
446def StagePatchMetadataFileForCommit(patch_metadata_file_path: str) -> None:
447    """Stages the updated patch metadata file for commit.
448
449    Args:
450        patch_metadata_file_path: The absolute path to the patch metadata file.
451
452    Raises:
453        ValueError: Failed to stage the patch metadata file for commit or
454        invalid patch metadata file.
455    """
456
457    if not os.path.isfile(patch_metadata_file_path):
458        raise ValueError(
459            f"Invalid patch metadata file provided: {patch_metadata_file_path}"
460        )
461
462    # Cmd to stage the patch metadata file for commit.
463    subprocess.check_output(
464        [
465            "git",
466            "-C",
467            os.path.dirname(patch_metadata_file_path),
468            "add",
469            patch_metadata_file_path,
470        ]
471    )
472
473
474def StagePackagesPatchResultsForCommit(
475    package_info_dict: Dict[str, patch_utils.PatchInfo],
476    commit_messages: List[str],
477) -> List[str]:
478    """Stages the patch results of the packages to the commit message.
479
480    Args:
481        package_info_dict: A dictionary where the key is the package name and
482        the value is a dictionary that contains information about the patches
483        of the package (key).
484        commit_messages: The commit message that has the updated ebuilds and
485        upreving information.
486
487    Returns:
488        commit_messages with new additions
489    """
490
491    # For each package, check if any patches for that package have
492    # changed, if so, add which patches have changed to the commit
493    # message.
494    for package_name, patch_info in package_info_dict.items():
495        if (
496            patch_info.disabled_patches
497            or patch_info.removed_patches
498            or patch_info.modified_metadata
499        ):
500            cur_package_header = f"\nFor the package {package_name}:"
501            commit_messages.append(cur_package_header)
502
503        # Add to the commit message that the patch metadata file was modified.
504        if patch_info.modified_metadata:
505            patch_metadata_path = patch_info.modified_metadata
506            metadata_file_name = os.path.basename(patch_metadata_path)
507            commit_messages.append(
508                f"The patch metadata file {metadata_file_name} was modified"
509            )
510
511            StagePatchMetadataFileForCommit(patch_metadata_path)
512
513        # Add each disabled patch to the commit message.
514        if patch_info.disabled_patches:
515            commit_messages.append("The following patches were disabled:")
516
517            for patch_path in patch_info.disabled_patches:
518                commit_messages.append(os.path.basename(patch_path))
519
520        # Add each removed patch to the commit message.
521        if patch_info.removed_patches:
522            commit_messages.append("The following patches were removed:")
523
524            for patch_path in patch_info.removed_patches:
525                commit_messages.append(os.path.basename(patch_path))
526
527            RemovePatchesFromFilesDir(patch_info.removed_patches)
528
529    return commit_messages
530
531
532def UpdatePortageManifests(
533    packages: Iterable[str], chromeos_path: Path
534) -> None:
535    """Updates portage manifest files for packages.
536
537    Args:
538        packages: A list of packages to update manifests for.
539        chromeos_path: The absolute path to the chromeos checkout.
540
541    Raises:
542        CalledProcessError: ebuild failed to update manifest.
543    """
544    manifest_ebuilds = chroot.GetChrootEbuildPaths(chromeos_path, packages)
545    for ebuild_path in manifest_ebuilds:
546        ebuild_dir = os.path.dirname(ebuild_path)
547        subprocess_helpers.ChrootRunCommand(
548            chromeos_path, ["ebuild", ebuild_path, "manifest"]
549        )
550        subprocess_helpers.ChrootRunCommand(
551            chromeos_path, ["git", "-C", ebuild_dir, "add", "Manifest"]
552        )
553
554
555def UpdatePackages(
556    packages: Iterable[str],
557    manifest_packages: Iterable[str],
558    llvm_variant: LLVMVariant,
559    git_hash: str,
560    svn_version: int,
561    chroot_opts: ChrootOpts,
562    mode: Optional[failure_modes.FailureModes],
563    git_hash_source: Union[int, str],
564    extra_commit_msg_lines: Optional[Iterable[str]],
565    delete_branch: bool = True,
566    upload_changes: bool = True,
567    wip: bool = False,
568) -> Optional[git.CommitContents]:
569    """Updates an LLVM hash and uprevs the ebuild of the packages.
570
571    A temporary repo is created for the changes. The changes are
572    then uploaded for review.
573
574    Args:
575        packages: A list of all the packages that are going to be updated.
576        manifest_packages: A list of packages to update manifests for.
577        llvm_variant: The LLVM hash to update.
578        git_hash: The new git hash.
579        svn_version: The SVN-style revision number of git_hash.
580        chroot_opts: options that specify the ChromeOS chroot to use.
581        mode: The mode of the patch manager when handling an applicable patch.
582          If None is passed, the patch manager won't be invoked.
583        that failed to apply.
584            Ex. 'FailureModes.FAIL'
585        git_hash_source: The source of which git hash to use based off of.
586            Ex. 'google3', 'tot', or <version> such as 365123
587        extra_commit_msg_lines: extra lines to append to the commit message.
588            Newlines are added automatically.
589        delete_branch: Delete the git branch as a final step.
590        upload_changes: Upload the commit to gerrit as a CL.
591        wip: if True, any changes uploaded will be uploaded as
592            work-in-progress.
593
594    Returns:
595        If upload_changes is set, a git.CommitContents object. Otherwise None.
596    """
597    portage_packages = (PortagePackage(chroot_opts, pkg) for pkg in packages)
598    chromiumos_overlay_path = (
599        chroot_opts.chromeos_root / "src" / "third_party" / "chromiumos-overlay"
600    )
601    branch_name = "update-" + llvm_variant.value + "-" + git_hash
602
603    commit_message_header = "llvm"
604    if llvm_variant == LLVMVariant.next:
605        commit_message_header = "llvm-next"
606    if git_hash_source in get_llvm_hash.KNOWN_HASH_SOURCES:
607        commit_message_header += (
608            f"/{git_hash_source}: upgrade to {git_hash} (r{svn_version})"
609        )
610    else:
611        commit_message_header += f": upgrade to {git_hash} (r{svn_version})"
612
613    commit_lines = [
614        commit_message_header + "\n",
615        "The following packages have been updated:",
616    ]
617
618    # Holds the list of packages that are updating.
619    updated_packages: List[str] = []
620    change_list = None
621    git.CreateBranch(chromiumos_overlay_path, branch_name)
622    try:
623        for pkg in portage_packages:
624            pkg.update(llvm_variant, git_hash, svn_version)
625            updated_packages.append(pkg.package)
626            commit_lines.append(pkg.package)
627        if manifest_packages:
628            UpdatePortageManifests(manifest_packages, chroot_opts.chromeos_root)
629            commit_lines.append("Updated manifest for:")
630            commit_lines.extend(manifest_packages)
631        EnsurePackageMaskContains(chroot_opts.chromeos_root, git_hash)
632        # Handle the patches for each package.
633        if mode is not None:
634            package_info_dict = UpdatePackagesPatchMetadataFile(
635                chroot_opts, svn_version, updated_packages, mode
636            )
637            # Update the commit message if changes were made to a package's
638            # patches.
639            commit_lines = StagePackagesPatchResultsForCommit(
640                package_info_dict, commit_lines
641            )
642        if extra_commit_msg_lines:
643            commit_lines.extend(extra_commit_msg_lines)
644        git.CommitChanges(chromiumos_overlay_path, commit_lines)
645        if upload_changes:
646            change_list = git.UploadChanges(
647                chromiumos_overlay_path,
648                branch_name,
649                wip=wip,
650            )
651    finally:
652        if delete_branch:
653            git.DeleteBranch(chromiumos_overlay_path, branch_name)
654        else:
655            print(f"Not deleting branch {branch_name}")
656    return change_list
657
658
659def EnsurePackageMaskContains(
660    chromeos_path: Union[Path, str], git_hash: str
661) -> None:
662    """Adds the major version of llvm to package.mask if not already present.
663
664    Args:
665        chromeos_path: The absolute path to the chromeos checkout.
666        git_hash: The new git hash.
667
668    Raises:
669        FileExistsError: package.mask not found in ../../chromiumos-overlay
670    """
671
672    llvm_major_version = get_llvm_hash.GetLLVMMajorVersion(git_hash)
673
674    overlay_dir = os.path.join(
675        chromeos_path, "src/third_party/chromiumos-overlay"
676    )
677    mask_path = os.path.join(
678        overlay_dir, "profiles/targets/chromeos/package.mask"
679    )
680    with open(mask_path, "r+", encoding="utf-8") as mask_file:
681        mask_contents = mask_file.read()
682        expected_line = f"=sys-devel/llvm-{llvm_major_version}.0_pre*\n"
683        if expected_line not in mask_contents:
684            mask_file.write(expected_line)
685
686    subprocess.check_output(["git", "-C", overlay_dir, "add", mask_path])
687
688
689def UpdatePackagesPatchMetadataFile(
690    chroot_opts: ChrootOpts,
691    svn_version: int,
692    packages: Iterable[str],
693    mode: failure_modes.FailureModes,
694) -> Dict[str, patch_utils.PatchInfo]:
695    """Updates the packages metadata file.
696
697    Args:
698        chroot_opts: options that specify the ChromeOS chroot to use.
699        svn_version: The version to use for patch management.
700        packages: All the packages to update their patch metadata file.
701        mode: The mode for the patch manager to use when an applicable patch
702        fails to apply.
703            Ex: 'FailureModes.FAIL'
704
705    Returns:
706        A dictionary where the key is the package name and the value is a
707        dictionary that has information on the patches.
708    """
709
710    # A dictionary where the key is the package name and the value is a
711    # dictionary that has information on the patches.
712    package_info: Dict[str, patch_utils.PatchInfo] = {}
713
714    llvm_hash = get_llvm_hash.LLVMHash()
715
716    with llvm_hash.CreateTempDirectory() as temp_dir:
717        with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as dirname:
718            # Ensure that 'svn_version' exists in the chromiumum mirror of
719            # LLVM by finding its corresponding git hash.
720            git_hash = get_llvm_hash.GetGitHashFrom(dirname, svn_version)
721            move_head_cmd = ["git", "-C", dirname, "checkout", git_hash, "-q"]
722            subprocess.run(move_head_cmd, stdout=subprocess.DEVNULL, check=True)
723
724            for cur_package in packages:
725                # Get the absolute path to $FILESDIR of the package.
726                chroot_ebuild_str = subprocess_helpers.ChrootRunCommand(
727                    chroot_opts.chromeos_root,
728                    ["equery", "w", cur_package],
729                    chroot_name=chroot_opts.chroot_name,
730                    out_name=chroot_opts.out_name,
731                ).strip()
732                if not chroot_ebuild_str:
733                    raise RuntimeError(
734                        f"could not find ebuild for {cur_package}"
735                    )
736                chroot_ebuild_path = Path(
737                    chroot.ConvertChrootPathsToAbsolutePaths(
738                        str(chroot_opts.chromeos_root), [chroot_ebuild_str]
739                    )[0]
740                )
741                patches_json_fp = (
742                    chroot_ebuild_path.parent / "files" / "PATCHES.json"
743                )
744                if not patches_json_fp.is_file():
745                    raise RuntimeError(
746                        f"patches file {patches_json_fp} is not a file"
747                    )
748
749                src_path = Path(dirname)
750                with patch_utils.git_clean_context(src_path):
751                    if mode in (
752                        failure_modes.FailureModes.FAIL,
753                        failure_modes.FailureModes.CONTINUE,
754                    ):
755                        patches_info = patch_utils.apply_all_from_json(
756                            svn_version=svn_version,
757                            llvm_src_dir=src_path,
758                            patches_json_fp=patches_json_fp,
759                            continue_on_failure=mode
760                            == failure_modes.FailureModes.CONTINUE,
761                        )
762                    elif mode == failure_modes.FailureModes.DISABLE_PATCHES:
763                        patches_info = patch_utils.update_version_ranges(
764                            svn_version, src_path, patches_json_fp
765                        )
766                    else:
767                        raise RuntimeError(f"unsupported failure mode: {mode}")
768
769                package_info[cur_package] = patches_info
770
771    return package_info
772
773
774def ChangeRepoManifest(
775    git_hash: str,
776    src_tree: Path,
777    extra_commit_msg_lines: Optional[Iterable[str]] = None,
778    delete_branch=True,
779    upload_changes=True,
780):
781    """Change the repo internal manifest for llvm-project.
782
783    Args:
784        git_hash: The LLVM git hash to change to.
785        src_tree: ChromiumOS source tree checkout.
786        extra_commit_msg_lines: Lines to append to the commit message.
787        delete_branch: Delete the branch as a final step.
788        upload_changes: Upload the changes to gerrit.
789
790    Returns:
791        The uploaded changelist CommitContents.
792    """
793    manifest_dir = manifest_utils.get_chromeos_manifest_path(src_tree).parent
794    branch_name = "update-llvm-project-" + git_hash
795    commit_lines = (
796        textwrap.dedent(
797            f"""
798            manifest: Update llvm-project to {git_hash}
799
800            Upgrade the local LLVM revision to match the new llvm ebuild
801            hash. This must be merged along with any chromiumos-overlay
802            changes to LLVM. Automatic uprevs rely on the manifest hash
803            to match what is specified by LLVM_HASH.
804
805            This CL is generated by the update_chromeos_llvm_hash.py script.
806
807            BUG=None
808            TEST=CQ
809            """
810        )
811        .lstrip()
812        .splitlines()
813    )
814
815    change_list = None
816    git.CreateBranch(manifest_dir, branch_name)
817    try:
818        manifest_path = manifest_utils.update_chromeos_manifest(
819            git_hash,
820            src_tree,
821        )
822        subprocess.run(
823            ["git", "-C", manifest_dir, "add", manifest_path.name], check=True
824        )
825        if extra_commit_msg_lines:
826            commit_lines.extend(extra_commit_msg_lines)
827        git.CommitChanges(manifest_dir, commit_lines)
828        if upload_changes:
829            change_list = git.UploadChanges(manifest_dir, branch_name)
830    finally:
831        if delete_branch:
832            git.DeleteBranch(manifest_dir, branch_name)
833        else:
834            print(f"Not deleting branch {branch_name}")
835    return change_list
836
837
838def main():
839    """Updates the LLVM next hash for each package.
840
841    Raises:
842        AssertionError: The script was run inside the chroot.
843    """
844
845    chroot.VerifyOutsideChroot()
846
847    args_output = GetCommandLineArgs()
848
849    chroot.VerifyChromeOSRoot(args_output.chromeos_path)
850
851    llvm_variant = LLVMVariant.current
852    if args_output.is_llvm_next:
853        llvm_variant = LLVMVariant.next
854
855    git_hash_source = args_output.llvm_version
856
857    git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(
858        git_hash_source
859    )
860    # Filter out empty strings. For example "".split{",") returns [""].
861    packages = set(p for p in args_output.update_packages.split(",") if p)
862    manifest_packages = set(
863        p for p in args_output.manifest_packages.split(",") if p
864    )
865    if not manifest_packages and not args_output.is_llvm_next:
866        # Set default manifest packages only for the current llvm.
867        manifest_packages = set(DEFAULT_MANIFEST_PACKAGES)
868
869    if args_output.no_patching:
870        patch_update_mode = None
871    else:
872        patch_update_mode = failure_modes.FailureModes(args_output.failure_mode)
873
874    change_list = UpdatePackages(
875        packages=packages,
876        manifest_packages=manifest_packages,
877        llvm_variant=llvm_variant,
878        git_hash=git_hash,
879        svn_version=svn_version,
880        chroot_opts=ChrootOpts(args_output.chromeos_path),
881        mode=patch_update_mode,
882        git_hash_source=git_hash_source,
883        extra_commit_msg_lines=None,
884        delete_branch=not args_output.no_delete_branch,
885        upload_changes=not args_output.no_upload_changes,
886    )
887    if change_list:
888        print(f"Successfully updated packages to {git_hash} ({svn_version})")
889        print(f"Gerrit URL: {change_list.url}")
890        print(f"Change list number: {change_list.cl_number}")
891    else:
892        print("--no-upload passed, did not create a change list")
893
894    if args_output.repo_manifest and not args_output.is_llvm_next:
895        print(
896            f"Updating internal manifest to {git_hash} ({svn_version})...",
897            end="",
898        )
899        cq_depend_line = (
900            [f"Cq-Depend: chromium:{change_list.cl_number}"]
901            if change_list
902            else None
903        )
904        change_list = ChangeRepoManifest(
905            git_hash,
906            args_output.chromeos_path,
907            extra_commit_msg_lines=cq_depend_line,
908            delete_branch=not args_output.no_delete_branch,
909            upload_changes=not args_output.no_upload_changes,
910        )
911        print(" Done!")
912        if change_list:
913            print("New repo manifest CL:")
914            print(f"  URL: {change_list.url}")
915            print(f"  CL Number: {change_list.cl_number}")
916        else:
917            print("--no-upload passed, did not create a change list")
918
919
920if __name__ == "__main__":
921    main()
922