1#!/usr/bin/env python 2# Copyright (c) 2012 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Moves C++ files to a new location, updating any include paths that point 7to them, and re-ordering headers as needed. If multiple source files are 8specified, the destination must be a directory. Updates include guards in 9moved header files. Assumes Chromium coding style. 10 11Attempts to update paths used in .gyp(i) files, but does not reorder 12or restructure .gyp(i) files in any way. 13 14Updates full-path references to files in // comments in source files. 15 16Must run in a git checkout, as it relies on git grep for a fast way to 17find files that reference the moved file. 18""" 19 20 21import optparse 22import os 23import re 24import subprocess 25import sys 26 27import mffr 28 29if __name__ == '__main__': 30 # Need to add the directory containing sort-headers.py to the Python 31 # classpath. 32 sys.path.append(os.path.abspath(os.path.join(sys.path[0], '..'))) 33sort_headers = __import__('sort-headers') 34 35 36HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh'] 37 38 39def IsHandledFile(path): 40 return os.path.splitext(path)[1] in HANDLED_EXTENSIONS 41 42 43def MakeDestinationPath(from_path, to_path): 44 """Given the from and to paths, return a correct destination path. 45 46 The initial destination path may either a full path or a directory. 47 Also does basic sanity checks. 48 """ 49 if not IsHandledFile(from_path): 50 raise Exception('Only intended to move individual source files ' 51 '(%s does not have a recognized extension).' % 52 from_path) 53 if os.path.isdir(to_path): 54 to_path = os.path.join(to_path, os.path.basename(from_path)) 55 else: 56 dest_extension = os.path.splitext(to_path)[1] 57 if dest_extension not in HANDLED_EXTENSIONS: 58 raise Exception('Destination must be either a full path with ' 59 'a recognized extension or a directory.') 60 return to_path 61 62 63def MoveFile(from_path, to_path): 64 """Performs a git mv command to move a file from |from_path| to |to_path|. 65 """ 66 if not os.system('git mv %s %s' % (from_path, to_path)) == 0: 67 raise Exception('Fatal: Failed to run git mv command.') 68 69 70def UpdatePostMove(from_path, to_path): 71 """Given a file that has moved from |from_path| to |to_path|, 72 updates the moved file's include guard to match the new path and 73 updates all references to the file in other source files. Also tries 74 to update references in .gyp(i) files using a heuristic. 75 """ 76 # Include paths always use forward slashes. 77 from_path = from_path.replace('\\', '/') 78 to_path = to_path.replace('\\', '/') 79 80 if os.path.splitext(from_path)[1] in ['.h', '.hh']: 81 UpdateIncludeGuard(from_path, to_path) 82 83 # Update include/import references. 84 files_with_changed_includes = mffr.MultiFileFindReplace( 85 r'(#(include|import)\s*["<])%s([>"])' % re.escape(from_path), 86 r'\1%s\3' % to_path, 87 ['*.cc', '*.h', '*.m', '*.mm']) 88 89 # Reorder headers in files that changed. 90 for changed_file in files_with_changed_includes: 91 def AlwaysConfirm(a, b): return True 92 sort_headers.FixFileWithConfirmFunction(changed_file, AlwaysConfirm, True) 93 94 # Update comments; only supports // comments, which are primarily 95 # used in our code. 96 # 97 # This work takes a bit of time. If this script starts feeling too 98 # slow, one good way to speed it up is to make the comment handling 99 # optional under a flag. 100 mffr.MultiFileFindReplace( 101 r'(//.*)%s' % re.escape(from_path), 102 r'\1%s' % to_path, 103 ['*.cc', '*.h', '*.m', '*.mm']) 104 105 # Update references in .gyp(i) files. 106 def PathMinusFirstComponent(path): 107 """foo/bar/baz -> bar/baz""" 108 parts = re.split(r"[/\\]", path, 1) 109 if len(parts) == 2: 110 return parts[1] 111 else: 112 return parts[0] 113 mffr.MultiFileFindReplace( 114 r'([\'"])%s([\'"])' % re.escape(PathMinusFirstComponent(from_path)), 115 r'\1%s\2' % PathMinusFirstComponent(to_path), 116 ['*.gyp*']) 117 118 119def MakeIncludeGuardName(path_from_root): 120 """Returns an include guard name given a path from root.""" 121 guard = path_from_root.replace('/', '_') 122 guard = guard.replace('\\', '_') 123 guard = guard.replace('.', '_') 124 guard += '_' 125 return guard.upper() 126 127 128def UpdateIncludeGuard(old_path, new_path): 129 """Updates the include guard in a file now residing at |new_path|, 130 previously residing at |old_path|, with an up-to-date include guard. 131 132 Prints a warning if the update could not be completed successfully (e.g., 133 because the old include guard was not formatted correctly per Chromium style). 134 """ 135 old_guard = MakeIncludeGuardName(old_path) 136 new_guard = MakeIncludeGuardName(new_path) 137 138 with open(new_path) as f: 139 contents = f.read() 140 141 new_contents = contents.replace(old_guard, new_guard) 142 # The file should now have three instances of the new guard: two at the top 143 # of the file plus one at the bottom for the comment on the #endif. 144 if new_contents.count(new_guard) != 3: 145 print ('WARNING: Could not successfully update include guard; perhaps ' 146 'old guard is not per style guide? You will have to update the ' 147 'include guard manually. (%s)' % new_path) 148 149 with open(new_path, 'w') as f: 150 f.write(new_contents) 151 152def main(): 153 if not os.path.isdir('.git'): 154 print 'Fatal: You must run from the root of a git checkout.' 155 return 1 156 157 parser = optparse.OptionParser(usage='%prog FROM_PATH... TO_PATH') 158 parser.add_option('--already_moved', action='store_true', 159 dest='already_moved', 160 help='Causes the script to skip moving the file.') 161 parser.add_option('--no_error_for_non_source_file', action='store_false', 162 default='True', 163 dest='error_for_non_source_file', 164 help='Causes the script to simply print a warning on ' 165 'encountering a non-source file rather than raising an ' 166 'error.') 167 opts, args = parser.parse_args() 168 169 if len(args) < 2: 170 parser.print_help() 171 return 1 172 173 from_paths = args[:len(args)-1] 174 orig_to_path = args[-1] 175 176 if len(from_paths) > 1 and not os.path.isdir(orig_to_path): 177 print 'Target %s is not a directory.' % orig_to_path 178 print 179 parser.print_help() 180 return 1 181 182 for from_path in from_paths: 183 if not opts.error_for_non_source_file and not IsHandledFile(from_path): 184 print '%s does not appear to be a source file, skipping' % (from_path) 185 continue 186 to_path = MakeDestinationPath(from_path, orig_to_path) 187 if not opts.already_moved: 188 MoveFile(from_path, to_path) 189 UpdatePostMove(from_path, to_path) 190 return 0 191 192 193if __name__ == '__main__': 194 sys.exit(main()) 195