OLD | NEW |
(Empty) | |
| 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 |
| 7 to them, and re-ordering headers as needed. If multiple source files are |
| 8 specified, the destination must be a directory. Updates include guards in |
| 9 moved header files. Assumes Chromium coding style. |
| 10 |
| 11 Attempts to update paths used in .gyp(i) files, but does not reorder |
| 12 or restructure .gyp(i) files in any way. |
| 13 |
| 14 Updates full-path references to files in // comments in source files. |
| 15 |
| 16 Must run in a git checkout, as it relies on git grep for a fast way to |
| 17 find files that reference the moved file. |
| 18 """ |
| 19 |
| 20 |
| 21 import optparse |
| 22 import os |
| 23 import re |
| 24 import subprocess |
| 25 import sys |
| 26 |
| 27 import mffr |
| 28 |
| 29 if __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], '..'))) |
| 33 sort_headers = __import__('sort-headers') |
| 34 import sort_sources |
| 35 |
| 36 |
| 37 HANDLED_EXTENSIONS = ['.cc', '.mm', '.h', '.hh', '.cpp'] |
| 38 |
| 39 |
| 40 def IsHandledFile(path): |
| 41 return os.path.splitext(path)[1] in HANDLED_EXTENSIONS |
| 42 |
| 43 |
| 44 def MakeDestinationPath(from_path, to_path): |
| 45 """Given the from and to paths, return a correct destination path. |
| 46 |
| 47 The initial destination path may either a full path or a directory. |
| 48 Also does basic sanity checks. |
| 49 """ |
| 50 if not IsHandledFile(from_path): |
| 51 raise Exception('Only intended to move individual source files ' |
| 52 '(%s does not have a recognized extension).' % |
| 53 from_path) |
| 54 |
| 55 # Remove '.', '..', etc. |
| 56 to_path = os.path.normpath(to_path) |
| 57 |
| 58 if os.path.isdir(to_path): |
| 59 to_path = os.path.join(to_path, os.path.basename(from_path)) |
| 60 else: |
| 61 dest_extension = os.path.splitext(to_path)[1] |
| 62 if dest_extension not in HANDLED_EXTENSIONS: |
| 63 raise Exception('Destination must be either a full path with ' |
| 64 'a recognized extension or a directory.') |
| 65 return to_path |
| 66 |
| 67 |
| 68 def MoveFile(from_path, to_path): |
| 69 """Performs a git mv command to move a file from |from_path| to |to_path|. |
| 70 """ |
| 71 if not os.system('git mv %s %s' % (from_path, to_path)) == 0: |
| 72 raise Exception('Fatal: Failed to run git mv command.') |
| 73 |
| 74 |
| 75 def UpdatePostMove(from_path, to_path): |
| 76 """Given a file that has moved from |from_path| to |to_path|, |
| 77 updates the moved file's include guard to match the new path and |
| 78 updates all references to the file in other source files. Also tries |
| 79 to update references in .gyp(i) files using a heuristic. |
| 80 """ |
| 81 # Include paths always use forward slashes. |
| 82 from_path = from_path.replace('\\', '/') |
| 83 to_path = to_path.replace('\\', '/') |
| 84 |
| 85 if os.path.splitext(from_path)[1] in ['.h', '.hh']: |
| 86 UpdateIncludeGuard(from_path, to_path) |
| 87 |
| 88 # Update include/import references. |
| 89 files_with_changed_includes = mffr.MultiFileFindReplace( |
| 90 r'(#(include|import)\s*["<])%s([>"])' % re.escape(from_path), |
| 91 r'\1%s\3' % to_path, |
| 92 ['*.cc', '*.h', '*.m', '*.mm', '*.cpp']) |
| 93 |
| 94 # Reorder headers in files that changed. |
| 95 for changed_file in files_with_changed_includes: |
| 96 def AlwaysConfirm(a, b): return True |
| 97 sort_headers.FixFileWithConfirmFunction(changed_file, AlwaysConfirm, True) |
| 98 |
| 99 # Update comments; only supports // comments, which are primarily |
| 100 # used in our code. |
| 101 # |
| 102 # This work takes a bit of time. If this script starts feeling too |
| 103 # slow, one good way to speed it up is to make the comment handling |
| 104 # optional under a flag. |
| 105 mffr.MultiFileFindReplace( |
| 106 r'(//.*)%s' % re.escape(from_path), |
| 107 r'\1%s' % to_path, |
| 108 ['*.cc', '*.h', '*.m', '*.mm', '*.cpp']) |
| 109 |
| 110 # Update references in GYP and BUILD.gn files. |
| 111 # |
| 112 # GYP files are mostly located under the first level directory (ex. |
| 113 # chrome/chrome_browser.gypi), but sometimes they are located in |
| 114 # directories at a deeper level (ex. extensions/shell/app_shell.gypi). On |
| 115 # the other hand, BUILD.gn files can be placed in any directories. |
| 116 # |
| 117 # Paths in a GYP or BUILD.gn file are relative to the directory where the |
| 118 # file is placed. |
| 119 # |
| 120 # For instance, "chrome/browser/chromeos/device_uma.h" is listed as |
| 121 # "browser/chromeos/device_uma.h" in "chrome/chrome_browser_chromeos.gypi", |
| 122 # but it's listed as "device_uma.h" in "chrome/browser/chromeos/BUILD.gn". |
| 123 # |
| 124 # To handle this, the code here will visit directories from the top level |
| 125 # src directory to the directory of |from_path| and try to update GYP and |
| 126 # BUILD.gn files in each directory. |
| 127 # |
| 128 # The code only handles files moved/renamed within the same build file. If |
| 129 # files are moved beyond the same build file, the affected build files |
| 130 # should be fixed manually. |
| 131 def SplitByFirstComponent(path): |
| 132 """'foo/bar/baz' -> ('foo', 'bar/baz') |
| 133 'bar' -> ('bar', '') |
| 134 '' -> ('', '') |
| 135 """ |
| 136 parts = re.split(r"[/\\]", path, 1) |
| 137 if len(parts) == 2: |
| 138 return (parts[0], parts[1]) |
| 139 else: |
| 140 return (parts[0], '') |
| 141 |
| 142 visiting_directory = '' |
| 143 from_rest = from_path |
| 144 to_rest = to_path |
| 145 while True: |
| 146 files_with_changed_sources = mffr.MultiFileFindReplace( |
| 147 r'([\'"])%s([\'"])' % from_rest, |
| 148 r'\1%s\2' % to_rest, |
| 149 [os.path.join(visiting_directory, 'BUILD.gn'), |
| 150 os.path.join(visiting_directory, '*.gyp*')]) |
| 151 for changed_file in files_with_changed_sources: |
| 152 sort_sources.ProcessFile(changed_file, should_confirm=False) |
| 153 from_first, from_rest = SplitByFirstComponent(from_rest) |
| 154 to_first, to_rest = SplitByFirstComponent(to_rest) |
| 155 visiting_directory = os.path.join(visiting_directory, from_first) |
| 156 if not from_rest or not to_rest: |
| 157 break |
| 158 |
| 159 |
| 160 def MakeIncludeGuardName(path_from_root): |
| 161 """Returns an include guard name given a path from root.""" |
| 162 guard = path_from_root.replace('/', '_') |
| 163 guard = guard.replace('\\', '_') |
| 164 guard = guard.replace('.', '_') |
| 165 guard += '_' |
| 166 return guard.upper() |
| 167 |
| 168 |
| 169 def UpdateIncludeGuard(old_path, new_path): |
| 170 """Updates the include guard in a file now residing at |new_path|, |
| 171 previously residing at |old_path|, with an up-to-date include guard. |
| 172 |
| 173 Prints a warning if the update could not be completed successfully (e.g., |
| 174 because the old include guard was not formatted correctly per Chromium style). |
| 175 """ |
| 176 old_guard = MakeIncludeGuardName(old_path) |
| 177 new_guard = MakeIncludeGuardName(new_path) |
| 178 |
| 179 with open(new_path) as f: |
| 180 contents = f.read() |
| 181 |
| 182 new_contents = contents.replace(old_guard, new_guard) |
| 183 # The file should now have three instances of the new guard: two at the top |
| 184 # of the file plus one at the bottom for the comment on the #endif. |
| 185 if new_contents.count(new_guard) != 3: |
| 186 print ('WARNING: Could not successfully update include guard; perhaps ' |
| 187 'old guard is not per style guide? You will have to update the ' |
| 188 'include guard manually. (%s)' % new_path) |
| 189 |
| 190 with open(new_path, 'w') as f: |
| 191 f.write(new_contents) |
| 192 |
| 193 def main(): |
| 194 if not os.path.isdir('.git'): |
| 195 print 'Fatal: You must run from the root of a git checkout.' |
| 196 return 1 |
| 197 |
| 198 parser = optparse.OptionParser(usage='%prog FROM_PATH... TO_PATH') |
| 199 parser.add_option('--already_moved', action='store_true', |
| 200 dest='already_moved', |
| 201 help='Causes the script to skip moving the file.') |
| 202 parser.add_option('--no_error_for_non_source_file', action='store_false', |
| 203 default='True', |
| 204 dest='error_for_non_source_file', |
| 205 help='Causes the script to simply print a warning on ' |
| 206 'encountering a non-source file rather than raising an ' |
| 207 'error.') |
| 208 opts, args = parser.parse_args() |
| 209 |
| 210 if len(args) < 2: |
| 211 parser.print_help() |
| 212 return 1 |
| 213 |
| 214 from_paths = args[:len(args)-1] |
| 215 orig_to_path = args[-1] |
| 216 |
| 217 if len(from_paths) > 1 and not os.path.isdir(orig_to_path): |
| 218 print 'Target %s is not a directory.' % orig_to_path |
| 219 print |
| 220 parser.print_help() |
| 221 return 1 |
| 222 |
| 223 for from_path in from_paths: |
| 224 if not opts.error_for_non_source_file and not IsHandledFile(from_path): |
| 225 print '%s does not appear to be a source file, skipping' % (from_path) |
| 226 continue |
| 227 to_path = MakeDestinationPath(from_path, orig_to_path) |
| 228 if not opts.already_moved: |
| 229 MoveFile(from_path, to_path) |
| 230 UpdatePostMove(from_path, to_path) |
| 231 return 0 |
| 232 |
| 233 |
| 234 if __name__ == '__main__': |
| 235 sys.exit(main()) |
OLD | NEW |