blob: 52bc6cddaa873c9f44c4cee51d431dabdcd4c096 [file] [log] [blame]
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org06617272010-11-04 13:50:50 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""Generic utils."""
6
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00007import codecs
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +00008import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00009import os
maruel@chromium.org3742c842010-09-09 19:27:14 +000010import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000011import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000012import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000013import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000015import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000016import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000017import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000018import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000019
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000020import subprocess2
21
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022
maruel@chromium.org66c83e62010-09-07 14:18:45 +000023class Error(Exception):
24 """gclient exception class."""
25 pass
26
27
msb@chromium.orgac915bb2009-11-13 17:03:01 +000028def SplitUrlRevision(url):
29 """Splits url and returns a two-tuple: url, rev"""
30 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000031 # Make sure ssh://user-name@example.com/~/test.git@stable works
32 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000033 components = re.search(regex, url).groups()
34 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000035 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000036 if len(components) == 1:
37 components += [None]
38 return tuple(components)
39
40
floitsch@google.comeaab7842011-04-28 09:07:58 +000041def IsDateRevision(revision):
42 """Returns true if the given revision is of the form "{ ... }"."""
43 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
44
45
46def MakeDateRevision(date):
47 """Returns a revision representing the latest revision before the given
48 date."""
49 return "{" + date + "}"
50
51
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000052def SyntaxErrorToError(filename, e):
53 """Raises a gclient_utils.Error exception with the human readable message"""
54 try:
55 # Try to construct a human readable error message
56 if filename:
57 error_message = 'There is a syntax error in %s\n' % filename
58 else:
59 error_message = 'There is a syntax error\n'
60 error_message += 'Line #%s, character %s: "%s"' % (
61 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
62 except:
63 # Something went wrong, re-raise the original exception
64 raise e
65 else:
66 raise Error(error_message)
67
68
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000069class PrintableObject(object):
70 def __str__(self):
71 output = ''
72 for i in dir(self):
73 if i.startswith('__'):
74 continue
75 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
76 return output
77
78
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000079def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000080 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +000081 # codecs.open() has different behavior than open() on python 2.6 so use
82 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +000083 s = f.read()
84 try:
85 return s.decode('utf-8')
86 except UnicodeDecodeError:
87 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000088
89
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000090def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000091 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000092 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000093
94
maruel@chromium.orgf9040722011-03-09 14:47:51 +000095def rmtree(path):
96 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000097
maruel@chromium.orgf9040722011-03-09 14:47:51 +000098 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000099
100 shutil.rmtree() doesn't work on Windows if any of the files or directories
101 are read-only, which svn repositories and some .svn files are. We need to
102 be able to force the files to be writable (i.e., deletable) as we traverse
103 the tree.
104
105 Even with all this, Windows still sometimes fails to delete a file, citing
106 a permission error (maybe something to do with antivirus scans or disk
107 indexing). The best suggestion any of the user forums had was to wait a
108 bit and try again, so we do that too. It's hand-waving, but sometimes it
109 works. :/
110
111 On POSIX systems, things are a little bit simpler. The modes of the files
112 to be deleted doesn't matter, only the modes of the directories containing
113 them are significant. As the directory tree is traversed, each directory
114 has its mode set appropriately before descending into it. This should
115 result in the entire tree being removed, with the possible exception of
116 *path itself, because nothing attempts to change the mode of its parent.
117 Doing so would be hazardous, as it's not a directory slated for removal.
118 In the ordinary case, this is not a problem: for our purposes, the user
119 will never lack write permission on *path's parent.
120 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000121 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122 return
123
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000124 if os.path.islink(path) or not os.path.isdir(path):
125 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000126
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000127 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000128 # Give up and use cmd.exe's rd command.
129 path = os.path.normcase(path)
130 for _ in xrange(3):
131 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
132 if exitcode == 0:
133 return
134 else:
135 print >> sys.stderr, 'rd exited with code %d' % exitcode
136 time.sleep(3)
137 raise Exception('Failed to remove path %s' % path)
138
139 # On POSIX systems, we need the x-bit set on the directory to access it,
140 # the r-bit to see its contents, and the w-bit to remove files from it.
141 # The actual modes of the files within the directory is irrelevant.
142 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000143
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000144 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000145 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000146
147 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000148 # If fullpath is a symbolic link that points to a directory, isdir will
149 # be True, but we don't want to descend into that as a directory, we just
150 # want to remove the link. Check islink and treat links as ordinary files
151 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000152 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000153 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000154 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000155 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000156 # Recurse.
157 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000158
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000159 remove(os.rmdir, path)
160
161# TODO(maruel): Rename the references.
162RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000163
164
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000165def safe_makedirs(tree):
166 """Creates the directory in a safe manner.
167
168 Because multiple threads can create these directories concurently, trap the
169 exception and pass on.
170 """
171 count = 0
172 while not os.path.exists(tree):
173 count += 1
174 try:
175 os.makedirs(tree)
176 except OSError, e:
177 # 17 POSIX, 183 Windows
178 if e.errno not in (17, 183):
179 raise
180 if count > 40:
181 # Give up.
182 raise
183
184
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000185def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000186 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187
maruel@chromium.org17d01792010-09-01 18:07:10 +0000188 If |always| is True, a message indicating what is being done
189 is printed to stdout all the time even if not output is generated. Otherwise
190 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000191 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000192 stdout = kwargs.setdefault('stdout', sys.stdout)
193 if header is None:
194 header = "\n________ running '%s' in '%s'\n" % (
195 ' '.join(args), kwargs.get('cwd', '.'))
196
maruel@chromium.org17d01792010-09-01 18:07:10 +0000197 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000198 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000199 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000200 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000201 def filter_msg(line):
202 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000203 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000204 elif filter_fn:
205 filter_fn(line)
206 kwargs['filter_fn'] = filter_msg
207 kwargs['call_filter_on_first_line'] = True
208 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000209 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000210 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000211
maruel@chromium.org17d01792010-09-01 18:07:10 +0000212
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000213class Wrapper(object):
214 """Wraps an object, acting as a transparent proxy for all properties by
215 default.
216 """
217 def __init__(self, wrapped):
218 self._wrapped = wrapped
219
220 def __getattr__(self, name):
221 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000222
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000223
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000224class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000225 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000226 def __init__(self, wrapped, delay):
227 super(AutoFlush, self).__init__(wrapped)
228 if not hasattr(self, 'lock'):
229 self.lock = threading.Lock()
230 self.__last_flushed_at = time.time()
231 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000232
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000233 @property
234 def autoflush(self):
235 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000236
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000237 def write(self, out, *args, **kwargs):
238 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000239 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000240 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000241 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000242 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000243 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000244 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000245 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000246 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000247 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000248 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000249
250
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000251class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000252 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000253 threads with a NN> prefix.
254 """
255 def __init__(self, wrapped, include_zero=False):
256 super(Annotated, self).__init__(wrapped)
257 if not hasattr(self, 'lock'):
258 self.lock = threading.Lock()
259 self.__output_buffers = {}
260 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000261
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000262 @property
263 def annotated(self):
264 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000265
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000266 def write(self, out):
267 index = getattr(threading.currentThread(), 'index', 0)
268 if not index and not self.__include_zero:
269 # Unindexed threads aren't buffered.
270 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000271
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000272 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000273 try:
274 # Use a dummy array to hold the string so the code can be lockless.
275 # Strings are immutable, requiring to keep a lock for the whole dictionary
276 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000277 if not index in self.__output_buffers:
278 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000279 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000280 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000281 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000282 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000283
284 # Continue lockless.
285 obj[0] += out
286 while '\n' in obj[0]:
287 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000288 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000289 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000290 obj[0] = remaining
291
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000292 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000293 """Flush buffered output."""
294 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000295 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000296 try:
297 # Detect threads no longer existing.
298 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000299 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000300 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000301 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000302 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000303 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000304 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000305 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000306 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000307
308 # Don't keep the lock while writting. Will append \n when it shouldn't.
309 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000310 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000311 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
312 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000313
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000314
315def MakeFileAutoFlush(fileobj, delay=10):
316 autoflush = getattr(fileobj, 'autoflush', None)
317 if autoflush:
318 autoflush.delay = delay
319 return fileobj
320 return AutoFlush(fileobj, delay)
321
322
323def MakeFileAnnotated(fileobj, include_zero=False):
324 if getattr(fileobj, 'annotated', None):
325 return fileobj
326 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000327
328
maruel@chromium.org17d01792010-09-01 18:07:10 +0000329def CheckCallAndFilter(args, stdout=None, filter_fn=None,
330 print_stdout=None, call_filter_on_first_line=False,
331 **kwargs):
332 """Runs a command and calls back a filter function if needed.
333
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000334 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000335 print_stdout: If True, the command's stdout is forwarded to stdout.
336 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000337 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000338 character trimmed.
339 stdout: Can be any bufferable output.
340
341 stderr is always redirected to stdout.
342 """
343 assert print_stdout or filter_fn
344 stdout = stdout or sys.stdout
345 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000346 kid = subprocess2.Popen(
347 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
348 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000349
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000350 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000351 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000352
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000353 # Also, we need to forward stdout to prevent weird re-ordering of output.
354 # This has to be done on a per byte basis to make sure it is not buffered:
355 # normally buffering is done for each line, but if svn requests input, no
356 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000357 try:
358 in_byte = kid.stdout.read(1)
359 if in_byte:
360 if call_filter_on_first_line:
361 filter_fn(None)
362 in_line = ''
363 while in_byte:
364 if in_byte != '\r':
365 if print_stdout:
366 stdout.write(in_byte)
367 if in_byte != '\n':
368 in_line += in_byte
369 else:
370 filter_fn(in_line)
371 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000372 else:
373 filter_fn(in_line)
374 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000375 in_byte = kid.stdout.read(1)
376 # Flush the rest of buffered output. This is only an issue with
377 # stdout/stderr not ending with a \n.
378 if len(in_line):
379 filter_fn(in_line)
380 rv = kid.wait()
381 except KeyboardInterrupt:
382 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
383 raise
384
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000385 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000386 raise subprocess2.CalledProcessError(
387 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000388 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000389
390
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000391def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000392 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000393 real_from_dir = os.path.realpath(from_dir)
394 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000395 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000396 split_path = os.path.split(path)
397 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000398 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000399 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000400
401 # If we did not find the file in the current directory, make sure we are in a
402 # sub directory that is controlled by this configuration.
403 if path != real_from_dir:
404 entries_filename = os.path.join(path, filename + '_entries')
405 if not os.path.exists(entries_filename):
406 # If .gclient_entries does not exist, a previous call to gclient sync
407 # might have failed. In that case, we cannot verify that the .gclient
408 # is the one we want to use. In order to not to cause too much trouble,
409 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000410 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000411 "file you want to use" % (filename, path))
412 return path
413 scope = {}
414 try:
415 exec(FileRead(entries_filename), scope)
416 except SyntaxError, e:
417 SyntaxErrorToError(filename, e)
418 all_directories = scope['entries'].keys()
419 path_to_check = real_from_dir[len(path)+1:]
420 while path_to_check:
421 if path_to_check in all_directories:
422 return path
423 path_to_check = os.path.dirname(path_to_check)
424 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000425
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000426 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000427 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000428
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000429
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000430def PathDifference(root, subpath):
431 """Returns the difference subpath minus root."""
432 root = os.path.realpath(root)
433 subpath = os.path.realpath(subpath)
434 if not subpath.startswith(root):
435 return None
436 # If the root does not have a trailing \ or /, we add it so the returned
437 # path starts immediately after the seperator regardless of whether it is
438 # provided.
439 root = os.path.join(root, '')
440 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000441
442
443def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000444 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000445
rcui@google.com13595ff2011-10-13 01:25:07 +0000446 Returns nearest upper-level directory with the passed in file.
447 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000448 if not path:
449 path = os.getcwd()
450 path = os.path.realpath(path)
451 while True:
452 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000453 if os.path.exists(file_path):
454 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000455 (new_path, _) = os.path.split(path)
456 if new_path == path:
457 return None
458 path = new_path
459
460
461def GetGClientRootAndEntries(path=None):
462 """Returns the gclient root and the dict of entries."""
463 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000464 root = FindFileUpwards(config_file, path)
465 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000466 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000467 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000468 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000469 env = {}
470 execfile(config_path, env)
471 config_dir = os.path.dirname(config_path)
472 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000473
474
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000475def lockedmethod(method):
476 """Method decorator that holds self.lock for the duration of the call."""
477 def inner(self, *args, **kwargs):
478 try:
479 try:
480 self.lock.acquire()
481 except KeyboardInterrupt:
482 print >> sys.stderr, 'Was deadlocked'
483 raise
484 return method(self, *args, **kwargs)
485 finally:
486 self.lock.release()
487 return inner
488
489
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000490class WorkItem(object):
491 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000492 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
493 # As a workaround, use a single lock. Yep you read it right. Single lock for
494 # all the 100 objects.
495 lock = threading.Lock()
496
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000497 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000498 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000499 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000500
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000501 def run(self, work_queue):
502 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000503 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000504 pass
505
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000506 @property
507 def name(self):
508 return self._name
509
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000510
511class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000512 """Runs a set of WorkItem that have interdependencies and were WorkItem are
513 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000514
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000515 In gclient's case, Dependencies sometime needs to be run out of order due to
516 From() keyword. This class manages that all the required dependencies are run
517 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000518
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000519 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000520 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000521 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000522 """jobs specifies the number of concurrent tasks to allow. progress is a
523 Progress instance."""
524 # Set when a thread is done or a new item is enqueued.
525 self.ready_cond = threading.Condition()
526 # Maximum number of concurrent tasks.
527 self.jobs = jobs
528 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000529 self.queued = []
530 # List of strings representing each Dependency.name that was run.
531 self.ran = []
532 # List of items currently running.
533 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000534 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000535 self.exceptions = Queue.Queue()
536 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000537 self.progress = progress
538 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000539 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000540
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000541 self.ignore_requirements = ignore_requirements
542
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000543 def enqueue(self, d):
544 """Enqueue one Dependency to be executed later once its requirements are
545 satisfied.
546 """
547 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000548 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000549 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000550 self.queued.append(d)
551 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000552 logging.debug('enqueued(%s)' % d.name)
553 if self.progress:
554 self.progress._total = total + 1
555 self.progress.update(0)
556 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000557 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000558 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000559
560 def flush(self, *args, **kwargs):
561 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000562 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000563 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000564 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000565 while True:
566 # Check for task to run first, then wait.
567 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000568 if not self.exceptions.empty():
569 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000570 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000571 self._flush_terminated_threads()
572 if (not self.queued and not self.running or
573 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000574 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000575 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000576
577 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000578 for i in xrange(len(self.queued)):
579 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000580 if (self.ignore_requirements or
581 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000582 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000583 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000584 break
585 else:
586 # Couldn't find an item that could run. Break out the outher loop.
587 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000588
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000589 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000590 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000591 break
592 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000593 try:
594 self.ready_cond.wait(10)
595 except KeyboardInterrupt:
596 # Help debugging by printing some information:
597 print >> sys.stderr, (
598 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
599 'Running: %d') % (
600 self.jobs,
601 len(self.queued),
602 ', '.join(self.ran),
603 len(self.running)))
604 for i in self.queued:
605 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
606 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000607 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000608 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000609 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000610
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000611 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000612 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000613 # To get back the stack location correctly, the raise a, b, c form must be
614 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000615 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000616 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000617 if self.progress:
618 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000619
maruel@chromium.org3742c842010-09-09 19:27:14 +0000620 def _flush_terminated_threads(self):
621 """Flush threads that have terminated."""
622 running = self.running
623 self.running = []
624 for t in running:
625 if t.isAlive():
626 self.running.append(t)
627 else:
628 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000629 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000630 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000631 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000632 if t.item.name in self.ran:
633 raise Error(
634 'gclient is confused, "%s" is already in "%s"' % (
635 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000636 if not t.item.name in self.ran:
637 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000638
639 def _run_one_task(self, task_item, args, kwargs):
640 if self.jobs > 1:
641 # Start the thread.
642 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000643 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000644 self.running.append(new_thread)
645 new_thread.start()
646 else:
647 # Run the 'thread' inside the main thread. Don't try to catch any
648 # exception.
649 task_item.run(*args, **kwargs)
650 self.ran.append(task_item.name)
651 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000652 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000653
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000654 class _Worker(threading.Thread):
655 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000656 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000657 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000658 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000659 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000660 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000661 self.args = args
662 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000663
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000664 def run(self):
665 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000666 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000667 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000668 try:
669 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000670 except Exception:
671 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000672 logging.info('Caught exception in thread %s' % self.item.name)
673 logging.info(str(sys.exc_info()))
674 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000675 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000676
maruel@chromium.org3742c842010-09-09 19:27:14 +0000677 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000678 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000679 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000680 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000681 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000682
683
684def GetEditor(git):
685 """Returns the most plausible editor to use."""
686 if git:
687 editor = os.environ.get('GIT_EDITOR')
688 else:
689 editor = os.environ.get('SVN_EDITOR')
690 if not editor:
691 editor = os.environ.get('EDITOR')
692 if not editor:
693 if sys.platform.startswith('win'):
694 editor = 'notepad'
695 else:
696 editor = 'vim'
697 return editor
698
699
700def RunEditor(content, git):
701 """Opens up the default editor in the system to get the CL description."""
702 file_handle, filename = tempfile.mkstemp(text=True)
703 # Make sure CRLF is handled properly by requiring none.
704 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000705 print >> sys.stderr, (
706 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000707 fileobj = os.fdopen(file_handle, 'w')
708 # Still remove \r if present.
709 fileobj.write(re.sub('\r?\n', '\n', content))
710 fileobj.close()
711
712 try:
713 cmd = '%s %s' % (GetEditor(git), filename)
714 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
715 # Msysgit requires the usage of 'env' to be present.
716 cmd = 'env ' + cmd
717 try:
718 # shell=True to allow the shell to handle all forms of quotes in
719 # $EDITOR.
720 subprocess2.check_call(cmd, shell=True)
721 except subprocess2.CalledProcessError:
722 return None
723 return FileRead(filename)
724 finally:
725 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000726
727
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000728def UpgradeToHttps(url):
729 """Upgrades random urls to https://.
730
731 Do not touch unknown urls like ssh:// or git://.
732 Do not touch http:// urls with a port number,
733 Fixes invalid GAE url.
734 """
735 if not url:
736 return url
737 if not re.match(r'[a-z\-]+\://.*', url):
738 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
739 # relative url and will use http:///foo. Note that it defaults to http://
740 # for compatibility with naked url like "localhost:8080".
741 url = 'http://%s' % url
742 parsed = list(urlparse.urlparse(url))
743 # Do not automatically upgrade http to https if a port number is provided.
744 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
745 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000746 return urlparse.urlunparse(parsed)
747
748
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000749def ParseCodereviewSettingsContent(content):
750 """Process a codereview.settings file properly."""
751 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
752 try:
753 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
754 except ValueError:
755 raise Error(
756 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000757 def fix_url(key):
758 if keyvals.get(key):
759 keyvals[key] = UpgradeToHttps(keyvals[key])
760 fix_url('CODE_REVIEW_SERVER')
761 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000762 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000763
764
765def NumLocalCpus():
766 """Returns the number of processors.
767
768 Python on OSX 10.6 raises a NotImplementedError exception.
769 """
770 try:
771 import multiprocessing
772 return multiprocessing.cpu_count()
773 except: # pylint: disable=W0702
774 # Mac OS 10.6 only
775 # pylint: disable=E1101
776 return int(os.sysconf('SC_NPROCESSORS_ONLN'))