blob: 041813b0d164322a816669b175e3f7259ac109cb [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
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000161
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000162def safe_makedirs(tree):
163 """Creates the directory in a safe manner.
164
165 Because multiple threads can create these directories concurently, trap the
166 exception and pass on.
167 """
168 count = 0
169 while not os.path.exists(tree):
170 count += 1
171 try:
172 os.makedirs(tree)
173 except OSError, e:
174 # 17 POSIX, 183 Windows
175 if e.errno not in (17, 183):
176 raise
177 if count > 40:
178 # Give up.
179 raise
180
181
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000182def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000183 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000184
maruel@chromium.org17d01792010-09-01 18:07:10 +0000185 If |always| is True, a message indicating what is being done
186 is printed to stdout all the time even if not output is generated. Otherwise
187 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000188 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000189 stdout = kwargs.setdefault('stdout', sys.stdout)
190 if header is None:
191 header = "\n________ running '%s' in '%s'\n" % (
192 ' '.join(args), kwargs.get('cwd', '.'))
193
maruel@chromium.org17d01792010-09-01 18:07:10 +0000194 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000195 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000196 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000197 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000198 def filter_msg(line):
199 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000200 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000201 elif filter_fn:
202 filter_fn(line)
203 kwargs['filter_fn'] = filter_msg
204 kwargs['call_filter_on_first_line'] = True
205 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000206 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000207 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000208
maruel@chromium.org17d01792010-09-01 18:07:10 +0000209
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000210class Wrapper(object):
211 """Wraps an object, acting as a transparent proxy for all properties by
212 default.
213 """
214 def __init__(self, wrapped):
215 self._wrapped = wrapped
216
217 def __getattr__(self, name):
218 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000219
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000220
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000221class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000222 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000223 def __init__(self, wrapped, delay):
224 super(AutoFlush, self).__init__(wrapped)
225 if not hasattr(self, 'lock'):
226 self.lock = threading.Lock()
227 self.__last_flushed_at = time.time()
228 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000229
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000230 @property
231 def autoflush(self):
232 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000233
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000234 def write(self, out, *args, **kwargs):
235 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000236 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000237 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000238 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000239 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000240 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000241 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000242 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000243 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000244 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000245 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000246
247
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000248class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000249 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000250 threads with a NN> prefix.
251 """
252 def __init__(self, wrapped, include_zero=False):
253 super(Annotated, self).__init__(wrapped)
254 if not hasattr(self, 'lock'):
255 self.lock = threading.Lock()
256 self.__output_buffers = {}
257 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000258
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000259 @property
260 def annotated(self):
261 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000262
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000263 def write(self, out):
264 index = getattr(threading.currentThread(), 'index', 0)
265 if not index and not self.__include_zero:
266 # Unindexed threads aren't buffered.
267 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000268
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000269 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000270 try:
271 # Use a dummy array to hold the string so the code can be lockless.
272 # Strings are immutable, requiring to keep a lock for the whole dictionary
273 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000274 if not index in self.__output_buffers:
275 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000276 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000277 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000278 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000279 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000280
281 # Continue lockless.
282 obj[0] += out
283 while '\n' in obj[0]:
284 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000285 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000286 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000287 obj[0] = remaining
288
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000289 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000290 """Flush buffered output."""
291 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000292 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000293 try:
294 # Detect threads no longer existing.
295 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000296 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000297 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000298 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000299 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000300 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000301 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000302 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000303 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000304
305 # Don't keep the lock while writting. Will append \n when it shouldn't.
306 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000307 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000308 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
309 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000310
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000311
312def MakeFileAutoFlush(fileobj, delay=10):
313 autoflush = getattr(fileobj, 'autoflush', None)
314 if autoflush:
315 autoflush.delay = delay
316 return fileobj
317 return AutoFlush(fileobj, delay)
318
319
320def MakeFileAnnotated(fileobj, include_zero=False):
321 if getattr(fileobj, 'annotated', None):
322 return fileobj
323 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000324
325
maruel@chromium.org17d01792010-09-01 18:07:10 +0000326def CheckCallAndFilter(args, stdout=None, filter_fn=None,
327 print_stdout=None, call_filter_on_first_line=False,
328 **kwargs):
329 """Runs a command and calls back a filter function if needed.
330
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000331 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000332 print_stdout: If True, the command's stdout is forwarded to stdout.
333 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000334 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000335 character trimmed.
336 stdout: Can be any bufferable output.
337
338 stderr is always redirected to stdout.
339 """
340 assert print_stdout or filter_fn
341 stdout = stdout or sys.stdout
342 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000343 kid = subprocess2.Popen(
344 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
345 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000346
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000347 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000348 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000349
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000350 # Also, we need to forward stdout to prevent weird re-ordering of output.
351 # This has to be done on a per byte basis to make sure it is not buffered:
352 # normally buffering is done for each line, but if svn requests input, no
353 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000354 try:
355 in_byte = kid.stdout.read(1)
356 if in_byte:
357 if call_filter_on_first_line:
358 filter_fn(None)
359 in_line = ''
360 while in_byte:
361 if in_byte != '\r':
362 if print_stdout:
363 stdout.write(in_byte)
364 if in_byte != '\n':
365 in_line += in_byte
366 else:
367 filter_fn(in_line)
368 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000369 else:
370 filter_fn(in_line)
371 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000372 in_byte = kid.stdout.read(1)
373 # Flush the rest of buffered output. This is only an issue with
374 # stdout/stderr not ending with a \n.
375 if len(in_line):
376 filter_fn(in_line)
377 rv = kid.wait()
378 except KeyboardInterrupt:
379 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
380 raise
381
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000382 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000383 raise subprocess2.CalledProcessError(
384 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000385 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000386
387
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000388def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000389 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000390 real_from_dir = os.path.realpath(from_dir)
391 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000392 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000393 split_path = os.path.split(path)
394 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000395 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000396 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000397
398 # If we did not find the file in the current directory, make sure we are in a
399 # sub directory that is controlled by this configuration.
400 if path != real_from_dir:
401 entries_filename = os.path.join(path, filename + '_entries')
402 if not os.path.exists(entries_filename):
403 # If .gclient_entries does not exist, a previous call to gclient sync
404 # might have failed. In that case, we cannot verify that the .gclient
405 # is the one we want to use. In order to not to cause too much trouble,
406 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000407 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000408 "file you want to use" % (filename, path))
409 return path
410 scope = {}
411 try:
412 exec(FileRead(entries_filename), scope)
413 except SyntaxError, e:
414 SyntaxErrorToError(filename, e)
415 all_directories = scope['entries'].keys()
416 path_to_check = real_from_dir[len(path)+1:]
417 while path_to_check:
418 if path_to_check in all_directories:
419 return path
420 path_to_check = os.path.dirname(path_to_check)
421 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000422
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000423 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000424 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000425
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000426
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000427def PathDifference(root, subpath):
428 """Returns the difference subpath minus root."""
429 root = os.path.realpath(root)
430 subpath = os.path.realpath(subpath)
431 if not subpath.startswith(root):
432 return None
433 # If the root does not have a trailing \ or /, we add it so the returned
434 # path starts immediately after the seperator regardless of whether it is
435 # provided.
436 root = os.path.join(root, '')
437 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000438
439
440def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000441 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000442
rcui@google.com13595ff2011-10-13 01:25:07 +0000443 Returns nearest upper-level directory with the passed in file.
444 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000445 if not path:
446 path = os.getcwd()
447 path = os.path.realpath(path)
448 while True:
449 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000450 if os.path.exists(file_path):
451 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000452 (new_path, _) = os.path.split(path)
453 if new_path == path:
454 return None
455 path = new_path
456
457
458def GetGClientRootAndEntries(path=None):
459 """Returns the gclient root and the dict of entries."""
460 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000461 root = FindFileUpwards(config_file, path)
462 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000463 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000464 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000465 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000466 env = {}
467 execfile(config_path, env)
468 config_dir = os.path.dirname(config_path)
469 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000470
471
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000472def lockedmethod(method):
473 """Method decorator that holds self.lock for the duration of the call."""
474 def inner(self, *args, **kwargs):
475 try:
476 try:
477 self.lock.acquire()
478 except KeyboardInterrupt:
479 print >> sys.stderr, 'Was deadlocked'
480 raise
481 return method(self, *args, **kwargs)
482 finally:
483 self.lock.release()
484 return inner
485
486
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000487class WorkItem(object):
488 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000489 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
490 # As a workaround, use a single lock. Yep you read it right. Single lock for
491 # all the 100 objects.
492 lock = threading.Lock()
493
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000494 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000495 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000496 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000497
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000498 def run(self, work_queue):
499 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000500 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000501 pass
502
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000503 @property
504 def name(self):
505 return self._name
506
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000507
508class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000509 """Runs a set of WorkItem that have interdependencies and were WorkItem are
510 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000511
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000512 In gclient's case, Dependencies sometime needs to be run out of order due to
513 From() keyword. This class manages that all the required dependencies are run
514 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000515
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000516 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000517 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000518 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000519 """jobs specifies the number of concurrent tasks to allow. progress is a
520 Progress instance."""
521 # Set when a thread is done or a new item is enqueued.
522 self.ready_cond = threading.Condition()
523 # Maximum number of concurrent tasks.
524 self.jobs = jobs
525 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000526 self.queued = []
527 # List of strings representing each Dependency.name that was run.
528 self.ran = []
529 # List of items currently running.
530 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000531 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000532 self.exceptions = Queue.Queue()
533 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000534 self.progress = progress
535 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000536 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000537
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000538 self.ignore_requirements = ignore_requirements
539
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000540 def enqueue(self, d):
541 """Enqueue one Dependency to be executed later once its requirements are
542 satisfied.
543 """
544 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000545 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000546 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000547 self.queued.append(d)
548 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000549 logging.debug('enqueued(%s)' % d.name)
550 if self.progress:
551 self.progress._total = total + 1
552 self.progress.update(0)
553 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000554 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000555 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000556
557 def flush(self, *args, **kwargs):
558 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000559 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000560 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000561 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000562 while True:
563 # Check for task to run first, then wait.
564 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000565 if not self.exceptions.empty():
566 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000567 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000568 self._flush_terminated_threads()
569 if (not self.queued and not self.running or
570 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000571 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000572 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000573
574 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000575 for i in xrange(len(self.queued)):
576 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000577 if (self.ignore_requirements or
578 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000579 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000580 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000581 break
582 else:
583 # Couldn't find an item that could run. Break out the outher loop.
584 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000585
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000586 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000587 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000588 break
589 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000590 try:
591 self.ready_cond.wait(10)
592 except KeyboardInterrupt:
593 # Help debugging by printing some information:
594 print >> sys.stderr, (
595 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
596 'Running: %d') % (
597 self.jobs,
598 len(self.queued),
599 ', '.join(self.ran),
600 len(self.running)))
601 for i in self.queued:
602 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
603 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000604 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000605 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000606 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000607
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000608 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000609 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000610 # To get back the stack location correctly, the raise a, b, c form must be
611 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000612 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000613 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000614 if self.progress:
615 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000616
maruel@chromium.org3742c842010-09-09 19:27:14 +0000617 def _flush_terminated_threads(self):
618 """Flush threads that have terminated."""
619 running = self.running
620 self.running = []
621 for t in running:
622 if t.isAlive():
623 self.running.append(t)
624 else:
625 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000626 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000627 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000628 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000629 if t.item.name in self.ran:
630 raise Error(
631 'gclient is confused, "%s" is already in "%s"' % (
632 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000633 if not t.item.name in self.ran:
634 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000635
636 def _run_one_task(self, task_item, args, kwargs):
637 if self.jobs > 1:
638 # Start the thread.
639 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000640 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000641 self.running.append(new_thread)
642 new_thread.start()
643 else:
644 # Run the 'thread' inside the main thread. Don't try to catch any
645 # exception.
646 task_item.run(*args, **kwargs)
647 self.ran.append(task_item.name)
648 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000649 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000650
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000651 class _Worker(threading.Thread):
652 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000653 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000654 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000655 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000656 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000657 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000658 self.args = args
659 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000660
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000661 def run(self):
662 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000663 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000664 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000665 try:
666 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000667 except Exception:
668 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000669 logging.info('Caught exception in thread %s' % self.item.name)
670 logging.info(str(sys.exc_info()))
671 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000672 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000673
maruel@chromium.org3742c842010-09-09 19:27:14 +0000674 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000675 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000676 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000677 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000678 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000679
680
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000681def GetEditor(git, git_editor=None):
682 """Returns the most plausible editor to use.
683
684 In order of preference:
685 - GIT_EDITOR/SVN_EDITOR environment variable
686 - core.editor git configuration variable (if supplied by git-cl)
687 - VISUAL environment variable
688 - EDITOR environment variable
689 - vim (non-Windows) or notepad (Windows)
690
691 In the case of git-cl, this matches git's behaviour, except that it does not
692 include dumb terminal detection.
693
694 In the case of gcl, this matches svn's behaviour, except that it does not
695 accept a command-line flag or check the editor-cmd configuration variable.
696 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000697 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000698 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000699 else:
700 editor = os.environ.get('SVN_EDITOR')
701 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000702 editor = os.environ.get('VISUAL')
703 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000704 editor = os.environ.get('EDITOR')
705 if not editor:
706 if sys.platform.startswith('win'):
707 editor = 'notepad'
708 else:
709 editor = 'vim'
710 return editor
711
712
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000713def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000714 """Opens up the default editor in the system to get the CL description."""
715 file_handle, filename = tempfile.mkstemp(text=True)
716 # Make sure CRLF is handled properly by requiring none.
717 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000718 print >> sys.stderr, (
719 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000720 fileobj = os.fdopen(file_handle, 'w')
721 # Still remove \r if present.
722 fileobj.write(re.sub('\r?\n', '\n', content))
723 fileobj.close()
724
725 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000726 editor = GetEditor(git, git_editor=git_editor)
727 if not editor:
728 return None
729 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000730 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
731 # Msysgit requires the usage of 'env' to be present.
732 cmd = 'env ' + cmd
733 try:
734 # shell=True to allow the shell to handle all forms of quotes in
735 # $EDITOR.
736 subprocess2.check_call(cmd, shell=True)
737 except subprocess2.CalledProcessError:
738 return None
739 return FileRead(filename)
740 finally:
741 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000742
743
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000744def UpgradeToHttps(url):
745 """Upgrades random urls to https://.
746
747 Do not touch unknown urls like ssh:// or git://.
748 Do not touch http:// urls with a port number,
749 Fixes invalid GAE url.
750 """
751 if not url:
752 return url
753 if not re.match(r'[a-z\-]+\://.*', url):
754 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
755 # relative url and will use http:///foo. Note that it defaults to http://
756 # for compatibility with naked url like "localhost:8080".
757 url = 'http://%s' % url
758 parsed = list(urlparse.urlparse(url))
759 # Do not automatically upgrade http to https if a port number is provided.
760 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
761 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000762 return urlparse.urlunparse(parsed)
763
764
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000765def ParseCodereviewSettingsContent(content):
766 """Process a codereview.settings file properly."""
767 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
768 try:
769 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
770 except ValueError:
771 raise Error(
772 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000773 def fix_url(key):
774 if keyvals.get(key):
775 keyvals[key] = UpgradeToHttps(keyvals[key])
776 fix_url('CODE_REVIEW_SERVER')
777 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000778 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000779
780
781def NumLocalCpus():
782 """Returns the number of processors.
783
784 Python on OSX 10.6 raises a NotImplementedError exception.
785 """
786 try:
787 import multiprocessing
788 return multiprocessing.cpu_count()
789 except: # pylint: disable=W0702
790 # Mac OS 10.6 only
791 # pylint: disable=E1101
792 return int(os.sysconf('SC_NPROCESSORS_ONLN'))