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. | |
Nico
2013/03/27 16:35:29
Doesn't say why this is used instead of tooling's
dcheng
2013/03/28 00:10:17
Done now =)
| |
5 | |
6 import collections | |
7 import functools | |
8 import multiprocessing | |
9 import subprocess | |
10 import sys | |
11 | |
12 | |
13 """Represents an edit to a file.""" | |
14 Edit= collections.namedtuple( | |
15 'Edit', ('edit_type', 'offset', 'length', 'replacement')) | |
16 | |
17 | |
18 """Gets the list of files to run the tool over from git.""" | |
19 def _GetFilesFromGit(paths = None): | |
20 try: | |
21 args = ['git', 'ls-files'] | |
22 if paths: | |
23 args.extend(paths) | |
24 command = subprocess.Popen(args, stdout=subprocess.PIPE) | |
25 output, _ = command.communicate() | |
26 return [f for f in output.splitlines() if f[-3:] == '.cc'] | |
Nico
2013/03/27 16:35:29
f.endswith('.cc')
(there's also .m, .mm, .cpp, .h
dcheng
2013/03/28 00:10:17
1) I didn't really want to parse compile_commands.
| |
27 except OSError: | |
28 # TODO(dcheng): Handle errors. | |
29 sys.exit(1) | |
30 | |
31 | |
32 """Executes the tool. | |
33 | |
34 This is outside the class so it can be pickled.""" | |
35 def _ExecuteTool(toolname, build_directory, filename): | |
36 # TODO(dcheng): Scrape output and handle errors. | |
37 command = subprocess.Popen((toolname, '-p', build_directory, filename), | |
38 stdout=subprocess.PIPE, | |
39 stderr=subprocess.PIPE) | |
40 stdout, stderr = command.communicate() | |
41 if command.returncode != 0: | |
42 return {'status': False, 'filename': filename, 'stderr': stderr} | |
43 else: | |
44 return {'status': True, 'stdout': stdout} | |
45 | |
46 | |
47 """God class that does everything.""" | |
48 class _CompilerDispatcher(object): | |
49 def __init__(self, toolname, build_directory, filenames): | |
50 self.__toolname = toolname | |
51 self.__build_directory = build_directory | |
52 self.__filenames = filenames | |
53 self.__success_count = 0 | |
54 self.__failed_count = 0 | |
55 self.__edits = collections.defaultdict(list) | |
56 | |
57 @property | |
58 def edits(self): | |
59 return self.__edits | |
60 | |
61 """God function that does everything.""" | |
62 def Run(self): | |
63 # TODO(dcheng): Add some timing. | |
64 pool = multiprocessing.Pool() | |
65 result_iterator = pool.imap_unordered( | |
66 functools.partial(_ExecuteTool, self.__toolname, | |
67 self.__build_directory), | |
68 self.__filenames) | |
69 for result in result_iterator: | |
70 self.__ProcessResult(result) | |
71 sys.stdout.write('\n') | |
72 sys.stdout.flush() | |
73 | |
74 """Process the result.""" | |
75 def __ProcessResult(self, result): | |
76 if result['status']: | |
77 self.__success_count += 1 | |
78 self.__AddEditsFromStdout(result['stdout']) | |
79 else: | |
80 self.__failed_count += 1 | |
81 sys.stdout.write('\nFailed to process %s\n' % result['filename']) | |
82 sys.stdout.write(result['stderr']) | |
83 sys.stdout.write('\n') | |
84 percentage = ( | |
85 float(self.__success_count + self.__failed_count) / | |
86 len(self.__filenames)) * 100 | |
87 sys.stdout.write('Succeeded: %d, Failed: %d [%.2f%%]\r' % ( | |
88 self.__success_count, self.__failed_count, percentage)) | |
89 sys.stdout.flush() | |
90 | |
91 """Extracts and add the list of edits generated on the tool's stdout.""" | |
92 def __AddEditsFromStdout(self, stdout): | |
93 lines = stdout.splitlines() | |
94 start_index = lines.index('==== BEGIN EDITS ====') | |
95 end_index = lines.index('==== END EDITS ====') | |
96 for line in lines[start_index + 1:end_index]: | |
97 edit_type, path, offset, length, replacement = line.split(':', 4) | |
98 # TODO(dcheng): [6:] is a horrible hack to trim off ../../ and is fragile. | |
99 self.__edits[path[6:]].append( | |
100 Edit(edit_type, int(offset), int(length), replacement)) | |
101 | |
102 | |
103 """Applies the edits!""" | |
104 def _ApplyEdits(edits): | |
105 edit_count = 0 | |
106 for k, v in edits.iteritems(): | |
107 # Sort the edits and iterate through them in reverse order. Sorting allows | |
108 # duplicate edits to be quickly skipped, while reversing means that | |
109 # subsequent edits don't need to have their offsets updated with each edit | |
110 # applied. | |
111 v.sort() | |
112 last_edit = None | |
113 with open(k, 'r+') as f: | |
114 contents = f.read() | |
115 for edit in reversed(v): | |
116 if edit == last_edit: | |
117 continue | |
118 last_edit = edit | |
119 # TODO(dcheng): Would bytearray be better? | |
120 contents = (contents[:edit.offset] + | |
121 edit.replacement + | |
122 contents[edit.offset + edit.length:]) | |
123 edit_count += 1 | |
124 f.seek(0) | |
125 f.truncate() | |
126 f.write(contents) | |
127 print 'Applied %d edits to %d files' % (edit_count, len(edits)) | |
128 | |
129 | |
130 def main(argv): | |
131 # TODO(dcheng): Assert that we're running from chrome/src. | |
132 filenames = frozenset(_GetFilesFromGit(argv[2:])) | |
133 dispatcher = _CompilerDispatcher(argv[0], argv[1], filenames) | |
134 dispatcher.Run() | |
135 # Filter out any files that aren't in the git repository, since it's not | |
136 # useful to modify files that aren't under source control--typically, these | |
137 # are generated files or files in a git submodule that's not part of Chrome. | |
138 filtered_edits = {k : v for k, v in dispatcher.edits.iteritems() | |
139 if k in filenames} | |
140 _ApplyEdits(filtered_edits) | |
141 | |
142 | |
143 if __name__ == '__main__': | |
144 sys.exit(main(sys.argv[1:])) | |
OLD | NEW |