337 lines
12 KiB
Python
337 lines
12 KiB
Python
# Copyright (c) 2014 The Chromium Embedded Framework Authors. All rights
|
|
# reserved. Use of this source code is governed by a BSD-style license that
|
|
# can be found in the LICENSE file.
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import print_function
|
|
from io import open
|
|
from optparse import Option, OptionParser, OptionValueError
|
|
import os
|
|
import re
|
|
import sys
|
|
from exec_util import exec_cmd
|
|
from file_util import copy_file, move_file, read_file, remove_file
|
|
import git_util as git
|
|
|
|
backup_ext = '.cefbak'
|
|
|
|
|
|
def msg(message):
|
|
""" Output a message. """
|
|
sys.stdout.write('--> ' + message + "\n")
|
|
|
|
|
|
def linebreak():
|
|
""" Output a line break. """
|
|
sys.stdout.write('-' * 80 + "\n")
|
|
|
|
|
|
def warn(message):
|
|
""" Output a warning. """
|
|
linebreak()
|
|
sys.stdout.write('!!!! WARNING: ' + message + "\n")
|
|
linebreak()
|
|
|
|
|
|
def extract_paths(file):
|
|
""" Extract the list of modified paths from the patch file. """
|
|
paths = []
|
|
with open(file, 'r', encoding='utf-8') as fp:
|
|
for line in fp:
|
|
if line[:4] != '+++ ':
|
|
continue
|
|
match = re.match('^([^\t]+)', line[4:])
|
|
if not match:
|
|
continue
|
|
paths.append(match.group(1).strip())
|
|
return paths
|
|
|
|
|
|
# Cannot be loaded as a module.
|
|
if __name__ != "__main__":
|
|
sys.stderr.write('This file cannot be loaded as a module!')
|
|
sys.exit()
|
|
|
|
# Parse command-line options.
|
|
disc = """
|
|
This utility updates existing patch files.
|
|
"""
|
|
|
|
|
|
# Support options with multiple arguments.
|
|
class MultipleOption(Option):
|
|
ACTIONS = Option.ACTIONS + ("extend",)
|
|
STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",)
|
|
TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",)
|
|
ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",)
|
|
|
|
def take_action(self, action, dest, opt, value, values, parser):
|
|
if action == "extend":
|
|
values.ensure_value(dest, []).append(value)
|
|
else:
|
|
Option.take_action(self, action, dest, opt, value, values, parser)
|
|
|
|
|
|
parser = OptionParser(option_class=MultipleOption, description=disc)
|
|
parser.add_option(
|
|
'--resave',
|
|
action='store_true',
|
|
dest='resave',
|
|
default=False,
|
|
help='resave existing patch files to pick up manual changes')
|
|
parser.add_option(
|
|
'--reapply',
|
|
action='store_true',
|
|
dest='reapply',
|
|
default=False,
|
|
help='reapply the patch without first reverting changes')
|
|
parser.add_option(
|
|
'--revert',
|
|
action='store_true',
|
|
dest='revert',
|
|
default=False,
|
|
help='revert all changes from existing patch files')
|
|
parser.add_option(
|
|
'--backup',
|
|
action='store_true',
|
|
dest='backup',
|
|
default=False,
|
|
help='backup patched files. Used in combination with --revert.')
|
|
parser.add_option(
|
|
'--restore',
|
|
action='store_true',
|
|
dest='restore',
|
|
default=False,
|
|
help='restore backup of patched files that have not changed. If a backup has ' +\
|
|
'changed the patch file will be resaved. Used in combination with --reapply.')
|
|
parser.add_option(
|
|
'--patch',
|
|
action='extend',
|
|
dest='patch',
|
|
type='string',
|
|
default=[],
|
|
help='optional patch name to process (multiples allowed)')
|
|
parser.add_option(
|
|
'--add',
|
|
action='extend',
|
|
dest='add',
|
|
type='string',
|
|
default=[],
|
|
help='optional relative file paths to add (multiples allowed). Used in ' +\
|
|
'combination with --resave and a single --patch value.')
|
|
(options, args) = parser.parse_args()
|
|
|
|
if options.resave and options.revert:
|
|
print('Invalid combination of options.')
|
|
parser.print_help(sys.stderr)
|
|
sys.exit()
|
|
|
|
if len(options.add) > 0 and (len(options.patch) != 1 or not options.resave):
|
|
print('--add can only be used with --resave and a single --patch value.')
|
|
parser.print_help(sys.stderr)
|
|
sys.exit()
|
|
|
|
# The CEF root directory is the parent directory of _this_ script.
|
|
cef_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
|
src_dir = os.path.abspath(os.path.join(cef_dir, os.pardir))
|
|
|
|
# Determine the type of Chromium checkout.
|
|
if not git.is_checkout(src_dir):
|
|
raise Exception('Not a valid checkout: %s' % src_dir)
|
|
|
|
patch_dir = os.path.join(cef_dir, 'patch')
|
|
patch_cfg = os.path.join(patch_dir, 'patch.cfg')
|
|
if not os.path.isfile(patch_cfg):
|
|
raise Exception('File does not exist: %s' % patch_cfg)
|
|
|
|
# Read the patch configuration file.
|
|
msg('Reading patch config %s' % patch_cfg)
|
|
scope = {}
|
|
exec (compile(open(patch_cfg, "rb").read(), patch_cfg, 'exec'), scope)
|
|
patches = scope["patches"]
|
|
|
|
failed_patches = {}
|
|
|
|
# Read each individual patch file.
|
|
patches_dir = os.path.join(patch_dir, 'patches')
|
|
for patch in patches:
|
|
# If specific patch names are specified only process those patches.
|
|
if options.patch and not patch['name'] in options.patch:
|
|
continue
|
|
|
|
sys.stdout.write('\n')
|
|
patch_file = os.path.join(patches_dir, patch['name'] + '.patch')
|
|
|
|
if os.path.isfile(patch_file):
|
|
msg('Reading patch file %s' % patch_file)
|
|
if 'path' in patch:
|
|
patch_root_abs = os.path.abspath(os.path.join(src_dir, patch['path']))
|
|
if not os.path.isdir(patch_root_abs):
|
|
line = 'Target directory does not exist: %s' % patch_root_abs
|
|
msg(line)
|
|
if options.resave:
|
|
# Report as a fatal error for manual resave only, as the missing
|
|
# directory may be platform-specific.
|
|
failed_patches[patch['name']] = [line]
|
|
continue
|
|
else:
|
|
patch_root_abs = src_dir
|
|
|
|
# Retrieve the list of paths modified by the patch file.
|
|
patch_paths = extract_paths(patch_file)
|
|
|
|
# List of paths added by the patch file.
|
|
added_paths = []
|
|
|
|
# True if any backed up files have changed.
|
|
has_backup_changes = False
|
|
|
|
if not options.resave:
|
|
if not options.reapply:
|
|
# Revert any changes to existing files in the patch.
|
|
for patch_path in patch_paths:
|
|
patch_path_abs = os.path.abspath(os.path.join(patch_root_abs, \
|
|
patch_path))
|
|
if os.path.exists(patch_path_abs):
|
|
if options.backup:
|
|
backup_path_abs = patch_path_abs + backup_ext
|
|
if not os.path.exists(backup_path_abs):
|
|
msg('Creating backup of %s' % patch_path_abs)
|
|
copy_file(patch_path_abs, backup_path_abs)
|
|
else:
|
|
msg('Skipping backup of %s' % patch_path_abs)
|
|
|
|
msg('Reverting changes to %s' % patch_path_abs)
|
|
cmd = 'git checkout -- %s' % (patch_path_abs)
|
|
result = exec_cmd(cmd, patch_root_abs)
|
|
if result['err'] != '':
|
|
msg('Failed to revert file: %s' % result['err'])
|
|
msg('Deleting file %s' % patch_path_abs)
|
|
os.remove(patch_path_abs)
|
|
added_paths.append(patch_path_abs)
|
|
if result['out'] != '':
|
|
sys.stdout.write(result['out'])
|
|
else:
|
|
msg('Skipping non-existing file %s' % patch_path_abs)
|
|
added_paths.append(patch_path_abs)
|
|
|
|
if not options.revert:
|
|
# Chromium files are occasionally (incorrectly) checked in with Windows
|
|
# line endings. This will cause the patch tool to fail when attempting
|
|
# to patch those files on Posix systems. Convert any such files to Posix
|
|
# line endings before applying the patch.
|
|
converted_files = []
|
|
for patch_path in patch_paths:
|
|
patch_path_abs = os.path.abspath(os.path.join(patch_root_abs, \
|
|
patch_path))
|
|
if os.path.exists(patch_path_abs):
|
|
with open(patch_path_abs, 'r', encoding='utf-8') as fp:
|
|
contents = fp.read()
|
|
if "\r\n" in contents:
|
|
msg('Converting to Posix line endings for %s' % patch_path_abs)
|
|
converted_files.append(patch_path_abs)
|
|
contents = contents.replace("\r\n", "\n")
|
|
with open(patch_path_abs, 'wb') as fp:
|
|
fp.write(contents)
|
|
|
|
# Apply the patch file.
|
|
msg('Applying patch to %s' % patch_root_abs)
|
|
patch_string = open(patch_file, 'rb').read()
|
|
result = exec_cmd('patch -p0', patch_root_abs, patch_string)
|
|
|
|
if len(converted_files) > 0:
|
|
# Restore Windows line endings in converted files so that the diff is
|
|
# correct if/when the patch file is re-saved.
|
|
for patch_path_abs in converted_files:
|
|
with open(patch_path_abs, 'rb') as fp:
|
|
contents = fp.read()
|
|
msg('Converting to Windows line endings for %s' % patch_path_abs)
|
|
contents = contents.replace("\n", "\r\n")
|
|
with open(patch_path_abs, 'wb') as fp:
|
|
fp.write(contents)
|
|
|
|
if result['err'] != '':
|
|
raise Exception('Failed to apply patch file: %s' % result['err'])
|
|
sys.stdout.write(result['out'])
|
|
if result['out'].find('FAILED') != -1:
|
|
failed_lines = []
|
|
for line in result['out'].split('\n'):
|
|
if line.find('FAILED') != -1:
|
|
failed_lines.append(line.strip())
|
|
warn('Failed to apply %s, fix manually and run with --resave' % \
|
|
patch['name'])
|
|
failed_patches[patch['name']] = failed_lines
|
|
continue
|
|
|
|
if options.restore:
|
|
# Restore from backup if a backup exists.
|
|
for patch_path in patch_paths:
|
|
patch_path_abs = os.path.abspath(os.path.join(patch_root_abs, \
|
|
patch_path))
|
|
backup_path_abs = patch_path_abs + backup_ext
|
|
if os.path.exists(backup_path_abs):
|
|
if read_file(patch_path_abs) == read_file(backup_path_abs):
|
|
msg('Restoring backup of %s' % patch_path_abs)
|
|
remove_file(patch_path_abs)
|
|
move_file(backup_path_abs, patch_path_abs)
|
|
else:
|
|
msg('Discarding backup of %s' % patch_path_abs)
|
|
remove_file(backup_path_abs)
|
|
has_backup_changes = True
|
|
else:
|
|
msg('No backup of %s' % patch_path_abs)
|
|
|
|
if (not options.revert and not options.reapply) or has_backup_changes:
|
|
if len(options.add) > 0:
|
|
# Add additional requested files to the patch.
|
|
for patch_path in options.add:
|
|
patch_path_abs = os.path.abspath(os.path.join(patch_root_abs, \
|
|
patch_path))
|
|
if os.path.exists(patch_path_abs):
|
|
msg('Adding file %s' % patch_path_abs)
|
|
patch_paths.append(patch_path)
|
|
else:
|
|
msg('Skipping non-existing file %s' % patch_path_abs)
|
|
|
|
msg('Saving changes to %s' % patch_file)
|
|
if added_paths:
|
|
# Inform git of the added paths so they appear in the patch file.
|
|
cmd = 'git add -N %s' % ' '.join(added_paths)
|
|
result = exec_cmd(cmd, patch_root_abs)
|
|
if result['err'] != '' and result['err'].find('warning:') != 0:
|
|
raise Exception('Failed to add paths: %s' % result['err'])
|
|
|
|
# Re-create the patch file.
|
|
patch_paths_str = ' '.join(patch_paths)
|
|
cmd = 'git diff --no-prefix --relative %s' % patch_paths_str
|
|
result = exec_cmd(cmd, patch_root_abs)
|
|
if result['err'] != '' and result['err'].find('warning:') != 0:
|
|
raise Exception('Failed to create patch file: %s' % result['err'])
|
|
|
|
if "\r\n" in result['out']:
|
|
# Patch files should always be saved with Posix line endings.
|
|
# This will avoid problems when attempting to re-apply the patch
|
|
# file on Posix systems.
|
|
msg('Converting to Posix line endings for %s' % patch_file)
|
|
result['out'] = result['out'].replace("\r\n", "\n")
|
|
|
|
f = open(patch_file, 'w', encoding='utf-8')
|
|
f.write(result['out'])
|
|
f.close()
|
|
else:
|
|
raise Exception('Patch file does not exist: %s' % patch_file)
|
|
|
|
if len(failed_patches) > 0:
|
|
sys.stdout.write("\n")
|
|
linebreak()
|
|
sys.stdout.write("!!!! FAILED PATCHES, fix manually and run with --resave\n")
|
|
for name in sorted(failed_patches.keys()):
|
|
sys.stdout.write("%s:\n" % name)
|
|
for line in failed_patches[name]:
|
|
if sys.platform == 'win32' and line.find('.rej') > 0:
|
|
# Convert paths to use Windows-style separator.
|
|
line = line.replace('/', '\\')
|
|
sys.stdout.write(" %s\n" % line)
|
|
linebreak()
|
|
sys.exit(1)
|