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.
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.
25 class PathConverter(object):
26 """PathConverter does path manipulates using Posix style pathnames.
28 Regardless of the native path type, all inputs and outputs to the path
29 functions are with POSIX style separators.
31 def ToNativePath(self, pathname):
32 return os.path.sep.join(pathname.split('/'))
34 def ToPosixPath(self, pathname):
35 return '/'.join(pathname.split(os.path.sep))
37 def isfile(self, pathname):
38 ospath = self.ToNativePath(pathname)
39 return os.path.isfile(ospath)
42 return self.ToPosixPath(os.getcwd())
44 def isabs(self, pathname):
45 ospath = self.ToNativePath(pathname)
46 return os.path.isabs(ospath)
48 def isdir(self, pathname):
49 ospath = self.ToNativePath(pathname)
50 return os.path.isdir(ospath)
52 def open(self, pathname):
53 ospath = self.ToNativePath(pathname)
56 def abspath(self, pathname):
57 ospath = self.ToNativePath(pathname)
58 ospath = os.path.abspath(ospath)
59 return self.ToPosixPath(ospath)
61 def dirname(self, pathname):
62 ospath = self.ToNativePath(pathname)
63 ospath = os.path.dirname(ospath)
64 return self.ToPosixPath(ospath)
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
73 class Resolver(object):
74 """Resolver finds and generates relative paths for include files.
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.
80 def __init__(self, pathobj=PathConverter()):
82 self.pathobj = pathobj
83 self.cwd = self.pathobj.getcwd()
84 self.offs = len(self.cwd)
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)
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)
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)
108 def AddDirectories(self, pathlist):
109 """Add list of space separated directories."""
111 dirlist = ' '.join(pathlist)
112 for dirname in dirlist.split(' '):
113 if not self.AddOneDirectory(dirname):
117 def GetDirectories(self):
118 return self.search_dirs
120 def RealToRelative(self, filepath, basepath):
121 """Returns a relative path from an absolute basepath and filepath."""
122 cache_key = (filepath, basepath)
124 if cache_key in filename_to_relative_cache:
125 cache_result = filename_to_relative_cache[cache_key]
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)
136 rel_path = SlowRealToRelative(filepath, basepath)
137 filename_to_relative_cache[cache_key] = rel_path
140 def FilenameToRelative(self, filepath):
141 """Returns a relative path from CWD to filepath."""
142 filepath = self.pathobj.abspath(filepath)
144 return self.RealToRelative(filepath, basepath)
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]
155 res = pathisfile_cache.get(absname)
157 res = self.pathobj.isfile(absname)
158 pathisfile_cache[absname] = res
161 if self.pathobj.isabs(filename):
163 result = self.FilenameToRelative(filename)
165 for pathname in self.search_dirs:
166 fullname = '%s/%s' % (pathname, filename)
168 result = self.FilenameToRelative(fullname)
170 findfile_cache[cache_key] = result
174 def LoadFile(filename):
175 # Catch cases where the file does not exist
177 fd = PathConverter().open(filename)
179 DebugPrint('Exception on file: %s' % filename)
181 # Go ahead and throw if you fail to read
185 scan_cache = {} # cache (abs_filename -> include_list)
188 class Scanner(object):
189 """Scanner searches for '#include' to find dependencies."""
191 def __init__(self, loader=None):
192 regex = r'^\s*\#[ \t]*include[ \t]*[<"]([^>"]+)[>"]'
193 self.parser = re.compile(regex, re.M)
196 self.loader = LoadFile
198 def ScanData(self, data):
199 """Generate a list of includes from this text block."""
200 return self.parser.findall(data)
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)))
213 class WorkQueue(object):
214 """WorkQueue contains the list of files to be scanned.
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.
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
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)
234 DebugPrint('Resolvd as %s' % resolved_name)
235 if resolved_name in self.added_set:
237 self.todo_list.append(resolved_name)
238 self.added_set.add(resolved_name)
240 def PopIfAvail(self):
241 """Fetch the next dependency to search."""
242 if not self.todo_list:
244 return self.todo_list.pop()
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()
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)
259 self.resolver.RemoveOneDirectory(scan_dir)
260 scan_name = self.PopIfAvail()
261 return sorted(self.added_set)
265 """Entry point used by gyp's pymod_do_main feature."""
268 resolver = Resolver()
273 if arg in ['-I', '-S']:
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
281 if arg.startswith('$!PRODUCT_DIR'):
283 resolver.AddDirectories([arg])
284 elif arg_type == '-S':
287 workQ = WorkQueue(resolver)
288 for filename in files:
289 workQ.PushIfNew(filename)
291 sorted_list = workQ.Run()
292 return '\n'.join(sorted_list) + '\n'
296 result = DoMain(sys.argv[1:])
297 sys.stdout.write(result)
300 if __name__ == '__main__':