• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2014 The Chromium Embedded Framework Authors. All rights
2# reserved. Use of this source code is governed by a BSD-style license that
3# can be found in the LICENSE file.
4
5from __future__ import absolute_import
6from __future__ import print_function
7from io import open
8from optparse import Option, OptionParser, OptionValueError
9import os
10import re
11import sys
12from exec_util import exec_cmd
13from file_util import copy_file, move_file, read_file, remove_file
14import git_util as git
15
16backup_ext = '.cefbak'
17
18
19def msg(message):
20  """ Output a message. """
21  sys.stdout.write('--> ' + message + "\n")
22
23
24def linebreak():
25  """ Output a line break. """
26  sys.stdout.write('-' * 80 + "\n")
27
28
29def warn(message):
30  """ Output a warning. """
31  linebreak()
32  sys.stdout.write('!!!! WARNING: ' + message + "\n")
33  linebreak()
34
35
36def extract_paths(file):
37  """ Extract the list of modified paths from the patch file. """
38  paths = []
39  with open(file, 'r', encoding='utf-8') as fp:
40    for line in fp:
41      if line[:4] != '+++ ':
42        continue
43      match = re.match('^([^\t]+)', line[4:])
44      if not match:
45        continue
46      paths.append(match.group(1).strip())
47  return paths
48
49
50# Cannot be loaded as a module.
51if __name__ != "__main__":
52  sys.stderr.write('This file cannot be loaded as a module!')
53  sys.exit()
54
55# Parse command-line options.
56disc = """
57This utility updates existing patch files.
58"""
59
60
61# Support options with multiple arguments.
62class MultipleOption(Option):
63  ACTIONS = Option.ACTIONS + ("extend",)
64  STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",)
65  TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",)
66  ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",)
67
68  def take_action(self, action, dest, opt, value, values, parser):
69    if action == "extend":
70      values.ensure_value(dest, []).append(value)
71    else:
72      Option.take_action(self, action, dest, opt, value, values, parser)
73
74
75parser = OptionParser(option_class=MultipleOption, description=disc)
76parser.add_option(
77    '--resave',
78    action='store_true',
79    dest='resave',
80    default=False,
81    help='resave existing patch files to pick up manual changes')
82parser.add_option(
83    '--reapply',
84    action='store_true',
85    dest='reapply',
86    default=False,
87    help='reapply the patch without first reverting changes')
88parser.add_option(
89    '--revert',
90    action='store_true',
91    dest='revert',
92    default=False,
93    help='revert all changes from existing patch files')
94parser.add_option(
95    '--backup',
96    action='store_true',
97    dest='backup',
98    default=False,
99    help='backup patched files. Used in combination with --revert.')
100parser.add_option(
101    '--restore',
102    action='store_true',
103    dest='restore',
104    default=False,
105    help='restore backup of patched files that have not changed. If a backup has ' +\
106         'changed the patch file will be resaved. Used in combination with --reapply.')
107parser.add_option(
108    '--patch',
109    action='extend',
110    dest='patch',
111    type='string',
112    default=[],
113    help='optional patch name to process (multiples allowed)')
114parser.add_option(
115    '--add',
116    action='extend',
117    dest='add',
118    type='string',
119    default=[],
120    help='optional relative file paths to add (multiples allowed). Used in ' +\
121         'combination with --resave and a single --patch value.')
122(options, args) = parser.parse_args()
123
124if options.resave and options.revert:
125  print('Invalid combination of options.')
126  parser.print_help(sys.stderr)
127  sys.exit()
128
129if len(options.add) > 0 and (len(options.patch) != 1 or not options.resave):
130  print('--add can only be used with --resave and a single --patch value.')
131  parser.print_help(sys.stderr)
132  sys.exit()
133
134# The CEF root directory is the parent directory of _this_ script.
135cef_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
136src_dir = os.path.abspath(os.path.join(cef_dir, os.pardir))
137
138# Determine the type of Chromium checkout.
139if not git.is_checkout(src_dir):
140  raise Exception('Not a valid checkout: %s' % src_dir)
141
142patch_dir = os.path.join(cef_dir, 'patch')
143patch_cfg = os.path.join(patch_dir, 'patch.cfg')
144if not os.path.isfile(patch_cfg):
145  raise Exception('File does not exist: %s' % patch_cfg)
146
147# Read the patch configuration file.
148msg('Reading patch config %s' % patch_cfg)
149scope = {}
150exec (compile(open(patch_cfg, "rb").read(), patch_cfg, 'exec'), scope)
151patches = scope["patches"]
152
153failed_patches = {}
154
155# Read each individual patch file.
156patches_dir = os.path.join(patch_dir, 'patches')
157for patch in patches:
158  # If specific patch names are specified only process those patches.
159  if options.patch and not patch['name'] in options.patch:
160    continue
161
162  sys.stdout.write('\n')
163  patch_file = os.path.join(patches_dir, patch['name'] + '.patch')
164
165  if os.path.isfile(patch_file):
166    msg('Reading patch file %s' % patch_file)
167    if 'path' in patch:
168      patch_root_abs = os.path.abspath(os.path.join(src_dir, patch['path']))
169    else:
170      patch_root_abs = src_dir
171
172    # Retrieve the list of paths modified by the patch file.
173    patch_paths = extract_paths(patch_file)
174
175    # List of paths added by the patch file.
176    added_paths = []
177
178    # True if any backed up files have changed.
179    has_backup_changes = False
180
181    if not options.resave:
182      if not options.reapply:
183        # Revert any changes to existing files in the patch.
184        for patch_path in patch_paths:
185          patch_path_abs = os.path.abspath(os.path.join(patch_root_abs, \
186                                                        patch_path))
187          if os.path.exists(patch_path_abs):
188            if options.backup:
189              backup_path_abs = patch_path_abs + backup_ext
190              if not os.path.exists(backup_path_abs):
191                msg('Creating backup of %s' % patch_path_abs)
192                copy_file(patch_path_abs, backup_path_abs)
193              else:
194                msg('Skipping backup of %s' % patch_path_abs)
195
196            msg('Reverting changes to %s' % patch_path_abs)
197            cmd = 'git checkout -- %s' % (patch_path_abs)
198            result = exec_cmd(cmd, patch_root_abs)
199            if result['err'] != '':
200              msg('Failed to revert file: %s' % result['err'])
201              msg('Deleting file %s' % patch_path_abs)
202              os.remove(patch_path_abs)
203              added_paths.append(patch_path_abs)
204            if result['out'] != '':
205              sys.stdout.write(result['out'])
206          else:
207            msg('Skipping non-existing file %s' % patch_path_abs)
208            added_paths.append(patch_path_abs)
209
210      if not options.revert:
211        # Chromium files are occasionally (incorrectly) checked in with Windows
212        # line endings. This will cause the patch tool to fail when attempting
213        # to patch those files on Posix systems. Convert any such files to Posix
214        # line endings before applying the patch.
215        converted_files = []
216        for patch_path in patch_paths:
217          patch_path_abs = os.path.abspath(os.path.join(patch_root_abs, \
218                                                        patch_path))
219          if os.path.exists(patch_path_abs):
220            with open(patch_path_abs, 'r', encoding='utf-8') as fp:
221              contents = fp.read()
222            if "\r\n" in contents:
223              msg('Converting to Posix line endings for %s' % patch_path_abs)
224              converted_files.append(patch_path_abs)
225              contents = contents.replace("\r\n", "\n")
226              with open(patch_path_abs, 'wb') as fp:
227                fp.write(contents)
228
229        # Apply the patch file.
230        msg('Applying patch to %s' % patch_root_abs)
231        patch_string = open(patch_file, 'rb').read()
232        result = exec_cmd('patch -p0', patch_root_abs, patch_string)
233
234        if len(converted_files) > 0:
235          # Restore Windows line endings in converted files so that the diff is
236          # correct if/when the patch file is re-saved.
237          for patch_path_abs in converted_files:
238            with open(patch_path_abs, 'rb') as fp:
239              contents = fp.read()
240            msg('Converting to Windows line endings for %s' % patch_path_abs)
241            contents = contents.replace("\n", "\r\n")
242            with open(patch_path_abs, 'wb') as fp:
243              fp.write(contents)
244
245        if result['err'] != '':
246          raise Exception('Failed to apply patch file: %s' % result['err'])
247        sys.stdout.write(result['out'])
248        if result['out'].find('FAILED') != -1:
249          failed_lines = []
250          for line in result['out'].split('\n'):
251            if line.find('FAILED') != -1:
252              failed_lines.append(line.strip())
253          warn('Failed to apply %s, fix manually and run with --resave' % \
254               patch['name'])
255          failed_patches[patch['name']] = failed_lines
256          continue
257
258        if options.restore:
259          # Restore from backup if a backup exists.
260          for patch_path in patch_paths:
261            patch_path_abs = os.path.abspath(os.path.join(patch_root_abs, \
262                                                          patch_path))
263            backup_path_abs = patch_path_abs + backup_ext
264            if os.path.exists(backup_path_abs):
265              if read_file(patch_path_abs) == read_file(backup_path_abs):
266                msg('Restoring backup of %s' % patch_path_abs)
267                remove_file(patch_path_abs)
268                move_file(backup_path_abs, patch_path_abs)
269              else:
270                msg('Discarding backup of %s' % patch_path_abs)
271                remove_file(backup_path_abs)
272                has_backup_changes = True
273            else:
274              msg('No backup of %s' % patch_path_abs)
275
276    if (not options.revert and not options.reapply) or has_backup_changes:
277      if len(options.add) > 0:
278        # Add additional requested files to the patch.
279        for patch_path in options.add:
280          patch_path_abs = os.path.abspath(os.path.join(patch_root_abs, \
281                                                        patch_path))
282          if os.path.exists(patch_path_abs):
283            msg('Adding file %s' % patch_path_abs)
284            patch_paths.append(patch_path)
285          else:
286            msg('Skipping non-existing file %s' % patch_path_abs)
287
288      msg('Saving changes to %s' % patch_file)
289      if added_paths:
290        # Inform git of the added paths so they appear in the patch file.
291        cmd = 'git add -N %s' % ' '.join(added_paths)
292        result = exec_cmd(cmd, patch_root_abs)
293        if result['err'] != '' and result['err'].find('warning:') != 0:
294          raise Exception('Failed to add paths: %s' % result['err'])
295
296      # Re-create the patch file.
297      patch_paths_str = ' '.join(patch_paths)
298      cmd = 'git diff --no-prefix --relative %s' % patch_paths_str
299      result = exec_cmd(cmd, patch_root_abs)
300      if result['err'] != '' and result['err'].find('warning:') != 0:
301        raise Exception('Failed to create patch file: %s' % result['err'])
302
303      if "\r\n" in result['out']:
304        # Patch files should always be saved with Posix line endings.
305        # This will avoid problems when attempting to re-apply the patch
306        # file on Posix systems.
307        msg('Converting to Posix line endings for %s' % patch_file)
308        result['out'] = result['out'].replace("\r\n", "\n")
309
310      f = open(patch_file, 'w', encoding='utf-8')
311      f.write(result['out'])
312      f.close()
313  else:
314    raise Exception('Patch file does not exist: %s' % patch_file)
315
316if len(failed_patches) > 0:
317  sys.stdout.write("\n")
318  linebreak()
319  sys.stdout.write("!!!! FAILED PATCHES, fix manually and run with --resave\n")
320  for name in sorted(failed_patches.keys()):
321    sys.stdout.write("%s:\n" % name)
322    for line in failed_patches[name]:
323      if sys.platform == 'win32' and line.find('.rej') > 0:
324        # Convert paths to use Windows-style separator.
325        line = line.replace('/', '\\')
326      sys.stdout.write("  %s\n" % line)
327  linebreak()
328  sys.exit(1)
329