| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # coding=utf-8 | 2 # coding=utf-8 |
| 3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 4 # Use of this source code is governed by a BSD-style license that can be | 4 # Use of this source code is governed by a BSD-style license that can be |
| 5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
| 6 | 6 |
| 7 """Runs strace or dtrace on a test and processes the logs to extract the | 7 """Runs strace or dtrace on a test and processes the logs to extract the |
| 8 dependencies from the source tree. | 8 dependencies from the source tree. |
| 9 | 9 |
| 10 Automatically extracts directories where all the files are used to make the | 10 Automatically extracts directories where all the files are used to make the |
| 11 dependencies list more compact. | 11 dependencies list more compact. |
| 12 """ | 12 """ |
| 13 | 13 |
| 14 import codecs | 14 import codecs |
| 15 import csv | 15 import csv |
| 16 import logging | 16 import logging |
| 17 import optparse | 17 import optparse |
| 18 import os | 18 import os |
| 19 import posixpath | 19 import posixpath |
| 20 import re | 20 import re |
| 21 import subprocess | 21 import subprocess |
| 22 import sys | 22 import sys |
| 23 | 23 |
| 24 ## OS-specific imports |
| 25 |
| 26 if sys.platform == 'win32': |
| 27 from ctypes.wintypes import create_unicode_buffer |
| 28 from ctypes.wintypes import windll, FormatError # pylint: disable=E0611 |
| 29 from ctypes.wintypes import GetLastError # pylint: disable=E0611 |
| 30 elif sys.platform == 'darwin': |
| 31 import Carbon.File # pylint: disable=F0401 |
| 32 |
| 24 | 33 |
| 25 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | 34 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 26 ROOT_DIR = os.path.dirname(os.path.dirname(BASE_DIR)) | 35 ROOT_DIR = os.path.dirname(os.path.dirname(BASE_DIR)) |
| 27 | 36 |
| 28 KEY_TRACKED = 'isolate_dependency_tracked' | 37 KEY_TRACKED = 'isolate_dependency_tracked' |
| 29 KEY_UNTRACKED = 'isolate_dependency_untracked' | 38 KEY_UNTRACKED = 'isolate_dependency_untracked' |
| 30 | 39 |
| 31 | 40 |
| 41 ## OS-specific functions |
| 42 |
| 32 if sys.platform == 'win32': | 43 if sys.platform == 'win32': |
| 33 from ctypes.wintypes import create_unicode_buffer | |
| 34 from ctypes.wintypes import windll, FormatError # pylint: disable=E0611 | |
| 35 from ctypes.wintypes import GetLastError # pylint: disable=E0611 | |
| 36 | |
| 37 | |
| 38 def QueryDosDevice(drive_letter): | 44 def QueryDosDevice(drive_letter): |
| 39 """Returns the Windows 'native' path for a DOS drive letter.""" | 45 """Returns the Windows 'native' path for a DOS drive letter.""" |
| 40 assert re.match(r'^[a-zA-Z]:$', drive_letter), drive_letter | 46 assert re.match(r'^[a-zA-Z]:$', drive_letter), drive_letter |
| 41 # Guesswork. QueryDosDeviceW never returns the required number of bytes. | 47 # Guesswork. QueryDosDeviceW never returns the required number of bytes. |
| 42 chars = 1024 | 48 chars = 1024 |
| 43 drive_letter = unicode(drive_letter) | 49 drive_letter = unicode(drive_letter) |
| 44 p = create_unicode_buffer(chars) | 50 p = create_unicode_buffer(chars) |
| 45 if 0 == windll.kernel32.QueryDosDeviceW(drive_letter, p, chars): | 51 if 0 == windll.kernel32.QueryDosDeviceW(drive_letter, p, chars): |
| 46 err = GetLastError() | 52 err = GetLastError() |
| 47 if err: | 53 if err: |
| 48 # pylint: disable=E0602 | 54 # pylint: disable=E0602 |
| 49 raise WindowsError( | 55 raise WindowsError( |
| 50 err, | 56 err, |
| 51 'QueryDosDevice(%s): %s (%d)' % ( | 57 'QueryDosDevice(%s): %s (%d)' % ( |
| 52 str(drive_letter), FormatError(err), err)) | 58 str(drive_letter), FormatError(err), err)) |
| 53 return p.value | 59 return p.value |
| 54 | 60 |
| 55 | 61 |
| 56 def GetShortPathName(long_path): | 62 def GetShortPathName(long_path): |
| 57 """Returns the Windows short path equivalent for a 'long' path.""" | 63 """Returns the Windows short path equivalent for a 'long' path.""" |
| 58 long_path = unicode(long_path) | 64 long_path = unicode(long_path) |
| 65 # Adds '\\\\?\\' when given an absolute path so the MAX_PATH (260) limit is |
| 66 # not enforced. |
| 67 if os.path.isabs(long_path) and not long_path.startswith('\\\\?\\'): |
| 68 long_path = '\\\\?\\' + long_path |
| 59 chars = windll.kernel32.GetShortPathNameW(long_path, None, 0) | 69 chars = windll.kernel32.GetShortPathNameW(long_path, None, 0) |
| 60 if chars: | 70 if chars: |
| 61 p = create_unicode_buffer(chars) | 71 p = create_unicode_buffer(chars) |
| 62 if windll.kernel32.GetShortPathNameW(long_path, p, chars): | 72 if windll.kernel32.GetShortPathNameW(long_path, p, chars): |
| 63 return p.value | 73 return p.value |
| 64 | 74 |
| 65 err = GetLastError() | 75 err = GetLastError() |
| 66 if err: | 76 if err: |
| 67 # pylint: disable=E0602 | 77 # pylint: disable=E0602 |
| 68 raise WindowsError( | 78 raise WindowsError( |
| 69 err, | 79 err, |
| 70 'GetShortPathName(%s): %s (%d)' % ( | 80 'GetShortPathName(%s): %s (%d)' % ( |
| 71 str(long_path), FormatError(err), err)) | 81 str(long_path), FormatError(err), err)) |
| 72 | 82 |
| 73 | 83 |
| 84 def GetLongPathName(short_path): |
| 85 """Returns the Windows long path equivalent for a 'short' path.""" |
| 86 short_path = unicode(short_path) |
| 87 # Adds '\\\\?\\' when given an absolute path so the MAX_PATH (260) limit is |
| 88 # not enforced. |
| 89 if os.path.isabs(short_path) and not short_path.startswith('\\\\?\\'): |
| 90 short_path = '\\\\?\\' + short_path |
| 91 chars = windll.kernel32.GetLongPathNameW(short_path, None, 0) |
| 92 if chars: |
| 93 p = create_unicode_buffer(chars) |
| 94 if windll.kernel32.GetLongPathNameW(short_path, p, chars): |
| 95 return p.value |
| 96 |
| 97 err = GetLastError() |
| 98 if err: |
| 99 # pylint: disable=E0602 |
| 100 raise WindowsError( |
| 101 err, |
| 102 'GetLongPathName(%s): %s (%d)' % ( |
| 103 str(short_path), FormatError(err), err)) |
| 104 |
| 105 |
| 74 def get_current_encoding(): | 106 def get_current_encoding(): |
| 75 """Returns the 'ANSI' code page associated to the process.""" | 107 """Returns the 'ANSI' code page associated to the process.""" |
| 76 return 'cp%d' % int(windll.kernel32.GetACP()) | 108 return 'cp%d' % int(windll.kernel32.GetACP()) |
| 77 | 109 |
| 78 | 110 |
| 79 class DosDriveMap(object): | 111 class DosDriveMap(object): |
| 80 """Maps \Device\HarddiskVolumeN to N: on Windows.""" | 112 """Maps \Device\HarddiskVolumeN to N: on Windows.""" |
| 81 # Keep one global cache. | 113 # Keep one global cache. |
| 82 _MAPPING = {} | 114 _MAPPING = {} |
| 83 | 115 |
| 84 def __init__(self): | 116 def __init__(self): |
| 85 if not self._MAPPING: | 117 if not self._MAPPING: |
| 118 # This is related to UNC resolver on windows. Ignore that. |
| 119 self._MAPPING['\\Device\\Mup'] = None |
| 120 |
| 86 for letter in (chr(l) for l in xrange(ord('C'), ord('Z')+1)): | 121 for letter in (chr(l) for l in xrange(ord('C'), ord('Z')+1)): |
| 87 try: | 122 try: |
| 88 letter = '%s:' % letter | 123 letter = '%s:' % letter |
| 89 mapped = QueryDosDevice(letter) | 124 mapped = QueryDosDevice(letter) |
| 90 # It can happen. Assert until we see it happens in the wild. In | 125 # It can happen. Assert until we see it happens in the wild. In |
| 91 # practice, prefer the lower drive letter. | 126 # practice, prefer the lower drive letter. |
| 92 assert mapped not in self._MAPPING | 127 assert mapped not in self._MAPPING |
| 93 if mapped not in self._MAPPING: | 128 if mapped not in self._MAPPING: |
| 94 self._MAPPING[mapped] = letter | 129 self._MAPPING[mapped] = letter |
| 95 except WindowsError: # pylint: disable=E0602 | 130 except WindowsError: # pylint: disable=E0602 |
| 96 pass | 131 pass |
| 97 | 132 |
| 98 def to_dos(self, path): | 133 def to_dos(self, path): |
| 99 """Converts a native NT path to DOS path.""" | 134 """Converts a native NT path to DOS path.""" |
| 100 m = re.match(r'(^\\Device\\[a-zA-Z0-9]+)(\\.*)?$', path) | 135 m = re.match(r'(^\\Device\\[a-zA-Z0-9]+)(\\.*)?$', path) |
| 101 if not m or m.group(1) not in self._MAPPING: | 136 assert m, path |
| 102 assert False, path | 137 assert m.group(1) in self._MAPPING, (path, self._MAPPING) |
| 103 drive = self._MAPPING[m.group(1)] | 138 drive = self._MAPPING[m.group(1)] |
| 104 if not m.group(2): | 139 if not drive or not m.group(2): |
| 105 return drive | 140 return drive |
| 106 return drive + m.group(2) | 141 return drive + m.group(2) |
| 107 | 142 |
| 108 | 143 |
| 144 def get_native_path_case(root, relative_path): |
| 145 """Returns the native path case.""" |
| 146 if sys.platform == 'win32': |
| 147 # Windows used to have an option to turn on case sensitivity on non Win32 |
| 148 # subsystem but that's out of scope here and isn't supported anymore. |
| 149 # First process root. |
| 150 if root: |
| 151 root = GetLongPathName(GetShortPathName(root)) + os.path.sep |
| 152 path = os.path.join(root, relative_path) if root else relative_path |
| 153 # Go figure why GetShortPathName() is needed. |
| 154 return GetLongPathName(GetShortPathName(path))[len(root):] |
| 155 elif sys.platform == 'darwin': |
| 156 # Technically, it's only HFS+ on OSX that is case insensitive. It's |
| 157 # the default setting on HFS+ but can be changed. |
| 158 root_ref, _ = Carbon.File.FSPathMakeRef(root) |
| 159 rel_ref, _ = Carbon.File.FSPathMakeRef(os.path.join(root, relative_path)) |
| 160 return rel_ref.FSRefMakePath()[len(root_ref.FSRefMakePath())+1:] |
| 161 else: |
| 162 # Give up on cygwin, as GetLongPathName() can't be called. |
| 163 return relative_path |
| 164 |
| 165 |
| 109 def get_flavor(): | 166 def get_flavor(): |
| 110 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py.""" | 167 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py.""" |
| 111 flavors = { | 168 flavors = { |
| 112 'cygwin': 'win', | 169 'cygwin': 'win', |
| 113 'win32': 'win', | 170 'win32': 'win', |
| 114 'darwin': 'mac', | 171 'darwin': 'mac', |
| 115 'sunos5': 'solaris', | 172 'sunos5': 'solaris', |
| 116 'freebsd7': 'freebsd', | 173 'freebsd7': 'freebsd', |
| 117 'freebsd8': 'freebsd', | 174 'freebsd8': 'freebsd', |
| 118 } | 175 } |
| (...skipping 711 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 830 if handler: | 887 if handler: |
| 831 handler(line) | 888 handler(line) |
| 832 else: | 889 else: |
| 833 assert False, '%s_%s' % (line[self.EVENT_NAME], line[self.TYPE]) | 890 assert False, '%s_%s' % (line[self.EVENT_NAME], line[self.TYPE]) |
| 834 | 891 |
| 835 def handle_EventTrace_Any(self, line): | 892 def handle_EventTrace_Any(self, line): |
| 836 pass | 893 pass |
| 837 | 894 |
| 838 def handle_FileIo_Create(self, line): | 895 def handle_FileIo_Create(self, line): |
| 839 m = re.match(r'^\"(.+)\"$', line[self.FILE_PATH]) | 896 m = re.match(r'^\"(.+)\"$', line[self.FILE_PATH]) |
| 840 self._handle_file(self._drive_map.to_dos(m.group(1)).lower()) | 897 self._handle_file(self._drive_map.to_dos(m.group(1))) |
| 841 | 898 |
| 842 def handle_FileIo_Rename(self, line): | 899 def handle_FileIo_Rename(self, line): |
| 843 # TODO(maruel): Handle? | 900 # TODO(maruel): Handle? |
| 844 pass | 901 pass |
| 845 | 902 |
| 846 def handle_FileIo_Any(self, line): | 903 def handle_FileIo_Any(self, line): |
| 847 pass | 904 pass |
| 848 | 905 |
| 849 def handle_Image_DCStart(self, line): | 906 def handle_Image_DCStart(self, line): |
| 850 # TODO(maruel): Handle? | 907 # TODO(maruel): Handle? |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 906 filename in self.non_existent): | 963 filename in self.non_existent): |
| 907 return | 964 return |
| 908 logging.debug('_handle_file(%s)' % filename) | 965 logging.debug('_handle_file(%s)' % filename) |
| 909 if os.path.isfile(filename): | 966 if os.path.isfile(filename): |
| 910 self.files.add(filename) | 967 self.files.add(filename) |
| 911 else: | 968 else: |
| 912 self.non_existent.add(filename) | 969 self.non_existent.add(filename) |
| 913 | 970 |
| 914 def __init__(self): | 971 def __init__(self): |
| 915 # Most ignores need to be determined at runtime. | 972 # Most ignores need to be determined at runtime. |
| 916 self.IGNORED = set([os.path.dirname(sys.executable).lower()]) | 973 self.IGNORED = set([os.path.dirname(sys.executable)]) |
| 917 # Add many directories from environment variables. | 974 # Add many directories from environment variables. |
| 918 vars_to_ignore = ( | 975 vars_to_ignore = ( |
| 919 'APPDATA', | 976 'APPDATA', |
| 920 'LOCALAPPDATA', | 977 'LOCALAPPDATA', |
| 921 'ProgramData', | 978 'ProgramData', |
| 922 'ProgramFiles', | 979 'ProgramFiles', |
| 923 'ProgramFiles(x86)', | 980 'ProgramFiles(x86)', |
| 924 'ProgramW6432', | 981 'ProgramW6432', |
| 925 'SystemRoot', | 982 'SystemRoot', |
| 926 'TEMP', | 983 'TEMP', |
| 927 'TMP', | 984 'TMP', |
| 928 ) | 985 ) |
| 929 for i in vars_to_ignore: | 986 for i in vars_to_ignore: |
| 930 if os.environ.get(i): | 987 if os.environ.get(i): |
| 931 self.IGNORED.add(os.environ[i].lower()) | 988 self.IGNORED.add(os.environ[i]) |
| 932 | 989 |
| 933 # Also add their short path name equivalents. | 990 # Also add their short path name equivalents. |
| 934 for i in list(self.IGNORED): | 991 for i in list(self.IGNORED): |
| 935 self.IGNORED.add(GetShortPathName(i).lower()) | 992 self.IGNORED.add(GetShortPathName(i)) |
| 936 | 993 |
| 937 # Add this one last since it has no short path name equivalent. | 994 # Add this one last since it has no short path name equivalent. |
| 938 self.IGNORED.add('\\systemroot') | 995 self.IGNORED.add('\\systemroot') |
| 939 self.IGNORED = tuple(sorted(self.IGNORED)) | 996 self.IGNORED = tuple(sorted(self.IGNORED)) |
| 940 | 997 |
| 941 @classmethod | 998 @classmethod |
| 942 def gen_trace(cls, cmd, cwd, logname): | 999 def gen_trace(cls, cmd, cwd, logname): |
| 943 logging.info('gen_trace(%s, %s, %s)' % (cmd, cwd, logname)) | 1000 logging.info('gen_trace(%s, %s, %s)' % (cmd, cwd, logname)) |
| 944 # Use "logman -?" for help. | 1001 # Use "logman -?" for help. |
| 945 | 1002 |
| (...skipping 272 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1218 assert not product_dir or not os.path.isabs(product_dir), product_dir | 1275 assert not product_dir or not os.path.isabs(product_dir), product_dir |
| 1219 | 1276 |
| 1220 cmd = fix_python_path(cmd) | 1277 cmd = fix_python_path(cmd) |
| 1221 assert ( | 1278 assert ( |
| 1222 (os.path.isfile(logfile) and not force_trace) or os.path.isabs(cmd[0]) | 1279 (os.path.isfile(logfile) and not force_trace) or os.path.isabs(cmd[0]) |
| 1223 ), cmd[0] | 1280 ), cmd[0] |
| 1224 | 1281 |
| 1225 # Resolve any symlink | 1282 # Resolve any symlink |
| 1226 root_dir = os.path.realpath(root_dir) | 1283 root_dir = os.path.realpath(root_dir) |
| 1227 | 1284 |
| 1228 if sys.platform == 'win32': | |
| 1229 # Help ourself and lowercase all the paths. | |
| 1230 # TODO(maruel): handle short path names by converting them to long path name | |
| 1231 # as needed. | |
| 1232 root_dir = root_dir.lower() | |
| 1233 if cwd_dir: | |
| 1234 cwd_dir = cwd_dir.lower() | |
| 1235 if product_dir: | |
| 1236 product_dir = product_dir.lower() | |
| 1237 | |
| 1238 def print_if(txt): | 1285 def print_if(txt): |
| 1239 if cwd_dir is None: | 1286 if cwd_dir is None: |
| 1240 print(txt) | 1287 print(txt) |
| 1241 | 1288 |
| 1242 flavor = get_flavor() | 1289 flavor = get_flavor() |
| 1243 if flavor == 'linux': | 1290 if flavor == 'linux': |
| 1244 api = Strace() | 1291 api = Strace() |
| 1245 elif flavor == 'mac': | 1292 elif flavor == 'mac': |
| 1246 api = Dtrace() | 1293 api = Dtrace() |
| 1247 elif sys.platform == 'win32': | 1294 elif sys.platform == 'win32': |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1280 for f in non_existent: | 1327 for f in non_existent: |
| 1281 print_if(' %s' % f) | 1328 print_if(' %s' % f) |
| 1282 | 1329 |
| 1283 expected, unexpected = relevant_files( | 1330 expected, unexpected = relevant_files( |
| 1284 files, root_dir.rstrip(os.path.sep) + os.path.sep) | 1331 files, root_dir.rstrip(os.path.sep) + os.path.sep) |
| 1285 if unexpected: | 1332 if unexpected: |
| 1286 print_if('Unexpected: %d' % len(unexpected)) | 1333 print_if('Unexpected: %d' % len(unexpected)) |
| 1287 for f in unexpected: | 1334 for f in unexpected: |
| 1288 print_if(' %s' % f) | 1335 print_if(' %s' % f) |
| 1289 | 1336 |
| 1337 # In case the file system is case insensitive. |
| 1338 expected = sorted(set(get_native_path_case(root_dir, f) for f in expected)) |
| 1339 |
| 1290 simplified = extract_directories(expected, root_dir) | 1340 simplified = extract_directories(expected, root_dir) |
| 1291 print_if('Interesting: %d reduced to %d' % (len(expected), len(simplified))) | 1341 print_if('Interesting: %d reduced to %d' % (len(expected), len(simplified))) |
| 1292 for f in simplified: | 1342 for f in simplified: |
| 1293 print_if(' %s' % f) | 1343 print_if(' %s' % f) |
| 1294 | 1344 |
| 1295 if cwd_dir is not None: | 1345 if cwd_dir is not None: |
| 1296 def cleanuppath(x): | 1346 def cleanuppath(x): |
| 1297 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows. | 1347 """Cleans up a relative path. Converts any os.path.sep to '/' on Windows. |
| 1298 """ | 1348 """ |
| 1299 if x: | 1349 if x: |
| 1300 x = x.rstrip(os.path.sep).replace(os.path.sep, '/') | 1350 x = x.rstrip(os.path.sep).replace(os.path.sep, '/') |
| 1301 if x == '.': | 1351 if x == '.': |
| 1302 x = '' | 1352 x = '' |
| 1303 if x: | 1353 if x: |
| 1304 x += '/' | 1354 x += '/' |
| 1305 return x | 1355 return x |
| 1306 | 1356 |
| 1307 # Both are relative directories to root_dir. | 1357 # Both are relative directories to root_dir. |
| 1308 cwd_dir = cleanuppath(cwd_dir) | 1358 cwd_dir = cleanuppath(cwd_dir) |
| 1309 product_dir = cleanuppath(product_dir) | 1359 product_dir = cleanuppath(product_dir) |
| 1310 | 1360 |
| 1311 def fix(f): | 1361 def fix(f): |
| 1312 """Bases the file on the most restrictive variable.""" | 1362 """Bases the file on the most restrictive variable.""" |
| 1313 logging.debug('fix(%s)' % f) | 1363 logging.debug('fix(%s)' % f) |
| 1314 # Important, GYP stores the files with / and not \. | 1364 # Important, GYP stores the files with / and not \. |
| 1315 if sys.platform == 'win32': | 1365 f = f.replace(os.path.sep, '/') |
| 1316 f = f.replace('\\', '/') | |
| 1317 | 1366 |
| 1318 if product_dir and f.startswith(product_dir): | 1367 if product_dir and f.startswith(product_dir): |
| 1319 return '<(PRODUCT_DIR)/%s' % f[len(product_dir):] | 1368 return '<(PRODUCT_DIR)/%s' % f[len(product_dir):] |
| 1320 else: | 1369 else: |
| 1321 # cwd_dir is usually the directory containing the gyp file. It may be | 1370 # cwd_dir is usually the directory containing the gyp file. It may be |
| 1322 # empty if the whole directory containing the gyp file is needed. | 1371 # empty if the whole directory containing the gyp file is needed. |
| 1323 return posix_relpath(f, cwd_dir) or './' | 1372 return posix_relpath(f, cwd_dir) or './' |
| 1324 | 1373 |
| 1325 corrected = [fix(f) for f in simplified] | 1374 corrected = [fix(f) for f in simplified] |
| 1326 tracked = [f for f in corrected if not f.endswith('/') and ' ' not in f] | 1375 tracked = [f for f in corrected if not f.endswith('/') and ' ' not in f] |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1387 os.path.abspath(options.log), | 1436 os.path.abspath(options.log), |
| 1388 args, | 1437 args, |
| 1389 options.root_dir, | 1438 options.root_dir, |
| 1390 options.cwd, | 1439 options.cwd, |
| 1391 options.product_dir, | 1440 options.product_dir, |
| 1392 options.force) | 1441 options.force) |
| 1393 | 1442 |
| 1394 | 1443 |
| 1395 if __name__ == '__main__': | 1444 if __name__ == '__main__': |
| 1396 sys.exit(main()) | 1445 sys.exit(main()) |
| OLD | NEW |