blob: 96ceacbf52f5df3fbc85317440430dfd5f5011e3 [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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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
import json
from pathlib import Path
import re
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, sha_to_revision
import paths
import source_manager
from utils import check_call, check_output
def parse_args():
parser = argparse.ArgumentParser(description="Cherry pick upstream LLVM patches.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--sha', nargs='+', help='sha of patches to cherry pick')
parser.add_argument(
'--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', required=True)
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(m.group(1))
@dataclass
class PatchItem:
end_version: Optional[int]
metadata: Dict[str, Any]
# info: Optional[List[str]]
# title: str
platforms: List[str]
rel_patch_path: str
start_version: int
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> PatchItem:
return PatchItem(
end_version=d['end_version'],
metadata=d['metadata'],
platforms=d['platforms'],
rel_patch_path=d['rel_patch_path'],
start_version=d['start_version'])
def to_dict(self) -> Dict[str, Any]:
return dataclasses.asdict(self, dict_factory=collections.OrderedDict)
@property
def is_local_patch(self) -> bool:
return not self.rel_patch_path.startswith('cherry/')
@property
def sha(self) -> str:
m = re.match(r'cherry/(.+)\.patch', self.rel_patch_path)
assert m, self.rel_patch_path
return m.group(1)
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'
@classmethod
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, separators=(',', ': '), sort_keys=True)
fh.write('\n')
def generate_patch_files(sha_list: List[str], start_version: int) -> PatchList:
""" generate upstream cherry-pick patch files """
upstream_dir = paths.TOOLCHAIN_LLVM_PATH
fetch_upstream()
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)
info: Optional[List[str]] = []
title = '[UPSTREAM] ' + commit_subject.strip()
rel_patch_path = f'cherry/{sha}.patch'
end_version = sha_to_revision(sha)
metadata = { 'info': info, 'title': title }
platforms = ['android']
result.append(PatchItem(end_version, metadata, platforms, rel_patch_path, start_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 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}'
else:
subject = '[patches] Cherry pick CLs from upstream'
commit_lines = [subject, '']
if bug:
if bug.isnumeric():
commit_lines += [f'Bug: http://b/{bug}', '']
else:
commit_lines += [f'Bug: {bug}', '']
for patch in new_patches:
sha = patch.sha[:11]
subject = patch.metadata['title']
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)
patch_list.extend(new_patches)
patch_list.sort()
patch_list.save_to_file()
if args.verify_merge:
print('verify merge...')
source_manager.setup_sources(source_dir=paths.LLVM_PATH)
if args.create_cl:
create_cl(new_patches, args.bug, args.reason)
if __name__ == '__main__':
main()