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