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 |