| 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 |