OLD | NEW |
| (Empty) |
1 #!/usr/bin/python | |
2 # Copyright (c) 2012 The Native Client 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 import os | |
7 import re | |
8 import sys | |
9 | |
10 """Header Scanner. | |
11 | |
12 This module will scan a set of input sources for include dependencies. Use | |
13 the command-line switch -Ixxxx to add include paths. All filenames and paths | |
14 are expected and returned with POSIX separators. | |
15 """ | |
16 | |
17 | |
18 debug = False | |
19 | |
20 | |
21 def DebugPrint(txt): | |
22 if debug: print txt | |
23 | |
24 | |
25 class PathConverter(object): | |
26 """PathConverter does path manipulates using Posix style pathnames. | |
27 | |
28 Regardless of the native path type, all inputs and outputs to the path | |
29 functions are with POSIX style separators. | |
30 """ | |
31 def ToNativePath(self, pathname): | |
32 return os.path.sep.join(pathname.split('/')) | |
33 | |
34 def ToPosixPath(self, pathname): | |
35 return '/'.join(pathname.split(os.path.sep)) | |
36 | |
37 def isfile(self, pathname): | |
38 ospath = self.ToNativePath(pathname) | |
39 return os.path.isfile(ospath) | |
40 | |
41 def getcwd(self): | |
42 return self.ToPosixPath(os.getcwd()) | |
43 | |
44 def isabs(self, pathname): | |
45 ospath = self.ToNativePath(pathname) | |
46 return os.path.isabs(ospath) | |
47 | |
48 def isdir(self, pathname): | |
49 ospath = self.ToNativePath(pathname) | |
50 return os.path.isdir(ospath) | |
51 | |
52 def open(self, pathname): | |
53 ospath = self.ToNativePath(pathname) | |
54 return open(ospath) | |
55 | |
56 def abspath(self, pathname): | |
57 ospath = self.ToNativePath(pathname) | |
58 ospath = os.path.abspath(ospath) | |
59 return self.ToPosixPath(ospath) | |
60 | |
61 def dirname(self, pathname): | |
62 ospath = self.ToNativePath(pathname) | |
63 ospath = os.path.dirname(ospath) | |
64 return self.ToPosixPath(ospath) | |
65 | |
66 | |
67 filename_to_relative_cache = {} # (filepath, basepath) -> relpath | |
68 findfile_cache = {} # (tuple(searchdirs), cwd, file) -> filename/None | |
69 pathisfile_cache = {} # abspath -> boolean, works because fs is static | |
70 # during a run. | |
71 | |
72 | |
73 class Resolver(object): | |
74 """Resolver finds and generates relative paths for include files. | |
75 | |
76 The Resolver object provides a mechanism to to find and convert a source or | |
77 include filename into a relative path based on provided search paths. All | |
78 paths use POSIX style separator. | |
79 """ | |
80 def __init__(self, pathobj=PathConverter()): | |
81 self.search_dirs = [] | |
82 self.pathobj = pathobj | |
83 self.cwd = self.pathobj.getcwd() | |
84 self.offs = len(self.cwd) | |
85 | |
86 def AddOneDirectory(self, pathname): | |
87 """Add an include search path.""" | |
88 pathname = self.pathobj.abspath(pathname) | |
89 DebugPrint('Adding DIR: %s' % pathname) | |
90 if pathname not in self.search_dirs: | |
91 if self.pathobj.isdir(pathname): | |
92 self.search_dirs.append(pathname) | |
93 else: | |
94 # We can end up here when using the gyp generator analyzer. To avoid | |
95 # spamming only log if debug enabled. | |
96 DebugPrint('Not a directory: %s\n' % pathname) | |
97 return False | |
98 return True | |
99 | |
100 def RemoveOneDirectory(self, pathname): | |
101 """Remove an include search path.""" | |
102 pathname = self.pathobj.abspath(pathname) | |
103 DebugPrint('Removing DIR: %s' % pathname) | |
104 if pathname in self.search_dirs: | |
105 self.search_dirs.remove(pathname) | |
106 return True | |
107 | |
108 def AddDirectories(self, pathlist): | |
109 """Add list of space separated directories.""" | |
110 failed = False | |
111 dirlist = ' '.join(pathlist) | |
112 for dirname in dirlist.split(' '): | |
113 if not self.AddOneDirectory(dirname): | |
114 failed = True | |
115 return not failed | |
116 | |
117 def GetDirectories(self): | |
118 return self.search_dirs | |
119 | |
120 def RealToRelative(self, filepath, basepath): | |
121 """Returns a relative path from an absolute basepath and filepath.""" | |
122 cache_key = (filepath, basepath) | |
123 cache_result = None | |
124 if cache_key in filename_to_relative_cache: | |
125 cache_result = filename_to_relative_cache[cache_key] | |
126 return cache_result | |
127 def SlowRealToRelative(filepath, basepath): | |
128 path_parts = filepath.split('/') | |
129 base_parts = basepath.split('/') | |
130 while path_parts and base_parts and path_parts[0] == base_parts[0]: | |
131 path_parts = path_parts[1:] | |
132 base_parts = base_parts[1:] | |
133 rel_parts = ['..'] * len(base_parts) + path_parts | |
134 rel_path = '/'.join(rel_parts) | |
135 return rel_path | |
136 rel_path = SlowRealToRelative(filepath, basepath) | |
137 filename_to_relative_cache[cache_key] = rel_path | |
138 return rel_path | |
139 | |
140 def FilenameToRelative(self, filepath): | |
141 """Returns a relative path from CWD to filepath.""" | |
142 filepath = self.pathobj.abspath(filepath) | |
143 basepath = self.cwd | |
144 return self.RealToRelative(filepath, basepath) | |
145 | |
146 def FindFile(self, filename): | |
147 """Search for <filename> across the search directories, if the path is not | |
148 absolute. Return the filepath relative to the CWD or None. """ | |
149 cache_key = (tuple(self.search_dirs), self.cwd, filename) | |
150 if cache_key in findfile_cache: | |
151 cache_result = findfile_cache[cache_key] | |
152 return cache_result | |
153 result = None | |
154 def isfile(absname): | |
155 res = pathisfile_cache.get(absname) | |
156 if res is None: | |
157 res = self.pathobj.isfile(absname) | |
158 pathisfile_cache[absname] = res | |
159 return res | |
160 | |
161 if self.pathobj.isabs(filename): | |
162 if isfile(filename): | |
163 result = self.FilenameToRelative(filename) | |
164 else: | |
165 for pathname in self.search_dirs: | |
166 fullname = '%s/%s' % (pathname, filename) | |
167 if isfile(fullname): | |
168 result = self.FilenameToRelative(fullname) | |
169 break | |
170 findfile_cache[cache_key] = result | |
171 return result | |
172 | |
173 | |
174 def LoadFile(filename): | |
175 # Catch cases where the file does not exist | |
176 try: | |
177 fd = PathConverter().open(filename) | |
178 except IOError: | |
179 DebugPrint('Exception on file: %s' % filename) | |
180 return '' | |
181 # Go ahead and throw if you fail to read | |
182 return fd.read() | |
183 | |
184 | |
185 scan_cache = {} # cache (abs_filename -> include_list) | |
186 | |
187 | |
188 class Scanner(object): | |
189 """Scanner searches for '#include' to find dependencies.""" | |
190 | |
191 def __init__(self, loader=None): | |
192 regex = r'^\s*\#[ \t]*include[ \t]*[<"]([^>"]+)[>"]' | |
193 self.parser = re.compile(regex, re.M) | |
194 self.loader = loader | |
195 if not loader: | |
196 self.loader = LoadFile | |
197 | |
198 def ScanData(self, data): | |
199 """Generate a list of includes from this text block.""" | |
200 return self.parser.findall(data) | |
201 | |
202 def ScanFile(self, filename): | |
203 """Generate a list of includes from this filename.""" | |
204 abs_filename = os.path.abspath(filename) | |
205 if abs_filename in scan_cache: | |
206 return scan_cache[abs_filename] | |
207 includes = self.ScanData(self.loader(filename)) | |
208 scan_cache[abs_filename] = includes | |
209 DebugPrint('Source %s contains:\n\t%s' % (filename, '\n\t'.join(includes))) | |
210 return includes | |
211 | |
212 | |
213 class WorkQueue(object): | |
214 """WorkQueue contains the list of files to be scanned. | |
215 | |
216 WorkQueue contains provides a queue of files to be processed. The scanner | |
217 will attempt to push new items into the queue, which will be ignored if the | |
218 item is already in the queue. If the item is new, it will be added to the | |
219 work list, which is drained by the scanner. | |
220 """ | |
221 def __init__(self, resolver, scanner=Scanner()): | |
222 self.added_set = set() | |
223 self.todo_list = list() | |
224 self.scanner = scanner | |
225 self.resolver = resolver | |
226 | |
227 def PushIfNew(self, filename): | |
228 """Add this dependency to the list of not already there.""" | |
229 DebugPrint('Adding %s' % filename) | |
230 resolved_name = self.resolver.FindFile(filename) | |
231 if not resolved_name: | |
232 DebugPrint('Failed to resolve %s' % filename) | |
233 return | |
234 DebugPrint('Resolvd as %s' % resolved_name) | |
235 if resolved_name in self.added_set: | |
236 return | |
237 self.todo_list.append(resolved_name) | |
238 self.added_set.add(resolved_name) | |
239 | |
240 def PopIfAvail(self): | |
241 """Fetch the next dependency to search.""" | |
242 if not self.todo_list: | |
243 return None | |
244 return self.todo_list.pop() | |
245 | |
246 def Run(self): | |
247 """Search through the available dependencies until the list becomes empty. | |
248 The list must be primed with one or more source files to search.""" | |
249 scan_name = self.PopIfAvail() | |
250 while scan_name: | |
251 includes = self.scanner.ScanFile(scan_name) | |
252 # Add the directory of the current scanned file for resolving includes | |
253 # while processing includes for this file. | |
254 scan_dir = PathConverter().dirname(scan_name) | |
255 added_dir = not self.resolver.AddOneDirectory(scan_dir) | |
256 for include_file in includes: | |
257 self.PushIfNew(include_file) | |
258 if added_dir: | |
259 self.resolver.RemoveOneDirectory(scan_dir) | |
260 scan_name = self.PopIfAvail() | |
261 return self.added_set | |
262 | |
263 | |
264 def DoMain(argv): | |
265 """Entry point used by gyp's pymod_do_main feature.""" | |
266 global debug | |
267 | |
268 resolver = Resolver() | |
269 files = [] | |
270 | |
271 arg_type = '' | |
272 for arg in argv: | |
273 if arg in ['-I', '-S']: | |
274 arg_type = arg | |
275 elif arg == '-D': | |
276 debug = True | |
277 elif arg_type == '-I': | |
278 # Skip generated include directories. These files may not exist and | |
279 # there should be explicit dependency on the target that generates | |
280 # these files. | |
281 if arg.startswith('$!PRODUCT_DIR'): | |
282 continue | |
283 resolver.AddDirectories([arg]) | |
284 elif arg_type == '-S': | |
285 files.append(arg) | |
286 | |
287 workQ = WorkQueue(resolver) | |
288 for filename in files: | |
289 workQ.PushIfNew(filename) | |
290 | |
291 sources_set = workQ.Run() | |
292 | |
293 # If any of the original files requested aren't found, add them anyway. | |
294 # This is so that source files that will be generated are still returned in | |
295 # the program output. | |
296 sources_set = sources_set.union(set(files)) | |
297 | |
298 sorted_list = sorted(sources_set) | |
299 return '\n'.join(sorted_list) + '\n' | |
300 | |
301 | |
302 def Main(): | |
303 result = DoMain(sys.argv[1:]) | |
304 sys.stdout.write(result) | |
305 | |
306 | |
307 if __name__ == '__main__': | |
308 Main() | |
OLD | NEW |