build: move libcc patches to electron repo (#14104)

In the GN build, libchromiumcontent is no longer a distinct library, but
merely a container for a set of scripts and patches. Maintaining those
patches in a separate repository is tedious and error-prone, so merge
them into the main repo.

Once this is merged and GN is the default way to build Electron, the
libchromiumcontent repository can be archived.
This commit is contained in:
Jeremy Apthorp 2018-09-13 22:02:16 -07:00 committed by GitHub
parent 9e85bdb02c
commit 76c5f5cc8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
147 changed files with 86931 additions and 6 deletions

36
script/README.md Normal file
View file

@ -0,0 +1,36 @@
## get-patch
Use it to save commits from upstream repositories as patch files.
### Examples
1. Write commit contents in the patch format to stdout.
```
$ ./script/get-patch --repo src --commit 8e8216e5
```
2. Create a patch file with a default name if a specified directory.
```
$ ./script/get-patch --repo src --commit 8e8216e5 --output-dir patches
```
3. Create a patch file with a custom name in the current directory.
```
$ ./script/get-patch --repo src --commit 8e8216e5 --filename my.patch
```
4. Create a patch file with a custom name in a specified directory.
```
$ ./script/get-patch --repo src --commit 8e8216e5 --output-dir patches --filename my.patch
```
5. Create patch files with default names in a specified directory.
```
$ ./script/get-patch --repo src --output-dir patches --commit 8e8216e5 164c37e3
```
6. Create patch files with custom names in a specified directory.
Note that number of filenames must match the number of commits.
```
$ ./script/get-patch --repo src --output-dir patches --commit 8e8216e5 164c37e3 --filename first.patch second.patch
```

69
script/apply-patches Executable file
View file

@ -0,0 +1,69 @@
#!/usr/bin/env python
import argparse
import os
import subprocess
import sys
import lib.git as git
from lib.patches import PatchesConfig
SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
PATCHES_DIR = os.path.join(SOURCE_ROOT, 'patches')
PATCHES_COMMON_DIR = os.path.join(PATCHES_DIR, 'common')
PATCHES_MIPS64EL_DIR = os.path.join(PATCHES_DIR, 'mips64el')
SRC = 'src'
SRC_DIR = os.path.join(SOURCE_ROOT, SRC)
def main():
args = parse_args()
project_root = os.path.abspath(args.project_root)
for folder in args.folders:
error = apply_patches_for_dir(folder, project_root, args.commit)
if error:
sys.stderr.write(error + '\n')
sys.stderr.flush()
return 1
return 0
def apply_patches_for_dir(directory, project_root, commit):
for root, dirs, files in os.walk(directory):
config = PatchesConfig.from_directory(root, project_root=project_root)
patches_list = config.get_patches_list()
if patches_list is None:
continue
(success, failed_patches) = patches_list.apply(commit=commit)
if not success:
patch_path = failed_patches[0].get_file_path()
return '{0} failed to apply'.format(os.path.basename(patch_path))
def parse_args():
parser = argparse.ArgumentParser(description='Apply all required patches.')
parser.add_argument('--commit', default=False, action='store_true',
help='Commit a patch after it has been applied')
parser.add_argument('--project-root', required=False, default=git.get_repo_root(os.path.abspath(__file__)),
help='Parent folder to resolve repos relative paths against')
parser.add_argument('-t', '--target_arch',
help='Target architecture')
parser.add_argument('-f', '--folder', dest='folders', help='Apply patches from this folder', nargs='*', default=[])
args = parser.parse_args()
if not args.folders:
args.folders.append(PATCHES_COMMON_DIR)
if args.target_arch == 'mips64el':
args.folders.append(PATCHES_MIPS64EL_DIR)
return args
if __name__ == '__main__':
sys.exit(main())

101
script/get-patch Executable file
View file

@ -0,0 +1,101 @@
#!/usr/bin/env python
"""Retrieves commit contents in a patch format.
Check README.md for usage examples or run it with `-h` argument to get an interface reference.
"""
import argparse
import os
import sys
from lib.git import get_patch as git_get_patch
def parse_args():
parser = argparse.ArgumentParser(
description='Get a commit contents in a patch format.')
parser.add_argument('-c', '--commit', nargs='+', required=True,
help='Commit hash(es).')
parser.add_argument('-f', '--filename', nargs='*', required=False,
help='Filename(s) for a patch file(s). Defaults to something like {0}.'
.format(get_default_patch_file_name(
'4b825dc642cb6eb9a060e54bf8d69288fbee4904')))
parser.add_argument('-o', '--output-dir', required=False,
help='Folder to save the patch in. Defaults to a current folder.')
parser.add_argument('-r', '--repo', required=True,
help='Path to a repository root folder.')
args = parser.parse_args()
# Additional rules
if args.filename is not None and (len(args.commit) != len(args.filename)):
parser.error("Number of filenames ({0}) must be equal to number of commits ({1})."
.format(len(args.filename), len(args.commit)))
return args
def get_default_patch_file_name(commit_hash):
template = 'backport_{commit_hash}.patch'
number_of_hash_characters_to_preserve = 8
commit_hash_string = commit_hash[:number_of_hash_characters_to_preserve+1]
patch_file_name = template.format(
commit_hash=commit_hash_string)
return patch_file_name
def get_output_path(output_dir, output_filename, commit_hash):
if (output_dir is None) and (output_filename is None):
return None
# Use current dir as a default.
if output_dir is None:
output_dir = '.'
# Use the default filename if it's not provided.
if output_filename is None:
output_filename = get_default_patch_file_name(commit_hash)
output_path = os.path.join(output_dir, output_filename)
return output_path
def save_to_file(data, file_path):
with open(file_path, 'w') as f:
f.write(data)
def process_patch(repo, commit_hash, output_path=None):
patch_contents = git_get_patch(repo, commit_hash)
if output_path is None:
sys.stdout.write(patch_contents)
else:
save_to_file(data=patch_contents, file_path=output_path)
def main():
args = parse_args()
commits = args.commit
filenames = args.filename
# (alexeykuzmin): Ugly hack alert.
if filenames is None:
filenames = [None] * len(commits)
for (commit, filename) in zip(commits, filenames):
output_path = get_output_path(args.output_dir, filename,
commit_hash=commit)
process_patch(args.repo, commit, output_path)
return 0
if __name__ == '__main__':
sys.exit(main())

105
script/lib/git.py Normal file
View file

@ -0,0 +1,105 @@
"""Git helper functions.
Everything in here should be project agnostic, shouldn't rely on project's structure,
and make any assumptions about the passed arguments or calls outcomes.
"""
import os
import subprocess
from util import scoped_cwd
def is_repo_root(path):
path_exists = os.path.exists(path)
if not path_exists:
return False
git_folder_path = os.path.join(path, '.git')
git_folder_exists = os.path.exists(git_folder_path)
return git_folder_exists
def get_repo_root(path):
"""Finds a closest ancestor folder which is a repo root."""
norm_path = os.path.normpath(path)
norm_path_exists = os.path.exists(norm_path)
if not norm_path_exists:
return None
if is_repo_root(norm_path):
return norm_path
parent_path = os.path.dirname(norm_path)
# Check if we're in the root folder already.
if parent_path == norm_path:
return None
return get_repo_root(parent_path)
def apply(repo, patch_path, directory=None, index=False, reverse=False):
args = ['git', 'apply',
'--ignore-space-change',
'--ignore-whitespace',
'--whitespace', 'fix'
]
if directory:
args += ['--directory', directory]
if index:
args += ['--index']
if reverse:
args += ['--reverse']
args += ['--', patch_path]
with scoped_cwd(repo):
return_code = subprocess.call(args)
applied_successfully = (return_code == 0)
return applied_successfully
def get_patch(repo, commit_hash):
args = ['git', 'diff-tree',
'-p',
commit_hash,
'--' # Explicitly tell Git that `commit_hash` is a revision, not a path.
]
with scoped_cwd(repo):
return subprocess.check_output(args)
def get_head_commit(repo):
args = ['git', 'rev-parse', 'HEAD']
with scoped_cwd(repo):
return subprocess.check_output(args).strip()
def reset(repo):
args = ['git', 'reset']
with scoped_cwd(repo):
subprocess.check_call(args)
def commit(repo, author, message):
""" Commit whatever in the index is now."""
# Let's setup committer info so git won't complain about it being missing.
# TODO: Is there a better way to set committer's name and email?
env = os.environ.copy()
env['GIT_COMMITTER_NAME'] = 'Anonymous Committer'
env['GIT_COMMITTER_EMAIL'] = 'anonymous@electronjs.org'
args = ['git', 'commit',
'--author', author,
'--message', message
]
with scoped_cwd(repo):
return_code = subprocess.call(args, env=env)
committed_successfully = (return_code == 0)
return committed_successfully

168
script/lib/patches.py Normal file
View file

@ -0,0 +1,168 @@
import os
import sys
import git
SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
VENDOR_DIR = os.path.join(SOURCE_ROOT, 'vendor')
PYYAML_LIB_DIR = os.path.join(VENDOR_DIR, 'pyyaml', 'lib')
sys.path.append(PYYAML_LIB_DIR)
import yaml
class Patch:
def __init__(self, file_path, repo_path, paths_prefix=None, author='Anonymous <anonymous@electronjs.org>', description=None):
self.author = author
self.description = description
self.file_path = file_path
self.paths_prefix = paths_prefix
self.repo_path = repo_path
def apply(self, reverse=False, commit=False, index=False):
# Add the change to index only if we're going to commit it later.
add_to_index = index or commit
patch_applied = git.apply(self.repo_path, self.file_path, directory=self.paths_prefix, index=add_to_index, reverse=reverse)
if not patch_applied:
return False
if commit:
message = self.__get_commit_message(reverse)
patch_committed = git.commit(self.repo_path, author=self.author, message=message)
return patch_committed
return True
def __get_commit_message(self, reverse):
message = self.description
if message is None:
message = os.path.basename(self.file_path)
if reverse:
message = 'Revert: ' + message
return message
def reverse(self):
return self.apply(reverse=True)
def get_file_path(self):
return self.file_path
class PatchesList:
"""A list of patches for a specific git repo."""
def __init__(self, repo_path, patches):
# TODO(alexeykuzmin): Make sure that all patches have the same repo_path.
self.repo_path = repo_path
self.patches = patches
def __len__(self):
return len(self.patches)
def apply(self, reverse=False, stop_on_error=True, commit=False):
all_patches_applied = True
failed_patches = []
for patch in self.patches:
# Even if `commit` is True we're not going to commit
# individual patches, it takes too much time in the Chromium repo.
# Applying all commits takes about 10 minutes (!) on a fast dev machine.
# Instead of it we are going only to add all changes to the index
# and commit them all at once later.
applied_successfully = patch.apply(reverse=reverse, index=commit, commit=False)
if not applied_successfully:
all_patches_applied = False
failed_patches.append(patch)
should_stop_now = not applied_successfully and stop_on_error
if should_stop_now:
break
if commit and not all_patches_applied:
git.reset(self.repo_path)
if commit and all_patches_applied:
author = 'Electron Build Process <build@electronjs.org>'
message = 'Apply Electron patches'
git.commit(self.repo_path, author=author, message=message)
return (all_patches_applied, failed_patches)
def reverse(self, stop_on_error=True, commit=False):
return self.apply(reverse=True, stop_on_error=stop_on_error, commit=commit)
class PatchesConfig:
@staticmethod
def from_directory(dir_path, project_root, config_name='.patches.yaml'):
config_path = os.path.join(dir_path, config_name)
return PatchesConfig(config_path, project_root)
def __init__(self, config_path, project_root):
self.path = config_path
self.patches_list = None
self.project_root = project_root
def __parse(self):
contents = None
if os.path.isfile(self.path):
with open(self.path, 'r') as stream:
try:
contents = yaml.load(stream)
except yaml.YAMLError as e:
print(e)
return contents
def __create_patch(self, raw_data, base_directory, repo_path, paths_prefix):
author = raw_data['author']
if author is None: # Shouldn't actually happen.
author = 'Anonymous <anonymous@electronjs.org>'
relative_file_path = raw_data['file']
absolute_file_path = os.path.join(base_directory, relative_file_path)
# Use a patch file path as a commit summary
# and optional description as a commit body.
description = relative_file_path
if raw_data['description'] is not None:
description += '\n\n' + raw_data['description']
return Patch(absolute_file_path, repo_path, paths_prefix=paths_prefix, author=author, description=description)
def __create_patches_list(self):
config_contents = self.__parse()
if config_contents is None:
return None
relative_repo_path = os.path.normpath(config_contents['repo'])
absolute_repo_path = os.path.join(self.project_root, relative_repo_path)
# If the 'repo' path is not really a git repository,
# then use that path as a prefix for patched files.
paths_prefix = None
if not git.is_repo_root(absolute_repo_path):
assert(git.is_repo_root(self.project_root))
absolute_repo_path = self.project_root
paths_prefix = relative_repo_path
patches_data = config_contents['patches']
base_directory = os.path.abspath(os.path.dirname(self.path))
patches = [self.__create_patch(data, base_directory, absolute_repo_path, paths_prefix) for data in patches_data]
patches_list = PatchesList(repo_path=absolute_repo_path, patches=patches)
return patches_list
def get_patches_list(self):
if self.patches_list is not None:
return self.patches_list
patches_list = self.__create_patches_list()
self.patches_list = patches_list
return patches_list