• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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