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