Web Inspector: Improve the Uncaught Exception View file a bug link
[WebKit-https.git] / Tools / Scripts / webkitpy / common / system / filesystem.py
1 # Copyright (C) 2010 Google Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
6 #
7 #     * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer.
9 #     * Redistributions in binary form must reproduce the above
10 # copyright notice, this list of conditions and the following disclaimer
11 # in the documentation and/or other materials provided with the
12 # distribution.
13 #     * Neither the name of Google Inc. nor the names of its
14 # contributors may be used to endorse or promote products derived from
15 # this software without specific prior written permission.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 """Wrapper object for the file system / source tree."""
30
31 import codecs
32 import errno
33 import exceptions
34 import filecmp
35 import glob
36 import hashlib
37 import os
38 import shutil
39 import sys
40 import tempfile
41 import time
42
43
44 class FileSystem(object):
45     """FileSystem interface for webkitpy.
46
47     Unless otherwise noted, all paths are allowed to be either absolute
48     or relative."""
49     sep = os.sep
50     pardir = os.pardir
51
52     def abspath(self, path):
53         # FIXME: This gross hack is needed while we transition from Cygwin to native Windows, because we
54         # have some mixing of file conventions from different tools:
55         if sys.platform == 'cygwin':
56             path = os.path.normpath(path)
57             path_components = path.split(os.sep)
58             if path_components and len(path_components[0]) == 2 and path_components[0][1] == ':':
59                 path_components[0] = path_components[0][0]
60                 path = os.path.join('/', 'cygdrive', *path_components)
61
62         return os.path.abspath(path)
63
64     def realpath(self, path):
65         return os.path.realpath(path)
66
67     def path_to_module(self, module_name):
68         """A wrapper for all calls to __file__ to allow easy unit testing."""
69         # FIXME: This is the only use of sys in this file. It's possible this function should move elsewhere.
70         return sys.modules[module_name].__file__  # __file__ is always an absolute path.
71
72     def expanduser(self, path):
73         return os.path.expanduser(path)
74
75     def basename(self, path):
76         return os.path.basename(path)
77
78     def chdir(self, path):
79         return os.chdir(path)
80
81     def copyfile(self, source, destination):
82         shutil.copyfile(source, destination)
83
84     def dirname(self, path):
85         return os.path.dirname(path)
86
87     def exists(self, path):
88         return os.path.exists(path)
89
90     def dirs_under(self, path, dir_filter=None):
91         """Return the list of all directories under the given path in topdown order.
92
93         Args:
94             dir_filter: if not None, the filter will be invoked
95                 with the filesystem object and the path of each dirfound.
96                 The dir is included in the result if the callback returns True.
97         """
98         def filter_all(fs, dirpath):
99             return True
100         dir_filter = dir_filter or filter_all
101
102         dirs = []
103         for (dirpath, dirnames, filenames) in os.walk(path):
104             if dir_filter(self, dirpath):
105                 dirs.append(dirpath)
106         return dirs
107
108     def files_under(self, path, dirs_to_skip=[], file_filter=None):
109         """Return the list of all files under the given path in topdown order.
110
111         Args:
112             dirs_to_skip: a list of directories to skip over during the
113                 traversal (e.g., .svn, resources, etc.)
114             file_filter: if not None, the filter will be invoked
115                 with the filesystem object and the dirname and basename of
116                 each file found. The file is included in the result if the
117                 callback returns True.
118         """
119         def filter_all(fs, dirpath, basename):
120             return True
121
122         file_filter = file_filter or filter_all
123         files = []
124         if self.isfile(path):
125             if file_filter(self, self.dirname(path), self.basename(path)):
126                 files.append(path)
127             return files
128
129         if self.basename(path) in dirs_to_skip:
130             return []
131
132         for (dirpath, dirnames, filenames) in os.walk(path):
133             for d in dirs_to_skip:
134                 if d in dirnames:
135                     dirnames.remove(d)
136
137             for filename in filenames:
138                 if file_filter(self, dirpath, filename):
139                     files.append(self.join(dirpath, filename))
140         return files
141
142     def getcwd(self):
143         return os.getcwd()
144
145     def glob(self, path):
146         return glob.glob(path)
147
148     def isabs(self, path):
149         return os.path.isabs(path)
150
151     def isfile(self, path):
152         return os.path.isfile(path)
153
154     def getsize(self, path):
155         return os.path.getsize(path)
156
157     def isdir(self, path):
158         return os.path.isdir(path)
159
160     def join(self, *comps):
161         return os.path.join(*comps)
162
163     def listdir(self, path):
164         return os.listdir(path)
165
166     def mkdtemp(self, **kwargs):
167         """Create and return a uniquely named directory.
168
169         This is like tempfile.mkdtemp, but if used in a with statement
170         the directory will self-delete at the end of the block (if the
171         directory is empty; non-empty directories raise errors). The
172         directory can be safely deleted inside the block as well, if so
173         desired.
174
175         Note that the object returned is not a string and does not support all of the string
176         methods. If you need a string, coerce the object to a string and go from there.
177         """
178         class TemporaryDirectory(object):
179             def __init__(self, filesystem, **kwargs):
180                 self._filesystem = filesystem
181                 self._kwargs = kwargs
182                 self._directory_path = tempfile.mkdtemp(**self._kwargs)
183
184             def __str__(self):
185                 return self._directory_path
186
187             def __enter__(self):
188                 return self._directory_path
189
190             def __exit__(self, type, value, traceback):
191                 if self._filesystem.exists(self._directory_path):
192                     self._filesystem.rmtree(self._directory_path)
193
194         return TemporaryDirectory(self, **kwargs)
195
196     def maybe_make_directory(self, *path):
197         """Create the specified directory if it doesn't already exist."""
198         try:
199             os.makedirs(self.join(*path))
200         except OSError as e:
201             if e.errno != errno.EEXIST:
202                 raise
203
204     def move(self, source, destination):
205         shutil.move(source, destination)
206
207     def mtime(self, path):
208         return os.stat(path).st_mtime
209
210     def normpath(self, path):
211         return os.path.normpath(path)
212
213     def open_binary_tempfile(self, suffix):
214         """Create, open, and return a binary temp file. Returns a tuple of the file and the name."""
215         temp_fd, temp_name = tempfile.mkstemp(suffix)
216         f = os.fdopen(temp_fd, 'wb')
217         return f, temp_name
218
219     def open_binary_file_for_reading(self, path):
220         return codecs.open(path, 'rb')
221
222     def read_binary_file(self, path):
223         """Return the contents of the file at the given path as a byte string."""
224         with file(path, 'rb') as f:
225             return f.read()
226
227     def write_binary_file(self, path, contents):
228         with file(path, 'wb') as f:
229             f.write(contents)
230
231     def open_text_file_for_reading(self, path, errors='strict'):
232         # Note: There appears to be an issue with the returned file objects
233         # not being seekable. See http://stackoverflow.com/questions/1510188/can-seek-and-tell-work-with-utf-8-encoded-documents-in-python .
234         return codecs.open(path, 'r', 'utf8', errors)
235
236     def open_text_file_for_writing(self, path, should_append=False):
237         if should_append:
238             return codecs.open(path, 'a', 'utf8')
239         return codecs.open(path, 'w', 'utf8')
240
241     def open_stdin(self):
242         return codecs.StreamReaderWriter(sys.stdin,
243                                              codecs.getreader('utf8'),
244                                              codecs.getwriter('utf8'),
245                                              'replace')
246
247     def read_text_file(self, path, errors='strict'):
248         """Return the contents of the file at the given path as a Unicode string.
249
250         The file is read assuming it is a UTF-8 encoded file with no BOM."""
251         with codecs.open(path, 'r', 'utf8', errors=errors) as f:
252             return f.read()
253
254     def write_text_file(self, path, contents, errors='strict'):
255         """Write the contents to the file at the given location.
256
257         The file is written encoded as UTF-8 with no BOM."""
258         with codecs.open(path, 'w', 'utf-8', errors=errors) as f:
259             f.write(contents.decode('utf-8', errors=errors) if type(contents) == str else contents)
260
261     def sha1(self, path):
262         contents = self.read_binary_file(path)
263         return hashlib.sha1(contents).hexdigest()
264
265     def relpath(self, path, start='.'):
266         return os.path.relpath(path, start)
267
268     class _WindowsError(exceptions.OSError):
269         """Fake exception for Linux and Mac."""
270         pass
271
272     def remove(self, path, osremove=os.remove):
273         """On Windows, if a process was recently killed and it held on to a
274         file, the OS will hold on to the file for a short while.  This makes
275         attempts to delete the file fail.  To work around that, this method
276         will retry for a few seconds until Windows is done with the file."""
277         try:
278             exceptions.WindowsError
279         except AttributeError:
280             exceptions.WindowsError = FileSystem._WindowsError
281
282         retry_timeout_sec = 3.0
283         sleep_interval = 0.1
284         while True:
285             try:
286                 osremove(path)
287                 return True
288             except exceptions.WindowsError as e:
289                 time.sleep(sleep_interval)
290                 retry_timeout_sec -= sleep_interval
291                 if retry_timeout_sec < 0:
292                     raise e
293
294     def rmtree(self, path):
295         """Delete the directory rooted at path, whether empty or not."""
296         shutil.rmtree(path, ignore_errors=True)
297
298     def copytree(self, source, destination):
299         shutil.copytree(source, destination)
300
301     def split(self, path):
302         """Return (dirname, basename + '.' + ext)"""
303         return os.path.split(path)
304
305     def splitext(self, path):
306         """Return (dirname + os.sep + basename, '.' + ext)"""
307         return os.path.splitext(path)
308
309     def compare(self, path1, path2):
310         return filecmp.cmp(path1, path2)
311
312     def map_base_host_path(self, path):
313         """Returns a path from the base host localized for this host. By default,
314         this host assumes it is the base host and maps the path to itself"""
315         return path
316
317     def move_to_base_host(self, source, destination):
318         """Moves a file from this host to the base host. By default, this host
319         assumes it is the base host and will just execute a move."""
320         self.move(source, destination)
321
322     def move_from_base_host(self, source, destination):
323         """Moves a file from the base host to this host. By default, this host
324         assumes it is the base host and will just execute a move."""
325         self.move(source, destination)
326
327     def copy_to_base_host(self, source, destination):
328         """Copy a file from this host to the base host. By default, this host
329         assumes it is the base host and will just execute a copytree/copyfile."""
330         if self.isdir(source):
331             self.copytree(source, destination)
332         else:
333             self.copyfile(source, destination)
334
335     def copy_from_base_host(self, source, destination):
336         """Copy a file from the base host to this host. By default, this host
337         assumes it is the base host and will just execute a copytree/copyfile."""
338         if self.isdir(source):
339             self.copytree(source, destination)
340         else:
341             self.copyfile(source, destination)