blob: b29f1c60e0cb10fd718d8e99d145e757f8ae2eb5 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (C) 2020 The Android Open Source Project
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import argparse
import collections
import dataclasses
from dataclasses import dataclass
from functools import lru_cache
import json
from pathlib import Path
import re
import sys
from typing import Any, Dict, List, Optional
from android_version import get_svn_revision_number, set_llvm_next
from merge_from_upstream import fetch_upstream
import paths
import source_manager
from utils import check_call, check_output
sys.path.append(str(paths.TOOLCHAIN_UTILS_DIR / 'llvm_tools'))
import git_llvm_rev
def parse_args():
parser = argparse.ArgumentParser(description="Cherry pick upstream LLVM patches.",
parser.add_argument('--sha', nargs='+', help='sha of patches to cherry pick')
'--start-version', default='llvm',
help="""svn revision to start applying patches. 'llvm' and 'llvm-next' can also be used.""")
parser.add_argument('--verify-merge', action='store_true',
help='check if patches can be applied cleanly')
parser.add_argument('--create-cl', action='store_true', help='create a CL')
parser.add_argument('--bug', help='bug to reference in CLs created (if any)')
parser.add_argument('--reason', help='issue/reason to mention in CL subject line')
args = parser.parse_args()
return args
def parse_start_version(start_version: str) -> int:
if start_version in ['llvm', 'llvm-next']:
set_llvm_next(start_version == 'llvm-next')
return int(get_svn_revision_number())
m = re.match(r'r?(\d+)', start_version)
assert m, f'invalid start_version: {start_version}'
return int(
class PatchItem:
comment: str
rel_patch_path: str
start_version: int
end_version: Optional[int]
def from_dict(cls, d: Dict[str, Any]) -> PatchItem:
return PatchItem(
def to_dict(self) -> Dict[str, Any]:
return dataclasses.asdict(self, dict_factory=collections.OrderedDict)
def is_local_patch(self) -> bool:
return not self.rel_patch_path.startswith('cherry/')
def sha(self) -> str:
m = re.match(r'cherry/(.+)\.patch', self.rel_patch_path)
assert m, self.rel_patch_path
def __lt__(self, other: PatchItem) -> bool:
""" Used to sort patches in PatchList:
1. Sort upstream patches by their end_version in increasing order.
2. Keep local patches at the end of the list, and don't change the relative order
between two local patches.
if self.is_local_patch or other.is_local_patch:
if not self.is_local_patch:
return True
return False
return self.end_version < other.end_version
class PatchList(list):
""" a list of PatchItem """
JSON_FILE_PATH = paths.SCRIPTS_DIR / 'patches' / 'PATCHES.json'
def load_from_file(cls) -> PatchList:
with open(cls.JSON_FILE_PATH, 'r') as fh:
array = json.load(fh)
return PatchList(PatchItem.from_dict(d) for d in array)
def save_to_file(self):
array = [patch.to_dict() for patch in self]
with open(self.JSON_FILE_PATH, 'w') as fh:
json.dump(array, fh, indent=4)
def fetch_upstream_once():
# Fetching upstream may take a long time. So print something.
print('fetch upstream...')
def generate_patch_files(sha_list: List[str], start_version: int) -> PatchList:
""" generate upstream cherry-pick patch files """
upstream_dir = paths.TOOLCHAIN_LLVM_PATH
result = PatchList()
for sha in sha_list:
if len(sha) < 40:
sha = get_full_sha(upstream_dir, sha)
file_path = paths.SCRIPTS_DIR / 'patches' / 'cherry' / f'{sha}.patch'
assert not file_path.exists(), f'{file_path} already exists'
with open(file_path, 'w') as fh:
check_call(f'git format-patch -1 {sha} --stdout',
stdout=fh, shell=True, cwd=upstream_dir)
commit_subject = check_output(
f'git log -n1 --format=%s {sha}', shell=True, cwd=upstream_dir)
comment = '[UPSTREAM] ' + commit_subject.strip()
rel_patch_path = f'cherry/{sha}.patch'
end_version = sha_to_revision(sha)
result.append(PatchItem(comment, rel_patch_path, start_version, end_version))
return result
def get_full_sha(upstream_dir: Path, short_sha: str) -> str:
return check_output(['git', 'rev-parse', short_sha], cwd=upstream_dir).strip()
def sha_to_revision(sha: str) -> int:
git_llvm_rev.MAIN_BRANCH = 'upstream-main'
llvm_config = git_llvm_rev.LLVMConfig(remote='aosp', dir=str(paths.TOOLCHAIN_LLVM_PATH))
rev = git_llvm_rev.translate_sha_to_rev(llvm_config, sha)
return rev.number
def create_cl(new_patches: PatchList, bug: Optional[str], reason: Optional[str]):
file_list = [p.rel_patch_path for p in new_patches] + ['PATCHES.json']
file_list = [str(paths.SCRIPTS_DIR / 'patches' / f) for f in file_list]
check_call(['git', 'add'] + file_list)
if reason:
subject = f'[patches] Cherry pick CLS for: {reason}'
subject = '[patches] Cherry pick CLs from upstream'
commit_lines = [subject, '']
if bug:
commit_lines += [f'Bug: http://b/{bug}', '']
for patch in new_patches:
sha = patch.sha[:11]
subject = patch.comment
if subject.startswith('[UPSTREAM] '):
subject = subject[len('[UPSTREAM] '):]
commit_lines.append(sha + ' ' + subject)
commit_lines += ['', 'Test: N/A']
check_call(['git', 'commit', '-m', '\n'.join(commit_lines)])
def main():
args = parse_args()
patch_list = PatchList.load_from_file()
if args.sha:
start_version = parse_start_version(args.start_version)
new_patches = generate_patch_files(args.sha, start_version)
if args.verify_merge:
print('verify merge...')
if args.create_cl:
create_cl(new_patches, args.bug, args.reason)
if __name__ == '__main__':